一、开篇什么是纹理为什么要用纹理写作要点用生活例子引入就像给白墙贴壁纸、给手机贴膜对比说明没有纹理→只有单调颜色有纹理→真实感暴增技术定义纹理就是贴在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.hint 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); // 生成纹理IDglBindTexture(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最好慢高质量渲染// 近处用线性远处用MipmapglTexParameteri(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 corelayout (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 coreout 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主版本号为3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);// 配置GLFW设置OpenGL次版本号为3组合成OpenGL 3.3glfwWindowHint(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个floatfloat 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 // 第二个三角形右下-左下-左上};// 声明缓冲区对象IDunsigned 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; // 纹理对象IDglGenTextures(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 mipmapsint 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 theyve outlived their purpose:// ------------------------------------------------------------------------glDeleteVertexArrays(1, VAO); // 删除VAOglDeleteBuffers(1, VBO); // 删除VBOglDeleteBuffers(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);// 设置uniformourShader.setInt(texture1, 0);ourShader.setInt(texture2, 1);解释纹理单元就像GPU的插槽最多支持至少16个GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS6.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);}效果第二个纹理从左向右滚动同时淡入淡出