在纹理映射过程中,二维纹理图像上的像素位置(即纹理坐标)会被映射到三维模型的顶点上。
过程解析
-
纹理ID (
textureID
)textureID
是由glGenTextures
创建的纹理对象的 ID。这个 ID 用于标识和操作纹理对象。textureID
会被传递给glBindTexture(GL_TEXTURE_2D, textureID)
,将纹理绑定到当前上下文中,后续的纹理操作都会作用于该纹理。
-
纹理单元 (
GL_TEXTURE0
)GL_TEXTURE0
是 OpenGL 中的一个纹理单元。它是通过glActiveTexture(GL_TEXTURE0)
激活的,表示接下来的纹理绑定操作会作用于纹理单元 0。- 然后通过
glBindTexture(GL_TEXTURE_2D, textureID)
将纹理绑定到当前纹理单元 0 上。
-
纹理坐标 (
TexCoords
)- 顶点着色器通过
in vec2 TexCoords
接收每个顶点的纹理坐标,并传递给片段着色器(out vec2 TexCoords
)。 - 在片段着色器中,
TexCoords
被用来从纹理中采样颜色值:FragColor = texture(texture1, TexCoords);
。
- 顶点着色器通过
-
纹理采样器 (
texture1
)texture1
是一个uniform sampler2D
类型的变量,代表片段着色器中的纹理采样器。- 在渲染循环中,
glUniform1i
被用来将texture1
采样器与纹理单元 0 关联:glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0)
。 - 这样,片段着色器会从纹理单元 0 绑定的纹理中采样颜色。
完整代码:
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stb_image.h> // 用于加载图片
#include <iostream>
// 着色器代码
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置
layout (location = 1) in vec2 aTexCoords; // 纹理坐标
out vec2 TexCoords; // 传递给片段着色器的纹理坐标
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoords = aTexCoords; // 将纹理坐标传递给片段着色器
}
)";
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
in vec2 TexCoords; // 从顶点着色器传递的纹理坐标
uniform sampler2D texture1; // 纹理采样器
void main() {
FragColor = texture(texture1, TexCoords); // 根据纹理坐标从纹理中采样
}
)";
// 创建着色器程序的辅助函数
GLuint createShaderProgram(const char* vertexShaderSource, const char* fragmentShaderSource) {
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
checkShaderCompile(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
checkShaderCompile(fragmentShader);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
checkProgramLink(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
// 检查着色器编译错误
void checkShaderCompile(GLuint shader) {
GLint success;
GLchar infoLog[512];
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cout << "Shader Compilation Failed: " << infoLog << std::endl;
}
}
// 检查着色器程序链接错误
void checkProgramLink(GLuint program) {
GLint success;
GLchar infoLog[512];
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(program, 512, nullptr, infoLog);
std::cout << "Program Linking Failed: " << infoLog << std::endl;
}
}
// 加载纹理的函数
GLuint loadTexture(const char* path) {
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID); // 绑定纹理对象
// 设置纹理的环绕和过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载图片
int width, height, nrChannels;
unsigned char* data = stbi_load(path, &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); // 生成 Mipmap
stbi_image_free(data);
} else {
std::cout << "Failed to load texture!" << std::endl;
}
return textureID;
}
// 主程序
int main() {
// 初始化 GLFW
if (!glfwInit()) {
std::cout << "GLFW Initialization Failed!" << std::endl;
return -1;
}
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Texture Mapping", nullptr, nullptr);
if (!window) {
std::cout << "Window Creation Failed!" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
});
// 初始化 GLEW
if (glewInit() != GLEW_OK) {
std::cout << "GLEW Initialization Failed!" << std::endl;
return -1;
}
// 创建着色器程序
GLuint shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);
// 定义顶点数据(位置和纹理坐标)
float vertices[] = {
// 位置 // 纹理坐标
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f
};
// 创建 VAO 和 VBO
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 加载纹理
GLuint textureID = loadTexture("texture.jpg");
// 渲染循环
while (!glfwWindowShouldClose(window)) {
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 激活着色器程序
glUseProgram(shaderProgram);
// 激活纹理单元 0 并绑定纹理
glActiveTexture(GL_TEXTURE0); // 激活纹理单元 GL_TEXTURE0
glBindTexture(GL_TEXTURE_2D, textureID); // 绑定纹理
// 将纹理采样器传递给着色器
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0); // 将 texture1 与纹理单元 0 关联
// 绘制物体
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glfwSwapBuffers(window);
glfwPollEvents();
}
// 清理资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
纹理坐标和三维坐标的关系
纹理坐标是二维的(通常为 u
和 v
坐标),而三维坐标是三维的(通常为 x
, y
, z
坐标)。纹理坐标通常范围是 [0, 1]
,其中:
u
表示纹理在水平方向上的位置(即 X 轴方向)。v
表示纹理在垂直方向上的位置(即 Y 轴方向)。
纹理映射的基本过程是通过一个数学关系将三维顶点的纹理坐标(u
, v
)与纹理图像的二维像素坐标进行映射,进而确定每个三维点在纹理图像中的相应位置。
如何将纹理坐标映射到三维表面
-
模型坐标系与纹理坐标系的关系
在 OpenGL 渲染中,我们通常通过 UV坐标来表示纹理坐标。每个顶点都配有一个纹理坐标,用来告诉 OpenGL 渲染引擎该纹理在该顶点上的具体位置。UV坐标的值通常在
[0, 1]
范围内,分别表示纹理图片的最左边和最下边(u=0, v=0
)以及最右边和最上边(u=1, v=1
)。 -
顶点数据中的纹理坐标
- 每个顶点除了有 三维位置坐标(
x
,y
,z
)外,还会有一个纹理坐标(u
,v
),它决定了该顶点在纹理图像上的位置。 - 这些纹理坐标通常会在建模时生成,并随模型一起导入到 OpenGL。
- 每个顶点除了有 三维位置坐标(
-
纹理坐标与顶点坐标的映射关系
- 假设我们有一个三角形网格模型,其中每个顶点都有对应的纹理坐标。OpenGL 会将这些纹理坐标与加载的纹理图像进行关联。
- 通过 顶点着色器,这些纹理坐标会传递给 片段着色器,在片段着色器中根据纹理坐标从纹理图像中采样颜色。
-
纹理坐标插值
- 在渲染过程中,OpenGL 会根据三角形的顶点坐标进行 插值(interpolation),即对于三角形内的每个片段(像素),它会根据邻近顶点的纹理坐标对纹理坐标进行插值,从而获得该片段的纹理坐标。
- 这样,片段着色器就可以使用插值后的纹理坐标从纹理图像中采样正确的像素。
详细过程
步骤 1:加载纹理图片
我们可以使用像 stb_image.h
这样的第三方库来加载图像文件。stb_image
是一个非常流行的图像加载库,支持多种图像格式(例如 PNG, JPEG, BMP 等)。
首先,确保包含 stb_image.h
库并在编译时定义 STB_IMAGE_IMPLEMENTATION
。
GLuint loadTexture(const char* path) {
int width, height, nrChannels;
// 加载图像文件
unsigned char *data = stbi_load(path, &width, &height, &nrChannels, 0);
if (data) {
GLuint textureID;
glGenTextures(1, &textureID); // 生成纹理ID
glBindTexture(GL_TEXTURE_2D, textureID); // 绑定纹理
// 设置纹理的环绕和过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // x轴重复
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // y轴重复
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 线性放大
// 根据图像数据生成纹理
GLenum format = (nrChannels == 3) ? GL_RGB : GL_RGBA; // 判断是否为RGB或RGBA
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D); // 生成纹理的mipmap
stbi_image_free(data); // 释放加载的图像数据
return textureID;
} else {
std::cout << "Failed to load texture" << std::endl;
return 0;
}
}
glTexImage2D
用来将图像数据(像素数据)上传到纹理对象中,并将其存储在 GPU 内存中。
步骤 2:创建纹理对象
在上面的代码中,我们使用 glGenTextures
创建了一个纹理对象textureID,并使用 glTexImage2D
将加载的图像数据绑定到纹理对象中。
步骤 3:定义顶点数据
接下来,我们定义 3D 模型的顶点数据。对于一个简单的立方体,我们需要定义每个顶点的坐标和纹理坐标。
每个顶点包含 3 个位置坐标(x, y, z
)和 2 个纹理坐标(u, v
)。纹理坐标 u
和 v
映射到纹理图片的水平和垂直位置。
GLfloat vertices[] = {
// 位置 // 纹理坐标 (u, v)
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
};
步骤 4:设置纹理坐标
通过顶点属性指示如何访问纹理坐标。纹理坐标通常在片段着色器中使用。
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
// 绑定顶点数据到 VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 纹理坐标属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
步骤 5:编写着色器
接下来,我们编写顶点着色器和片段着色器,处理纹理映射。
-
顶点着色器:传递纹理坐标
在顶点着色器中,我们将顶点的纹理坐标从模型空间传递到片段着色器:
#version 330 core layout (location = 0) in vec3 aPos; // 顶点位置 layout (location = 1) in vec2 aTexCoords; // 纹理坐标 out vec2 TexCoords; // 传递给片段着色器的纹理坐标 uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); TexCoords = aTexCoords; // 传递纹理坐标 }
-
片段着色器:使用纹理坐标进行纹理采样
在片段着色器中,我们根据传递的纹理坐标从纹理图像中采样对应的像素:
#version 330 core out vec4 FragColor; in vec2 TexCoords; // 从顶点着色器传递的纹理坐标 uniform sampler2D texture1; // 纹理采样器 void main() { FragColor = texture(texture1, TexCoords); // 使用纹理坐标从纹理中采样颜色 }
texture(texture1, TexCoords)
函数根据传入的纹理坐标从纹理texture1
中采样出颜色,并将其传递给片段着色器的输出变量FragColor
。 -
步骤 6:渲染
在渲染时,绑定纹理并绘制物体。通过
glActiveTexture
和glBindTexture
激活和绑定纹理,并使用glDrawArrays
或glDrawElements
绘制物体。
// 激活纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID); // 绑定加载的纹理
// 使用着色器程序
shader.use();
shader.setMat4("model", model);
shader.setMat4("view", view);
shader.setMat4("projection", projection);
// 绘制物体
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36); // 渲染立方体
glActiveTexture(GL_TEXTURE0)
激活纹理单元 0,以便接下来绑定的纹理会存储在单元 0 中。glBindTexture(GL_TEXTURE_2D, textureID)
将之前加载的纹理绑定到 OpenGL 上下文中的纹理单元GL_TEXTURE0
。shader.setInt("texture1", 0)
将着色器中的纹理采样器变量texture1
与纹理单元 0 关联,告诉片段着色器去纹理单元 0 中查找纹理数据。
在渲染过程中,OpenGL 会对三角形内部的所有像素执行插值,计算出每个片段的纹理坐标。片段着色器使用这些插值后的纹理坐标从纹理中取样,从而实现纹理映射。
总结
- UV坐标映射到3D顶点:每个顶点在3D空间中有一个位置,并且在纹理中有一个对应的UV坐标。纹理坐标是二维的,通常范围是
[0, 1]
,用来映射到纹理图像中的像素。 - 插值过程:在渲染过程中,OpenGL 会根据顶点的纹理坐标对三角形内的每个片段进行插值,最终生成正确的纹理映射。
- 纹理采样:在片段着色器中,根据插值后的纹理坐标,使用
texture()
函数从纹理中获取颜色值,将其应用到该片段上,形成最终的纹理效果。