现代 OpenGL 纹理使用流程 (The Cherno's Method)
现代 OpenGL 纹理使用流程 (The Cherno's Method)
Rif1. 核心概念:什么是纹理?
- 不仅仅是图片:在 OpenGL 中,“纹理 (Texture)”是一个广义的概念。它是一个数据容器,可以存储一维、二维或三维的数据阵列。最常见的用途是存储 2D 图像的颜色信息,但也可以用来存储法线(法线贴图)、高度(高度图)、光照查找表等。
- **纹理单元 (Texture Unit)**:GPU 内部有多个可以放置纹理的“插槽”,这些插槽被称为纹理单元。通常至少有16或32个(
GL_TEXTURE0,GL_TEXTURE1, …)。你可以将不同的纹理放入不同的单元,然后在着色器中同时访问它们,实现多重纹理混合等效果。 - **采样器 (Sampler)**:在 GLSL 着色器中,我们通过
sampler2D(或sampler3D等) 这样的特殊uniform变量来访问纹理。采样器知道去哪个纹理单元里读取数据。
2. C++ 封装:创建一个 Texture 类 (RAII)
为了方便管理和复用,我们将所有与纹理相关的 OpenGL 操作封装在一个 Texture 类中。这个类遵循 RAII (Resource Acquisition Is Initialization) 原则。
Texture.h (接口定义)
1 |
|
Texture.cpp (核心实现逻辑)
构造函数 Texture::Texture(const std::string& path) 的步骤:
**翻转图像 (重要)**:
- OpenGL 的纹理坐标 (0,0) 在左下角,而大多数图像文件格式(如 PNG, JPG)的 (0,0) 在左上角。
- 在加载图像前,必须告诉
stb_image库在加载时垂直翻转图像,以匹配 OpenGL 的坐标系。 stbi_set_flip_vertically_on_load(1);
从文件加载图像数据到 CPU 内存:
- 使用
stb_image库加载图像。 m_LocalBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP, 4);- 最后一个参数
4强制stb_image将图像转换为 RGBA 格式(4个通道),这简化了后续 OpenGL 的处理。
- 使用
生成并绑定一个 OpenGL 纹理对象:
glGenTextures(1, &m_RendererID);:向 GPU 申请一个纹理对象的 ID。glBindTexture(GL_TEXTURE_2D, m_RendererID);:将新创建的纹理对象绑定到GL_TEXTURE_2D这个目标上,表示我们接下来要操作的是一个2D纹理。
**设置纹理参数 (Filtering & Wrapping)**:
- 这些参数告诉 OpenGL 当纹理需要被放大/缩小时,或者当纹理坐标超出
[0, 1]范围时,应该如何处理。 - **Filtering (过滤)**:
GL_TEXTURE_MIN_FILTER: 纹理被缩小时的过滤方式。GL_LINEAR表示线性过滤(平滑过渡)。GL_TEXTURE_MAG_FILTER: 纹理被放大时的过滤方式。GL_LINEAR。
- **Wrapping (环绕)**:
GL_TEXTURE_WRAP_S: 水平方向(U/S轴)的环绕方式。GL_CLAMP_TO_EDGE表示将超出范围的坐标截断到边缘,避免边缘出现奇怪的颜色。GL_TEXTURE_WRAP_T: 垂直方向(V/T轴)的环绕方式。
- 调用:
glTexParameteri(GL_TEXTURE_2D, ...);(注意是glTex...而不是glTexture...)
- 这些参数告诉 OpenGL 当纹理需要被放大/缩小时,或者当纹理坐标超出
将 CPU 上的图像数据上传到 GPU:
- 这是最关键的一步,将
m_LocalBuffer中的像素数据发送到当前绑定的纹理对象中。 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer);GL_TEXTURE_2D: 目标是2D纹理。0: Mipmap 的级别,0是基础级别。GL_RGBA8: GPU 内部存储格式。告诉 GPU 用8位来存储每个通道(R, G, B, A)。m_Width,m_Height: 纹理的尺寸。0: 历史遗留参数,必须为0。GL_RGBA: 源图像数据的格式。告诉 OpenGL 我们提供的数据是 RGBA 顺序。GL_UNSIGNED_BYTE: 源图像数据每个通道的数据类型。m_LocalBuffer: 指向 CPU 内存中图像数据的指针。
- 这是最关键的一步,将
清理:
glBindTexture(GL_TEXTURE_2D, 0);:上传完成后,解绑纹理是一种好习惯。stbi_image_free(m_LocalBuffer);:图像数据已经被复制到 GPU 显存,CPU 上的这份拷贝就不再需要了,必须释放以避免内存泄漏。
其他成员函数的实现:
Texture::~Texture(): 在析构函数中调用glDeleteTextures(1, &m_RendererID);来释放 GPU 资源。Texture::Bind(unsigned int slot):glActiveTexture(GL_TEXTURE0 + slot);:激活指定的纹理单元。glBindTexture(GL_TEXTURE_2D, m_RendererID);:将本纹理对象绑定到这个激活的单元上。
Texture::Unbind():glBindTexture(GL_TEXTURE_2D, 0);
3. GLSL 着色器中的交互
顶点着色器 (Vertex Shader)
- 接收纹理坐标作为顶点属性,并将其传递给片段着色器。
1
2
3
4
5
6
7
8
9
10
11
layout(location = 0) in vec4 position;
layout(location = 1) in vec2 texCoord; // 从VBO接收纹理坐标
out vec2 v_TexCoord; // 将纹理坐标输出给片段着色器
void main()
{
gl_Position = position;
v_TexCoord = texCoord;
}
片段着色器 (Fragment Shader)
- 接收从顶点着色器传来的纹理坐标,并使用
sampler2Duniform 来进行纹理采样。1
2
3
4
5
6
7
8
9
10
11
12
13
out vec4 color;
in vec2 v_TexCoord; // 接收插值后的纹理坐标
uniform sampler2D u_Texture; // 纹理采样器
void main()
{
// 使用 texture() 函数进行采样
vec4 texColor = texture(u_Texture, v_TexCoord);
color = texColor;
}
4. 渲染循环中的使用流程 (Application.cpp)
初始化阶段:
- 创建
Shader对象。 - 创建
Texture对象。 - 关键连接:告诉着色器
u_Texture这个采样器应该去哪个纹理单元找纹理。1
2
3
4
5
6Texture texture("res/textures/my_texture.png");
Shader shader("res/shaders/Basic.shader");
shader.Bind();
// 告诉 shader, u_Texture 对应的是 0 号纹理单元
shader.SetUniform1i("u_Texture", 0);
- 创建
**渲染循环内部 (每一帧)**:
- 在调用绘制命令 (
glDraw...) 之前,必须确保所有需要的状态都已设置好。 - 绑定着色器。
- 绑定纹理到它被指定的那个纹理单元。
- 绑定 VAO。
- 发出绘制命令。
1
2
3
4
5
6
7
8
9
10// In while loop...
shader.Bind();
// 告诉 shader 去 0 号槽找纹理。这一步其实在初始化时做一次就行,因为uniform的值会保持。
// shader.SetUniform1i("u_Texture", 0);
// 激活 0 号纹理单元,并将我们的纹理绑定上去。
texture.Bind(0);
va.Bind();
glDrawElements(...);
- 在调用绘制命令 (
这个流程确保了 GPU 在执行绘制时,能够正确地找到纹理数据,并根据顶点提供的纹理坐标,将其正确地“贴”到物体表面。