纹理映射
纹理是一种将图像数据映射到物体表面的技术,可以极大地增加渲染的真实感和细节。本章将介绍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有多个纹理单元,每个单元可以绑定一个纹理:
| 纹理单元 | 宏定义 | 说明 |
|---|---|---|
| 0 | GL_TEXTURE0 | 默认纹理单元 |
| 1 | GL_TEXTURE1 | |
| ... | ... | |
| 15 | GL_TEXTURE15 | OpenGL 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);
}
};
常见问题
纹理显示为黑色
检查以下几点:
- 图像是否成功加载
- 纹理单元是否正确设置
- 着色器中的uniform是否正确设置
- 纹理格式是否与图像格式匹配
纹理上下颠倒
使用stb_image时,需要翻转Y轴:
stbi_set_flip_vertically_on_load(true);
纹理边缘有缝隙
确保纹理坐标正确,或使用 GL_CLAMP_TO_EDGE 环绕方式。
小结
纹理映射是增加渲染细节的重要技术:
- 纹理坐标定义了顶点到纹理图像的映射
- 纹理参数控制环绕和过滤方式
- Mipmap可以提高远距离纹理的质量和性能
- 多纹理可以实现复杂的混合效果
下一章我们将学习 变换,理解如何在3D空间中移动和旋转物体。
性能建议
- 使用合适的纹理尺寸(2的幂次方)
- 使用压缩纹理格式减少显存占用
- 为不同距离使用mipmap
- 批量绑定纹理减少状态切换