LearnOpenGL学习笔记—入门05:Texture
LearnOpenGL学习笔记—入门05:Texture
小学期终于结束了(并且摸鱼了一个星期后),可以回来更新了Orz,总是拖拖拉拉的(瘫
0 前言
本节笔记对应的内容 纹理
在入门01中我们配置好了环境
在入门02中我们可以检测输入并出现一个有颜色的窗口
在入门03中我们初步学习了图形渲染管线,尝试用不同方法画出了三角形和四边形
在入门04(上)中我们学习了shader和GLSL的相关知识,并给图形加上了变换的颜色以及彩色。
在入门04(下)中我们建立自己的shader类,并能够从外部读取shader的内容。
在这一节,我们将会了解有关材质的内容
1 纹理简介
在之前的学习中,我们可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。
但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色,这将会产生很多额外开销。
程序员更喜欢使用纹理(Texture)。
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;可以想象纹理是一张墙纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像墙纸的外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
2 stb_image.h
使用纹理之前要做的第一件事是把它们加载到我们的应用中。
所以我们如何才能把这些图像加载到应用中呢?
使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说要用的stb_image.h库。
在这里下载,进去以后点选Raw,在新出现的页面中,右键另存为,存到自己的目录里,在VS中右键,添加现有项目,将这个库导进来。
3 纹理坐标
如何将这么一份材质映射(Map)到一个三角形上呢?
我们需要指定三角形的每个顶点,各自对应纹理的哪个部分。
这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样。
之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标在x和y轴上,范围为0到1之间(2D纹理图像)。
使用纹理坐标获取纹理颜色叫做采样(Sampling)。
纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
4 纹理环绕方式/纹理过滤
关于纹理环绕方式,原教材感觉写的蛮精简了,不过需要注意的是,导入的材质图片最好是2的幂次方的边长
关于纹理过滤
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel)映射到纹理坐标。
当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。
OpenGL有对于纹理过滤(Texture Filtering)的选项。
纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
- GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。
当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
下图中有四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
- GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)是基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。
一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
下图中可以看到返回的颜色是邻近像素的混合色:
但是想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。
有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。
由于远处的物体所产生的片段很少,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它只拾取一个纹理颜色却要跨过纹理很大的部分。
在小物体上这会产生不真实的感觉,并且对它们使用高分辨率纹理很浪费内存。
OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。
- 多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。
让我们看一下多级渐远纹理是什么样子的:
- 手工为每个纹理图像创建一系列多级渐远纹理很麻烦,OpenGL有一个glGenerateMipmaps函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。后面的教程中会学会使用它。
- 在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。
就像普通的纹理过滤一样,切换多级渐远纹理级别时,也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。
为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
5 代码实现/彩色木箱
在前次代码的基础上,我们做一些小修改
我们把这两行放入main函数开头
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
使用网站自带的vertices数组
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
索引绘制
unsigned int indices[] = {
0,1,2,
2,3,0
};
顶点属性
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// uv属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
这时候运行我们会得到一个彩色长方形(当然这时候uv坐标没有起作用)
下面改一下shader的代码,在vertexSource中进行这样的修改
#version 330 core
layout(location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
layout(location = 2) in vec2 aTexCoord; // uv变量的属性位置值为 2
out vec4 vertexColor;
out vec2 TexCoord;
void main(){
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
vertexColor = vec4(aColor,1.0);
TexCoord = aTexCoord;
}
在fragmentSource中也做修改,预设的第一个sampler2D会给到我们的图片
#version 330 core
in vec4 vertexColor;
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
void main(){
FragColor = texture(ourTexture,TexCoord);
}
我们所用的一个木箱材质图片可以点这里下载,放到项目目录,同时在VS中项目添加现有项,把图片添加进来。
在讲EBO时用过这个图,现在可以把材质的部分加上,我们在texbuffer中放入图片,绑定TEXTURE_2D的通道输入给context
我们在写完EBO之后的代码部分,进行生成纹理
unsigned int TexBuffer;
glGenTextures(1, &TexBuffer);
glBindTexture(GL_TEXTURE_2D, TexBuffer);
glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中,然后绑定它
接下来为当前绑定的纹理对象设置环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
- 第一个参数指定了纹理目标,我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。
第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴。
最后一个参数需要我们传递一个环绕方式(Wrapping),在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为GL_REPEAT。 - 如果我们选择GL_CLAMP_TO_BORDER的环绕方式,我们还需要指定一个边缘的颜色。
这需要使用glTexParameter函数的fv后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
接下来为当前绑定的纹理对象设置过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
- 使用glTexParameter*函数为放大和缩小指定过滤方式,和纹理环绕方式的设置很相似:
- 因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,如果要为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。
接下来加载并生成纹理
int width, height, nrChannel;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
printf("Failed to load texture");
}
stbi_image_free(data);
- glTexImage2D
第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)
第二个参数为纹理指定多级渐远纹理的级别,这里填0,也就是基本级别。
第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
下个参数应该总是被设为0(历史遗留的问题)。
第七第八个参数定义了源图的格式和数据类型。
我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
最后一个参数是真正的图像数据。 - 如果要使用多级渐远纹理,生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
- 生成了纹理和相应的多级渐远纹理后,释放图像的内存。
接下来在glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器,放在while循环中:
glBindTexture(GL_TEXTURE_2D, TexBuffer);
然后实现之后就会如图的效果
或者可以改一改fragmentSource,来让箱子变七彩
线性代数中不存在两个四维向量相乘,这里的乘号是openGL定义的4对4的乘法
FragColor = texture(ourTexture,TexCoord)*vertexColor;
6 纹理单元/多材质代码
接下来会用到一个笑脸图片,点这里可以下载到项目目录,然后在vs中加入现有项
关于多个材质,其实是运用纹理单元。
一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理。
如下图右边TEXTURE_2D就像一个在方框中滑动的槽位,在不同槽位可以绑上不同材质。
将之前代码中的TexBuffer都替换成TexBufferA,并且激活对应槽位,如下举例
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
第二张材质同第一张的代码样式导入,名字改成B,然后激活到其他槽位再bind上去
unsigned int TexBufferB;
glGenTextures(1, &TexBufferB);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
unsigned char *data2 = stbi_load("awesomeface.png", &width, &height, &nrChannel, 0);
if (data2) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
printf("Failed to load texture");
}
stbi_image_free(data2);
修改fragmentSource
#version 330 core
in vec4 vertexColor;
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform sampler2D ourFace;
void main(){
FragColor = mix(texture(ourTexture,TexCoord),texture(ourFace,TexCoord),texture(ourFace,TexCoord).a*0.2);
}
渲染循环也做相应修改
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
myShader->use();
glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 1);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
此时出现的图像会上下颠倒,所以在材质的代码前加上一句
stbi_set_flip_vertically_on_load(true);
然后运行
7 完整代码
以下给出main.cpp以及两个shader的完整代码,其他的可见前几节
mian.cpp
#include <iostream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include "Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
//0 1 2 2 3 0
unsigned int indices[] = {
0,1,2,
2,3,0
};
void processInput(GLFWwindow* window){
if (glfwGetKey(window, GLFW_KEY_ESCAPE )== GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//Open GLFW Window
GLFWwindow* window = glfwCreateWindow(800,600,"My OpenGL Game",NULL,NULL);
if(window == NULL)
{
printf("Open window failed.");
glfwTerminate();
return - 1;
}
glfwMakeContextCurrent(window);
//Init GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glViewport(0, 0, 800, 600);
//glEnable(GL_CULL_FACE);
//glCullFace(GL_BACK);
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
Shader* myShader = new Shader("vertexSource.txt", "fragmentSource.txt");
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// uv属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
stbi_set_flip_vertically_on_load(true);
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
int width, height, nrChannel;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
printf("Failed to load texture");
}
stbi_image_free(data);
unsigned int TexBufferB;
glGenTextures(1, &TexBufferB);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
unsigned char *data2 = stbi_load("awesomeface.png", &width, &height, &nrChannel, 0);
if (data2) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
printf("Failed to load texture");
}
stbi_image_free(data2);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
myShader->use();
glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 1);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
vertexSource.txt
#version 330 core
layout(location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
layout(location = 2) in vec2 aTexCoord; // uv变量的属性位置值为 2
out vec4 vertexColor;
out vec2 TexCoord;
void main(){
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
vertexColor = vec4(aColor,1.0);
TexCoord = aTexCoord;
}
fragmentSource.txt
#version 330 core
in vec4 vertexColor;
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform sampler2D ourFace;
void main(){
FragColor = mix(texture(ourTexture,TexCoord),texture(ourFace,TexCoord),texture(ourFace,TexCoord).a*0.2);
}