跳到主要内容

纹理映射

纹理是一种将图像数据映射到物体表面的技术,可以极大地增加渲染的真实感和细节。本章将介绍OpenGL中纹理的基本概念和使用方法。

什么是纹理?

纹理是一张可以应用到物体表面的图像。通过纹理映射,我们可以为物体添加丰富的细节,而不需要增加额外的几何复杂度。

例如,一个木质桌面可以通过以下两种方式实现:

  • 使用数千个多边形模拟木纹(复杂且低效)
  • 使用一个简单的矩形加上木纹纹理(简单且高效)

纹理坐标

纹理坐标定义了顶点如何对应到纹理图像上的位置。OpenGL使用2D纹理坐标系统,范围从(0,0)到(1,1):

纹理坐标系统:
(0,1) ─────────── (1,1)
│ │
│ 纹理图像 │
│ │
(0,0) ─────────── (1,0)

设置纹理坐标

float vertices[] = {
// 位置 // 纹理坐标
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f // 左上
};

创建纹理

生成纹理对象

GLuint 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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

纹理环绕方式

当纹理坐标超出[0,1]范围时,环绕方式决定如何处理:

参数值说明
GL_REPEAT重复纹理
GL_MIRRORED_REPEAT镜像重复
GL_CLAMP_TO_EDGE边缘拉伸
GL_CLAMP_TO_BORDER边框颜色
GL_REPEAT:          GL_MIRRORED_REPEAT:    GL_CLAMP_TO_EDGE:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ A │ A │ │ A │ A'│ │ A │ A │
├───┼───┤ ├───┼───┤ ├───┼───┤
│ A │ A │ │ A'│ A │ │ A │ A │
└───┴───┘ └───┴───┘ └───┴───┘
重复 镜像重复 边缘拉伸

纹理过滤方式

当纹理被放大或缩小时,过滤方式决定如何采样:

放大过滤(GL_TEXTURE_MAG_FILTER)

  • GL_NEAREST:最近邻,像素化效果
  • GL_LINEAR:线性插值,平滑效果

缩小过滤(GL_TEXTURE_MIN_FILTER)

  • GL_NEAREST:最近邻
  • GL_LINEAR:线性插值
  • GL_NEAREST_MIPMAP_NEAREST:使用最近的mipmap,最近邻采样
  • GL_LINEAR_MIPMAP_NEAREST:使用最近的mipmap,线性采样
  • GL_NEAREST_MIPMAP_LINEAR:线性插值两个mipmap,最近邻采样
  • GL_LINEAR_MIPMAP_LINEAR:线性插值两个mipmap,线性采样(最高质量)

Mipmap

Mipmap是一系列预先计算好的、逐级缩小的纹理图像。当物体距离较远时,使用较小的mipmap可以提高性能和减少闪烁。

// 生成mipmap
glGenerateMipmap(GL_TEXTURE_2D);

加载纹理图像

使用stb_image库

stb_image是一个单头文件的图像加载库,支持多种格式:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

int width, height, nrChannels;
unsigned char* data = stbi_load("texture.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::cerr << "Failed to load texture" << std::endl;
}

stbi_image_free(data);

glTexImage2D参数说明

void glTexImage2D(
GLenum target, // 目标纹理类型
GLint level, // mipmap级别
GLint internalformat, // 内部格式
GLsizei width, // 宽度
GLsizei height, // 高度
GLint border, // 边框(必须为0)
GLenum format, // 像素数据格式
GLenum type, // 像素数据类型
const void* data // 像素数据
);

处理不同图像格式

// 根据通道数选择格式
GLenum format;
if (nrChannels == 1)
format = GL_RED;
else if (nrChannels == 3)
format = GL_RGB;
else if (nrChannels == 4)
format = GL_RGBA;

glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);

在着色器中使用纹理

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = aTexCoord;
}

片段着色器

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;

void main() {
FragColor = texture(texture1, TexCoord);
}

设置纹理单元

glUseProgram(shaderProgram);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);

// 激活纹理单元并绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);

多纹理

OpenGL支持同时使用多个纹理,通常用于实现复杂效果。

绑定多个纹理

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1);

片段着色器混合纹理

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main() {
// 线性混合两个纹理
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.5);
}

纹理单元

OpenGL有多个纹理单元,每个单元可以绑定一个纹理:

纹理单元宏定义说明
0GL_TEXTURE0默认纹理单元
1GL_TEXTURE1
......
15GL_TEXTURE15OpenGL 3.3至少支持16个
// 激活纹理单元
glActiveTexture(GL_TEXTURE0 + index); // 或 GL_TEXTUREn

// 绑定纹理到当前激活的单元
glBindTexture(GL_TEXTURE_2D, texture);

完整示例

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <iostream>
#include "shader.h"

int main() {
// 初始化GLFW和GLAD
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

GLFWwindow* window = glfwCreateWindow(800, 600, "Texture Example", NULL, NULL);
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

// 顶点数据
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
};

unsigned int indices[] = {0, 1, 3, 1, 2, 3};

// 创建VAO、VBO、EBO
GLuint VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(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);

// 创建纹理
GLuint 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_MIPMAP_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::cerr << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

// 创建着色器程序
Shader ourShader("vertex.glsl", "fragment.glsl");
ourShader.use();
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);

// 渲染循环
while (!glfwWindowShouldClose(window)) {
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);

ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glfwSwapBuffers(window);
glfwPollEvents();
}

// 清理
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);

glfwTerminate();
return 0;
}

纹理封装类

class Texture {
public:
GLuint ID;
std::string type;

Texture(const char* path, const std::string& typeName) : type(typeName) {
glGenTextures(1, &ID);

int width, height, nrComponents;
unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);

if (data) {
GLenum format;
if (nrComponents == 1) format = GL_RED;
else if (nrComponents == 3) format = GL_RGB;
else if (nrComponents == 4) format = GL_RGBA;

glBindTexture(GL_TEXTURE_2D, ID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

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);
} else {
std::cerr << "Texture failed to load at path: " << path << std::endl;
}

stbi_image_free(data);
}

void bind(GLuint unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, ID);
}

~Texture() {
glDeleteTextures(1, &ID);
}
};

常见问题

纹理显示为黑色

检查以下几点:

  1. 图像是否成功加载
  2. 纹理单元是否正确设置
  3. 着色器中的uniform是否正确设置
  4. 纹理格式是否与图像格式匹配

纹理上下颠倒

使用stb_image时,需要翻转Y轴:

stbi_set_flip_vertically_on_load(true);

纹理边缘有缝隙

确保纹理坐标正确,或使用 GL_CLAMP_TO_EDGE 环绕方式。

小结

纹理映射是增加渲染细节的重要技术:

  • 纹理坐标定义了顶点到纹理图像的映射
  • 纹理参数控制环绕和过滤方式
  • Mipmap可以提高远距离纹理的质量和性能
  • 多纹理可以实现复杂的混合效果

下一章我们将学习 变换,理解如何在3D空间中移动和旋转物体。

性能建议
  1. 使用合适的纹理尺寸(2的幂次方)
  2. 使用压缩纹理格式减少显存占用
  3. 为不同距离使用mipmap
  4. 批量绑定纹理减少状态切换