纹理(Texture)
- 纹理(Texture) 是一个2D图像(也有1D和3D纹理存在)用于给一个对象添加细节信息。
- 注意:与图像相似,纹理也可以用于存储一个大型的任意数据集合并发送给着色器。
- 为了映射纹理,我们需要为顶点关联一个纹理坐标(texture coordinate) 来指定从纹理那部分进行取样(sample)。纹理坐标值的范围在x和y坐标轴上都为0到1。使用纹理坐标来获取纹理颜色就称为采样(sampling)。三角形的纹理坐标如下所示:
float texCoords[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.5f, 1.0f
}
1. 纹理扭曲(texture wrapping)
-
当纹理坐标落在(0, 0)和(1, 1)范围外OpenGL可能的处理方式:(下图取自书中)
-
GL_REPEAT
:重复纹理图像(默认行为)。 -
GL_MIRRORED_REPEAT
:与GL_REPEAT
行为一样,只是每次重复时都镜像图像。 -
GL_CLAMP_TO_EDGE
:相当于将纹理图形进行拉伸。 -
GL_CLAMP_TO_BORDER
:位于范围外的坐标被指定为一个边界颜色值。
-
使用函数
glTexParameter*
设置上述选项,其中s, t和r分别对应坐标轴的x, y和z。
// 设置x坐标轴范围外坐标处理方式为重复镜像纹理图形
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
// 设置y坐标轴范围外坐标处理方式为重复镜像纹理图形
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
1. 第一个参数设置纹理目标,该示例指定2D纹理坐标。
2. 第二个参数指定要设置的坐标轴。
3. 第三个参数指定纹理扭曲选项。
- 如果设置为
GL_CLAMP_TO_BORDER
需设置一个边界颜色:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glRexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
2. 纹理过滤(texture filtering)
- 纹理过滤:纹理坐标不取决于图像的分辨率,而是可以是任何浮点数,因此OpenGL需要知道如何将纹理坐标映射到纹理像素(也称为texel)。
- OpenGL两种常见和重要的过滤方式:
-
GL_NEAREST
:也称为最邻近(nearest neighbor)或点过滤,OpenGL直接选择中心离纹理坐标最近的纹理元素。 -
GL_LINEAR
:也称为线性过滤,采用线性插值算法从纹理坐标邻近像素估算颜色值。
-
- 纹理过滤通过设置放大(magnifying)和缩减(minifying)操作来实现:
// 缩减时使用最邻近过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// 放大时使用线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-
mipmaps:简单说就是一个纹理图像序列组成的集合,其中每个子纹理是其前者的二分之一。mipmaps的使用原理就是,物体与观察者的距离每经过一个阈值,OpenGL就会使用适合该距离不同的mipmap纹理。(下图取自书中)
-
mipmaps纹理图像不同层级之间的切换可能造成边缘锐化,像普通纹理过滤一样,也可以为切换mipmap层级设置不同的过滤方法,四个选项如下:
GL_NEAREST_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR_MIPMAP_LINEAR
mipmap层级过滤的设置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-
注意:纹理放大不使用mipmaps,给纹理放大设置mipmap过滤选项会产生一个OpenGL的
GL_INVALID_ENUM
错误编码。
3. 加载和创建纹理
- 使用单头文件图像加载类库stb_image.h
- 下载:stb_image.h
- 使用:将头文件stb_image.h添加到你的项目;创建一个C++文件添加以下内容:
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h"
- 加载图像:
int width, height, nChannels; unsigned char* data = stbi_load("container.jpg", &width, &height, &nChannels, 0);
4. 生成一个纹理
- 创建纹理对象
unsigned int texture;
glGenTextures(1, &texture);
- 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture);
- 生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 生成当前绑定纹理所需的mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
1. 第1个参数:指定纹理目标。
2. 第2个参数:指定我们创建的纹理的mipmap层级。
3. 第3个参数:告诉OpenGL纹理的存储格式。
4. 第4和5个参数:纹理的宽高。
5. 第6个参数:总是0.
6. 第7和8个参数:指定源图像的格式和数据类型。
7. 最后一个参数:实际图像数据。
- 释放图像数据
stbi_image_free(data);
5. 应用纹理
- 顶点数据:包含位置坐标,颜色数据和纹理坐标。
float vertices[] = {
// positions // colors // texture
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(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
- 修改顶点着色器:添加纹理属性输入并设置纹理坐标和一个颜色传递到片元着色器
#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;
}
- GLSL中内置一种纹理对象的数据类型,称为取样器(sampler),携带一个指定纹理类型的后缀,如
sampler1D
,sampler3D
或sampler2D
。 - 修改片元着色器:读取纹理和接受顶点着色器的纹理坐标和颜色
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
// 使用GLSL内置的texture函数从纹理取样颜色
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
}
- 渲染循环中绑定纹理和绘制图形
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
-
渲染效果
- 混合纹理颜色和顶点颜色
// 片元着色器修改
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
6. 纹理单元(Texture Units)
- 使用
glUniform1i
函数我们可以给纹理取样器(texture sampler)设置一个location
值,这样我们就可以在一个片元着色器设置多个纹理。这个纹理的location
通常称为纹理单元。一个纹理默认的纹理单元是0,所以我们前面的程序没有设置纹理单元值。 - 纹理单元的主要目的就是让我们可以在一个着色器中使用不止一个纹理。只要我们在绑定纹理前激活相应的纹理单元,我们就可以绑定多个纹理。
glActiveTexture(GL_TEXTURE0); // 先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture1);
- OpenGL至少有16个纹理单元可以使用,
GL_TEXTURE0
到GL_TEXTURE15
。它们是按顺序定义的,所以如果我们想要获取GL_TEXTURE8
,可以使用GL_TEXTURE0+8
。 - 修改片元着色器以包含两个纹理取样器
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
// 第一个纹理取色80%,第二个纹理取色20%
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
- 加载第二个纹理图像(png格式图像,格式设置为
GL_RGBA
)
unsigned char* data2 = stbi_load("./Panda.png", &width, &height, &nChannels, 0);
if (data2)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture2" << std::endl;
}
stbi_image_free(data2);
- 设置片元着色器纹理取样器的uniform变量
ourShader.use();
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手工设置
ourShader.setInt("texture2", 1); // 使用自定义着色器类设置
- 渲染循环中激活和绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
-
渲染效果