OpenGL / OpenCL 编程指南 / 第 5 章:纹理映射
第 5 章:纹理映射
纹理(Texture)是将 2D 图像"贴"到 3D 表面的技术。本章涵盖纹理的创建、加载、采样配置以及性能优化策略。
5.1 纹理基础概念
5.1.1 什么是纹理?
纹理是存储在 GPU 显存中的图像数据。着色器通过 UV 坐标(也称纹理坐标)采样纹理,获取对应位置的颜色值。
3D 模型表面 纹理图像 (2D)
┌──────────────┐ ┌──────────────┐
│ v3 │ │ │
│ / \ │ UV映射 │ (0,1)──(1,1)
│ / \ │ ──────▶ │ │ 像素 │ │
│ v1───v2 │ │ (0,0)──(1,0)
└──────────────┘ └──────────────┘
5.1.2 UV 坐标系统
| 概念 | 说明 |
|---|---|
| U (水平) | 0.0 = 左边,1.0 = 右边 |
| V (垂直) | 0.0 = 底边(OpenGL),1.0 = 顶边 |
| 范围 | 通常 [0, 1],超出范围由 Wrapping 模式决定 |
⚠️ OpenGL 的 V 轴方向与图片文件相反:图片文件通常从上到下存储,OpenGL 纹理从下到上。stb_image 可以翻转加载:
stbi_set_flip_vertically_on_load(true)。
5.2 创建纹理对象
5.2.1 完整的纹理创建流程
// ===== 1. 生成纹理对象 =====
unsigned int texture;
glGenTextures(1, &texture);
// ===== 2. 绑定纹理 =====
glBindTexture(GL_TEXTURE_2D, texture);
// ===== 3. 设置纹理参数 =====
// 环绕模式(超出 [0,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_MIPMAP_LINEAR);
// 放大过滤(纹理像素 > 屏幕像素时)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// ===== 4. 加载图片数据 =====
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true);
unsigned char *data = stbi_load("assets/textures/container.jpg", &width, &height, &nrChannels, 0);
if (data) {
GLenum format = (nrChannels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
} else {
std::cerr << "Failed to load texture" << std::endl;
}
// ===== 5. 释放图片内存 =====
stbi_image_free(data);
5.2.2 glTexImage2D 参数详解
glTexImage2D(
GL_TEXTURE_2D, // 目标纹理类型
0, // Mipmap 级别(0 = 基础级别)
GL_RGB, // GPU 内部存储格式
width, height, // 纹理尺寸
0, // 历史遗留参数,必须为 0
GL_RGB, // 源数据格式
GL_UNSIGNED_BYTE, // 源数据类型
data // 图片数据指针
);
5.2.3 内部格式对照表
| 格式 | 每像素大小 | 说明 |
|---|---|---|
GL_RGB | 3 字节 | 无透明度 |
GL_RGBA | 4 字节 | 带透明度 |
GL_RED | 1 字节 | 灰度 |
GL_RG | 2 字节 | 双通道 |
GL_RGB16F | 6 字节 | HDR 纹理(16 位浮点) |
GL_RGBA32F | 16 字节 | 高精度浮点纹理 |
GL_DEPTH_COMPONENT24 | 3 字节 | 深度纹理 |
5.3 纹理环绕模式(Wrapping)
当 UV 坐标超出 [0, 1] 范围时的处理方式:
| 模式 | 效果 | 示意 |
|---|---|---|
GL_REPEAT | 平铺重复 | 常规贴图 |
GL_MIRRORED_REPEAT | 镜像重复 | 无缝镜像 |
GL_CLAMP_TO_EDGE | 重复边缘像素 | 纯色边框 |
GL_CLAMP_TO_BORDER | 使用指定边框颜色 | 自定义边框 |
// 设置边框颜色(CLAMP_TO_BORDER 模式)
float borderColor[] = { 1.0f, 0.0f, 0.0f, 1.0f }; // 红色边框
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
GL_REPEAT: GL_MIRRORED_REPEAT: GL_CLAMP_TO_EDGE:
┌─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┐
│A│B│A│B│A│ │A│B││B│A│A│ │A│A│A│A│A│
├─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┤
│C│D│C│D│C│ │C│D││D│C│C│ │C│C│C│C│C│
└─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┘
5.4 纹理过滤(Filtering)
5.4.1 问题背景
纹理上的一个像素(texel)不一定对应屏幕上的一个像素。过滤策略决定了如何处理这种不匹配。
5.4.2 过滤模式
| 模式 | 效果 | 性能 | 质量 |
|---|---|---|---|
GL_NEAREST | 最近邻采样(像素风) | 最快 | 低 |
GL_LINEAR | 双线性插值(平滑) | 中等 | 中 |
GL_NEAREST (放大): GL_LINEAR (放大):
┌─┬─┬─┐ ┌─┬─┬─┐
│█│ │ │ → 采样最近的像素 │▒│▒│▒│ → 4 个最近像素的加权平均
├─┼─┼─┤ ├─┼─┼─┤
│ │ │ │ │▒│▒│▒│
└─┴─┴─┘ └─┴─┴─┘
5.4.3 Mipmap 过滤(缩小)
当纹理在屏幕上变小时(如远处的物体),Mipmap 提供了预计算的低分辨率版本:
| 模式 | 说明 |
|---|---|
GL_NEAREST_MIPMAP_NEAREST | 选择最接近的 Mipmap 级别,最近邻采样 |
GL_LINEAR_MIPMAP_NEAREST | 选择最接近的 Mipmap 级别,线性插值 |
GL_NEAREST_MIPMAP_LINEAR | 在两个 Mipmap 级别间插值,每个级别最近邻 |
GL_LINEAR_MIPMAP_LINEAR | 三线性过滤:在两个 Mipmap 级别间双线性插值 |
💡 推荐设置:缩小用
GL_LINEAR_MIPMAP_LINEAR(三线性),放大用GL_LINEAR。注意放大时不能使用 Mipmap 模式,否则行为未定义。
5.5 Mipmap 详解
5.5.1 Mipmap 链
Level 0: 1024×1024 ← 原始纹理
Level 1: 512×512
Level 2: 256×256
Level 3: 128×128
Level 4: 64×64
Level 5: 32×32
Level 6: 16×16
Level 7: 8×8
Level 8: 4×4
Level 9: 2×2
Level 10: 1×1
每个级别的面积是上一级的 1/4,因此 Mipmap 额外占用约 1/3 的原始纹理内存。
5.5.2 自动 Mipmap 生成
glGenerateMipmap(GL_TEXTURE_2D); // 自动生成所有 Mipmap 级别
5.5.3 手动加载指定 Mipmap 级别
// 手动为每个级别指定不同的纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, data_level0);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGB, 128, 128, 0, GL_RGB, GL_UNSIGNED_BYTE, data_level1);
// ...
5.5.4 各向异性过滤
// 需要 GL_EXT_texture_filter_anisotropic 扩展
float maxAniso;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso);
各向异性过滤改善了斜角观察时纹理的模糊问题,是现代游戏中最常用的纹理质量设置之一。
5.6 纹理单元(Texture Units)
5.6.1 多纹理绑定
OpenGL 有多个纹理单元(通常 16~80 个),可以同时绑定多张纹理:
// 绑定纹理到不同单元
glActiveTexture(GL_TEXTURE0); // 激活纹理单元 0
glBindTexture(GL_TEXTURE_2D, diffuseTexture);
glActiveTexture(GL_TEXTURE1); // 激活纹理单元 1
glBindTexture(GL_TEXTURE_2D, specularTexture);
glActiveTexture(GL_TEXTURE2); // 激活纹理单元 2
glBindTexture(GL_TEXTURE_2D, normalMapTexture);
// 设置着色器中的 sampler 采样哪个单元
shader.use();
shader.setInt("material.diffuse", 0); // 采样纹理单元 0
shader.setInt("material.specular", 1); // 采样纹理单元 1
shader.setInt("material.normal", 2); // 采样纹理单元 2
5.6.2 纹理单元数量查询
int maxUnits;
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxUnits);
printf("Max texture units: %d\n", maxUnits); // 通常 16 或更多
5.7 纹理类型
5.7.1 纹理类型速查
| 类型 | 目标常量 | 典型用途 |
|---|---|---|
| 2D 纹理 | GL_TEXTURE_2D | 漫反射贴图、UI 图片 |
| 1D 纹理 | GL_TEXTURE_1D | 渐变色、查找表 |
| 3D 纹理 | GL_TEXTURE_3D | 体积数据(医学影像) |
| 立方体贴图 | GL_TEXTURE_CUBE_MAP | 天空盒、环境反射 |
| 2D 数组 | GL_TEXTURE_2D_ARRAY | 动画帧序列、地形图层 |
| 多重采样 | GL_TEXTURE_2D_MULTISAMPLE | MSAA 抗锯齿 |
5.7.2 立方体贴图(Cubemap)
// 加载 6 个面
const char* faces[6] = {
"right.jpg", "left.jpg",
"top.jpg", "bottom.jpg",
"front.jpg", "back.jpg"
};
unsigned int cubemap;
glGenTextures(1, &cubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap);
for (int i = 0; i < 6; i++) {
int w, h, ch;
unsigned char* data = stbi_load(faces[i], &w, &h, &ch, 0);
if (data) {
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB,
w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
}
stbi_image_free(data);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
// GLSL 中采样立方体贴图
in vec3 vDirection; // 3D 方向向量
uniform samplerCube skybox;
void main() {
FragColor = texture(skybox, vDirection);
}
5.8 纹理格式优化
5.8.1 压缩纹理格式
| 格式 | 每像素 | 平台 | 说明 |
|---|---|---|---|
GL_COMPRESSED_RGB_S3TC_DXT1 | 0.5 字节 | 桌面 | 无 Alpha |
GL_COMPRESSED_RGBA_S3TC_DXT5 | 1 字节 | 桌面 | 带 Alpha |
GL_COMPRESSED_RGB8_ETC2 | 0.5 字节 | 移动端 | OpenGL ES 3.0+ |
GL_COMPRESSED_RGBA_ASTC_4x4 | 1 字节 | 移动端 | 高质量,可变块大小 |
// 使用压缩格式加载
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5,
width, height, 0, imageSize, compressedData);
💡 压缩纹理可以直接上传到 GPU,无需 CPU 解压。GPU 硬件支持实时解压,几乎零性能开销。
5.9 完整示例:带纹理的矩形
顶点数据(带 UV)
float vertices[] = {
// 位置 // 颜色 // UV
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, 3, 1, 2, 3 };
顶点着色器
#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 vColor;
out vec2 vTexCoord;
void main() {
gl_Position = vec4(aPos, 1.0);
vColor = aColor;
vTexCoord = aTexCoord;
}
片段着色器
#version 460 core
in vec3 vColor;
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform float mixFactor;
void main() {
vec4 texColor = texture(ourTexture, vTexCoord);
FragColor = mix(texColor, vec4(vColor, 1.0), mixFactor);
}
渲染代码
// 加载纹理
unsigned int texture = loadTexture("assets/textures/container.jpg");
// 渲染循环中
shader.use();
shader.setFloat("mixFactor", 0.2f);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
shader.setInt("ourTexture", 0);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
5.10 注意事项
⚠️ 纹理尺寸应为 2 的幂次(如 256, 512, 1024)。非 2 的幂纹理(NPOT)在某些旧硬件上可能导致 Mipmap 生成失败或性能下降。OpenGL 4.x 已支持 NPOT,但保持 2 的幂仍是最佳实践。
⚠️ 忘记
glGenerateMipmap:使用 Mipmap 过滤模式但未生成 Mipmap,会导致纹理显示为黑色。
⚠️ stb_image 的坐标翻转:OpenGL 的纹理坐标 (0,0) 在左下角,图片文件从左上角开始。必须调用
stbi_set_flip_vertically_on_load(true)。
⚠️ 纹理单元与 Sampler 的对应:
sampler2D的值是纹理单元编号(不是纹理 ID)。忘记设置会导致所有 sampler 默认采样纹理单元 0。
5.11 业务场景
场景 1:地形渲染
使用多层纹理(草地、岩石、雪地)+ 混合权重贴图实现自然地形着色。
场景 2:天空盒
6 面立方体贴图创建天空环境。配合 IBL(基于图像的照明)实现环境反射。
场景 3:字体渲染
FreeType 库生成字体位图 → 上传为纹理 → 着色器中采样绘制文字。
5.12 扩展阅读
| 资源 | 说明 |
|---|---|
| Learn OpenGL - Textures | 纹理入门教程 |
| stb_image 文档 | 图片加载库 |
| 纹理压缩格式概述 | Khronos 纹理格式文档 |
本章小结
- 纹理通过 UV 坐标采样,UV 范围 [0,1],可配置超出范围的环绕模式
- 过滤模式:GL_NEAREST(像素风)和 GL_LINEAR(平滑)
- Mipmap 为缩小的纹理提供预计算的低分辨率版本,节省带宽并减少锯齿
- 纹理单元允许同时绑定多张纹理,通过 sampler 索引
- 立方体贴图用于天空盒和环境映射
- 压缩纹理格式节省显存并提高加载速度
上一章:第 4 章:GLSL 着色语言 下一章:第 6 章:坐标变换