强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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_RGB3 字节无透明度
GL_RGBA4 字节带透明度
GL_RED1 字节灰度
GL_RG2 字节双通道
GL_RGB16F6 字节HDR 纹理(16 位浮点)
GL_RGBA32F16 字节高精度浮点纹理
GL_DEPTH_COMPONENT243 字节深度纹理

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_MULTISAMPLEMSAA 抗锯齿

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_DXT10.5 字节桌面无 Alpha
GL_COMPRESSED_RGBA_S3TC_DXT51 字节桌面带 Alpha
GL_COMPRESSED_RGB8_ETC20.5 字节移动端OpenGL ES 3.0+
GL_COMPRESSED_RGBA_ASTC_4x41 字节移动端高质量,可变块大小
// 使用压缩格式加载
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 章:坐标变换