深入浅出OpenGL纹理:让3D物体更真实
一、开篇:什么是纹理?为什么要用纹理?
写作要点:
-
用生活例子引入:就像给白墙贴壁纸、给手机贴膜
-
对比说明:没有纹理→只有单调颜色;有纹理→真实感暴增
-
技术定义:纹理就是贴在3D模型表面的2D图片
二、纹理基础概念(核心理论)
2.1 纹理坐标系统
// 纹理坐标范围:0.0 到 1.0
(0,0) 左下角 (1,0) 右下角
(0,1) 左上角 (1,1) 右上角
2.2 纹理映射过程
顶点坐标 → 纹理坐标 → 采样颜色 → 像素颜色
(x,y) → (u,v) → 纹理图片 → 最终显示
三、纹理加载与生成(代码实战)
3.1 使用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);
注意点:
-
OpenGL原点在左下角,图片原点在左上角,需要翻转
-
stbi_set_flip_vertically_on_load(true);
3.2 生成OpenGL纹理对象
unsigned int texture;
glGenTextures(1, &texture); // 生成纹理ID
glBindTexture(GL_TEXTURE_2D, texture); // 绑定纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D); // 生成Mipmap
四、纹理参数详解(重要!)
4.1 环绕方式(处理坐标超出[0,1]的情况)
| 环绕方式 | 效果 | 使用场景 |
|---|---|---|
| GL_REPEAT | 重复贴图 | 地板、墙面 |
| GL_MIRRORED_REPEAT | 镜像重复 | 对称图案 |
| GL_CLAMP_TO_EDGE | 边缘拉伸 | 避免接缝 |
| GL_CLAMP_TO_BORDER | 边界颜色 | 特殊效果 |
代码示例:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
配图: 每种环绕方式的效果示意图
4.2 过滤方式(缩放时的处理)
| 过滤方式 | 质量 | 性能 | 适用场景 |
|---|---|---|---|
| GL_NEAREST | 差(像素风) | 快 | 像素游戏 |
| GL_LINEAR | 好(平滑) | 中 | 一般情况 |
| GL_NEAREST_MIPMAP_* | 好 | 中 | 远处物体 |
| GL_LINEAR_MIPMAP_LINEAR | 最好 | 慢 | 高质量渲染 |
// 近处用线性,远处用Mipmap
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
五、完整实战:绘制带纹理的矩形
5.1 顶点数据(位置+纹理坐标)
float vertices[] = {
// 位置 // 纹理坐标
0.5f, 0.5f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f // 左上
};
5.2 着色器代码
顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
片段着色器:
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main() {
FragColor = texture(ourTexture, TexCoord);
}
5.3 完整C++代码
#include <glad.h> // GLAD: 管理OpenGL函数指针,必须包含在glfw之前
#include <glfw3.h> // GLFW: 创建窗口、处理输入和OpenGL上下文
#include <stb_image.h> // stb_image: 加载图片纹理的库
#include <shader.h> // 自定义着色器类,编译链接顶点和片段着色器
#include <iostream> // 标准输入输出流,用于打印错误信息
// 函数声明:当窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
// 函数声明:处理键盘输入
void processInput(GLFWwindow* window);
// settings - 窗口设置常量
const unsigned int SCR_WIDTH = 800; // 窗口宽度:800像素
const unsigned int SCR_HEIGHT = 600; // 窗口高度:600像素
int main()
{
// ==================== GLFW初始化配置 ====================
// glfw: initialize and configure
// ------------------------------
glfwInit(); // 初始化GLFW库,必须在调用任何GLFW函数前执行
// 配置GLFW:设置OpenGL主版本号为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
// 配置GLFW:设置OpenGL次版本号为3(组合成OpenGL 3.3)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 配置GLFW:使用核心模式(Core Profile),不使用废弃功能
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
// macOS系统需要这个额外的配置才能向前兼容
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// ==================== 创建GLFW窗口 ====================
// glfw window creation
// --------------------
// 参数:宽度, 高度, 窗口标题, 监视器(全屏用), 共享资源
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL) // 检查窗口是否创建成功
{
std::cout << "Failed to create GLFW window" << std::endl; // 打印错误信息
glfwTerminate(); // 终止GLFW,释放资源
return -1; // 返回错误代码
}
glfwMakeContextCurrent(window); // 将创建的窗口的OpenGL上下文设为当前线程的上下文
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // 注册窗口大小变化时的回调函数
// ==================== 加载GLAD ====================
// glad: load all OpenGL function pointers
// ---------------------------------------
// gladLoadGLLoader: 加载OpenGL函数指针,参数是获取函数地址的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl; // GLAD初始化失败
return -1;
}
// ==================== 编译着色器 ====================
// build and compile our shader program
// ------------------------------------
// 创建Shader对象,从文件加载并编译顶点着色器和片段着色器,然后链接成程序
Shader ourShader("4.1.texture.vs", "4.1.texture.fs");
// ==================== 设置顶点数据 ====================
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
// 顶点数组:每个顶点包含位置(x,y,z)、颜色(r,g,b)、纹理坐标(u,v)
// 共4个顶点,每个顶点8个float
float vertices[] = {
// positions(位置) // colors(颜色) // texture coords(纹理坐标)
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 // 第二个三角形:右下->左下->左上
};
// 声明缓冲区对象ID
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO); // 生成1个顶点数组对象(VAO)
glGenBuffers(1, &VBO); // 生成1个顶点缓冲区对象(VBO)
glGenBuffers(1, &EBO); // 生成1个索引缓冲区对象(EBO)
// 绑定VAO,后续的顶点属性配置都会记录到这个VAO中
glBindVertexArray(VAO);
// 绑定VBO为GL_ARRAY_BUFFER目标
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据复制到VBO中
// 参数:目标, 数据大小, 数据指针, 数据使用方式(静态绘制)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 绑定EBO为GL_ELEMENT_ARRAY_BUFFER目标
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 将索引数据复制到EBO中
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// ==================== 设置顶点属性指针 ====================
// position attribute - 位置属性
// 参数:索引, 分量数, 数据类型, 是否归一化, 步长, 偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 启用位置属性(索引0)
// color attribute - 颜色属性
// 偏移量:前3个float是位置,所以从第3个float开始(3*sizeof(float))
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); // 启用颜色属性(索引1)
// texture coord attribute - 纹理坐标属性
// 偏移量:前6个float是位置+颜色,所以从第6个float开始(6*sizeof(float))
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2); // 启用纹理坐标属性(索引2)
// ==================== 加载和创建纹理 ====================
// load and create a texture
// -------------------------
unsigned int texture; // 纹理对象ID
glGenTextures(1, &texture); // 生成1个纹理对象
glBindTexture(GL_TEXTURE_2D, texture); // 绑定纹理,后续纹理操作都作用于这个纹理对象
// set the texture wrapping parameters - 设置纹理包裹方式
// GL_REPEAT: 超出[0,1]范围时重复纹理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // S轴(x方向)包裹方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // T轴(y方向)包裹方式
// set texture filtering parameters - 设置纹理过滤方式
// GL_LINEAR_MIPMAP_LINEAR: 使用三线性过滤,平滑过渡mipmap级别
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // 缩小过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大过滤(线性插值)
// load image, create texture and generate mipmaps
int width, height, nrChannels; // 存储图片的宽度、高度、颜色通道数
// stbi_load: 加载图片,参数(文件路径, 宽, 高, 通道数, 期望通道数)
// 返回图片数据的指针
unsigned char* data = stbi_load("container2.png", &width, &height, &nrChannels, 0);
if (data) // 如果图片加载成功(data不为空)
{
// glTexImage2D: 将图片数据上传到GPU显存
// 参数:纹理类型, mipmap级别, 内部格式, 宽度, 高度, 边框, 数据格式, 数据类型, 像素数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap链(不同分辨率的纹理副本)
}
else // 图片加载失败
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data); // 释放CPU端图片数据内存
// ==================== 渲染循环 ====================
// render loop
// -----------
// 循环直到窗口被要求关闭
while (!glfwWindowShouldClose(window))
{
// input - 处理输入
// -----
processInput(window); // 检查按键输入(如ESC键)
// render - 渲染
// ------
// 设置清除颜色(深青绿色),并清除颜色缓冲区
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// bind Texture - 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture); // 激活当前纹理对象
// render container - 绘制矩形
ourShader.use(); // 使用着色器程序
glBindVertexArray(VAO); // 绑定VAO(包含顶点数据和属性配置)
// glDrawElements: 使用索引绘制图元
// 参数:图元类型(三角形), 索引数量, 索引数据类型, 偏移量
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glfw: swap buffers and poll IO events
// -------------------------------------------------------------------------------
glfwSwapBuffers(window); // 交换前后缓冲区(双缓冲,显示当前帧)
glfwPollEvents(); // 轮询等待事件(键盘、鼠标等输入)
}
// ==================== 清理资源 ====================
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO); // 删除VAO
glDeleteBuffers(1, &VBO); // 删除VBO
glDeleteBuffers(1, &EBO); // 删除EBO
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate(); // 终止GLFW,释放所有资源
return 0; // 程序正常退出
}
// ==================== 输入处理函数 ====================
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window)
{
// glfwGetKey: 检查指定按键的状态,GLFW_PRESS表示被按下
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true); // 设置窗口关闭标志为true
}
// ==================== 窗口大小回调函数 ====================
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height); // 设置OpenGL渲染视口为整个窗口大小
}
六、进阶技巧
6.1 多纹理混合
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform float mixValue;
void main() {
vec4 color1 = texture(texture1, TexCoord);
vec4 color2 = texture(texture2, TexCoord);
FragColor = mix(color1, color2, mixValue);
}
6.2 纹理单元(Texture Unit)
// 激活纹理单元并绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
// 设置uniform
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
解释: 纹理单元就像GPU的插槽,最多支持至少16个(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)
6.3 动态纹理效果
uniform float time;
void main() {
// 纹理滚动效果
vec2 scrollingTexCoord = TexCoord + vec2(time * 0.1, 0.0);
FragColor = texture(ourTexture, scrollingTexCoord);
}
七、常见问题和解决方案
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 黑屏/不显示纹理 | 纹理未绑定或uniform未设置 | 检查glBindTexture和glUniform1i |
| 图片倒置 | OpenGL和图片坐标系不同 | 使用stbi_set_flip_vertically_on_load |
| 纹理模糊/锯齿 | 过滤方式不对 | 使用GL_LINEAR或Mipmap |
| 纹理接缝 | 环绕方式问题 | 使用GL_CLAMP_TO_EDGE |
| 性能差 | 纹理过大 | 使用Mipmap或压缩纹理 |
八、从1个纹理到2个纹理,质的飞跃
8.1 为什么要用多纹理?
| 单纹理 | 多纹理 | |-------|--------| | 一层贴图 | 多层叠加 | | 效果单一 | 无限组合 | | 适合简单物体 | 实现光照、阴影、法线贴图 | **实际应用:** - 游戏角色 = 漫反射贴图 + 法线贴图 + 高光贴图 - 地形 = 草地纹理 + 石头纹理 + 雪地纹理(根据高度混合) - UI界面 = 背景图 + 按钮图 + 特效图
8.2 双纹理的额外代码
// 单纹理版本(你已经会了)
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// ... 设置参数和加载图片
// 双纹理版本(多做的几步)
unsigned int texture1, texture2; // 1. 声明两个变量
// 2. 重复纹理生成过程(但参数可不同)
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// ... 设置参数和加载第一张图
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
// ... 设置参数和加载第二张图
// 3. 渲染时激活两个纹理单元
glActiveTexture(GL_TEXTURE0); // 关键:必须先激活
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
// 4. 告诉着色器使用哪个纹理单元
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
发现了吗?多一个纹理只是多了"复制粘贴+修改参数"的工作量!
8.3 让两个纹理动起来
通过简单的数学运算,让纹理产生动态效果
// 片段着色器
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform float time; // 传入时间
void main() {
// 让第二个纹理滚动
vec2 scrollingUV = TexCoord + vec2(time * 0.1, 0.0);
vec4 color1 = texture(texture1, TexCoord);
vec4 color2 = texture(texture2, scrollingUV);
// 随时间改变混合比例(呼吸效果)
float mixValue = (sin(time) + 1.0) / 2.0;
FragColor = mix(color1, color2, mixValue);
}
效果: 第二个纹理从左向右滚动,同时淡入淡出!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)