一、开篇:什么是纹理?为什么要用纹理?

写作要点:

  • 用生活例子引入:就像给白墙贴壁纸、给手机贴膜

  • 对比说明:没有纹理→只有单调颜色;有纹理→真实感暴增

  • 技术定义:纹理就是贴在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);
}

效果: 第二个纹理从左向右滚动,同时淡入淡出!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐