1. 流程
1.1 单个纹理
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节,为了能够使用纹理图片,我们需要一个叫做stb_image.h的头文件库来加载不同格式的图片作为纹理(全部文件),得到该头文件后,加入项目,并且新建一个叫做stb_image_wrap.cpp的文件,写入:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
然后在主程序里加入头文件:
#include "stb_image.h"
这样就可以加载自定义纹理图了(加载前使用stbi_set_flip_vertically_on_load(true);保证读取图片不会上下颠倒)
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
这里我们使用stbi_load读取图片数据,顺便获取宽高和颜色通道数等信息,接下来要做的其实就是新建绑定纹理了,这和之前的VBO很相似:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
做完这些,我们还有一些纹理的性质需要定义:
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
头两行是设定纹理坐标超过范围时该怎么着色:
后两行是设定在放大和缩小图片时我们应该对纹理进行什么形式的插值:
设定完性质以后,我们对之前加载的图片数据进行检测,如果有效,则利用glTexImage2D生成纹理(
第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
下个参数应该总是被设为0(历史遗留的问题)。
第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
最后一个参数是真正的图像数据。
)和利用glGenerateMipmap生成mipmap(多级渐远纹理),最后释放数据:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
那么纹理加载好了,我们该怎么确定哪部分的纹理加载到图形的哪部分呢?
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角,如下:
我们一样可以将纹理的映射写进着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
我们获取顶点对应的纹理坐标以后将其传入片段着色器,利用采样器sampler获取对应坐标的纹理采样:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
//顶点输入
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 // 左上
};
修改了输入的顶点,自然也要在设置顶点属性的时候做一些修改:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//启用位置0顶点属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);//启用颜色顶点属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);//启用纹理坐标顶点属性
那么就到了最后的渲染时刻,每次渲染前,绑定一次纹理即可
ourShader.use();
glBindTexture(GL_TEXTURE_2D, texture);//绑定纹理,自动把纹理赋值给片段着色器的采样器
glBindVertexArray(VAO); // 一般每次使用时再绑定,这里只有一个VAO,所以实际上可以不绑
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
效果:
1.2 两个纹理混合
如果我们一次要用到多个纹理怎么办?这时候之前片段着色器里texture的uniform修饰就派上用场了,我们可以定义两个纹理单元,然后用mix函数定义纹理颜色为texture1的80%和texture2的20%:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
接下来我们分别创建绑定两个纹理,注意在读取和存储jpg格式的笑脸图数据时是用的GL_RGBA格式:
//绑定纹理
glBindTexture(GL_TEXTURE_2D, texture[0]);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
stbi_set_flip_vertically_on_load(true);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
glBindTexture(GL_TEXTURE_2D, texture[1]);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
stbi_set_flip_vertically_on_load(true);
// 加载并生成纹理
data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
渲染之前我们要告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面:
ourShader.use();
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
渲染中每次我们绑定纹理前要首先激活对应的纹理单元:
//使用着色器程序
ourShader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture[0]);//绑定纹理,自动把纹理赋值给片段着色器的采样器
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture[1]);//绑定纹理,自动把纹理赋值给片段着色器的采样器
glBindVertexArray(VAO); // 一般每次使用时再绑定,这里只有一个VAO,所以实际上可以不绑
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
效果:
2. 练习
2.1 修改片段着色器,仅让笑脸图案朝另一个方向看
仅让笑脸纹理坐标左右颠倒即可
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, vec2(1 - TexCoord.x, TexCoord.y)), 0.2);
}
效果:
2.2 尝试用不同的纹理环绕方式,设定一个从0.0f到2.0f范围内的(而不是原来的0.0f到1.0f)纹理坐标。试试看能不能在箱子的角落放置4个笑脸
环绕方式为重复,扩大2倍纹理坐标即可:
//顶点输入
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 2.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 2.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, 2.0f // 左上
};
效果:
2.3 尝试在矩形上只显示纹理图像的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用GL_NEAREST的纹理过滤方式让像素显示得更清晰
缩小纹理坐标即可:
//顶点输入
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.1f, 0.1f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.1f, 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, 0.1f // 左上
};
纹理的放大性质选择GL_NEAREST:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
效果:
2.4 使用一个uniform变量作为mix函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度
如题,我们定义一个uniform变量作为mix函数的第三个参数
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform vec2 visibility;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, vec2(1 - TexCoord.x, TexCoord.y)), visibility.x);
}
然后我们在渲染循环里面获取这个变量的位置,一旦检测到按下上下键,就改变它的可见度(因为可见度范围在0-1之间,所以这里要加一个限制)
int vertexColorLocation = glGetUniformLocation(ourShader.ID, "visibility");
//使用着色器程序
ourShader.use();
if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
visi = std::fmin(visi + 0.001, 1);
else if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
visi = std::fmax(visi - 0.001, 0);
glUniform2f(vertexColorLocation, visi, 1.0f);
效果:
同样的我们也可以用之前提到的随时间变化:
// 渲染指令
float timeValue = glfwGetTime();
float visi = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(ourShader.ID, "visibility");
//使用着色器程序
ourShader.use();
glUniform2f(vertexColorLocation, visi, 1.0f);
效果: