OpenGL坐标变换入门:让2D图片在3D世界中动起来

张开发
2026/5/17 13:17:48 15 分钟阅读
OpenGL坐标变换入门:让2D图片在3D世界中动起来
从静态贴图到动态动画理解矩阵变换的核心思想前言在学习OpenGL的过程中我们通常会经历这样一个过程先学会画一个三角形然后学会给三角形贴上纹理再然后... 发现这个三角形只能呆在那里一动不动。如何让我们的图形动起来答案就是——坐标变换。本文将从一个初学者的视角解析如何通过GLM数学库和变换矩阵让一张简单的纹理图片在屏幕上实现平滑的左右移动。一、从静态到动态我们添加了什么对比之前的纹理贴图代码坐标变换主要增加了以下几个核心部分1.1 引入GLM数学库#include glm.hpp#include gtc/matrix_transform.hpp#include gtc/type_ptr.hppGLMOpenGL Mathematics是一个专为OpenGL设计的数学库它模仿GLSL的语法风格让我们可以在C代码中方便地操作矩阵和向量。1.2 创建变换矩阵在渲染循环中我们添加了这样一段代码// 创建变换矩阵glm::mat4 transform glm::mat4(1.0f); // 初始化为单位矩阵// 获取时间用于动态变化float time glfwGetTime();// 动态平移在X轴上 -1 到 1 之间来回移动float xOffset sin(time); // sin函数让值在 -1~1 间平滑变化transform glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));// 传递给着色器ourShader.use();unsigned int transformLoc glGetUniformLocation(ourShader.ID, transform);glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));1.3 着色器中的uniform变量在顶点着色器文件中我们需要声明一个uniform变量来接收这个变换矩阵// 5.1.transform.vs#version 330 corelayout (location 0) in vec3 aPos;layout (location 1) in vec2 aTexCoord;out vec2 TexCoord;uniform mat4 transform; // 接收C传来的变换矩阵void main(){gl_Position transform * vec4(aPos, 1.0); // 应用变换TexCoord aTexCoord;}二、理解变换矩阵2.1 什么是矩阵简单来说矩阵就是一个数字表格。在图形学中我们通常使用4×4矩阵来表示各种变换平移、旋转、缩放。glm::mat4 transform glm::mat4(1.0f); // 单位矩阵就像数字1单位矩阵长这样[1, 0, 0, 0][0, 1, 0, 0][0, 0, 1, 0][0, 0, 0, 1]任何向量乘以单位矩阵结果不变。2.2 平移变换transform glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));这行代码的意思是在当前变换的基础上沿着X轴移动xOffset个单位。xOffset为正数 → 向右移动xOffset为负数 → 向左移动2.3 动态效果的关键sin函数float time glfwGetTime(); // 获取程序运行时间秒float xOffset sin(time); // 让偏移量在 -1 到 1 间来回振荡glfwGetTime()返回从程序启动到当前的时间秒。sin()函数的值会随着时间在 -1 和 1 之间平滑地来回变化形成自然的往复运动。效果图片会在X轴上从 -1 到 1 的位置来回滑动周期约为 2π ≈ 6.28 秒。三、完整代码结构解析3.1 顶点数据的变化之前的纹理代码中每个顶点包含位置(3) 颜色(3) 纹理坐标(2) 8个float。而在变换示例中我们移除了颜色属性只保留位置(3) 纹理坐标(2) 5个floatfloat vertices[] {// positions // texture coords0.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 // 左上};相应的顶点属性指针也需要调整// 位置属性偏移0步长5*sizeof(float)glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);// 纹理坐标属性偏移3*sizeof(float)步长5*sizeof(float)glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));3.2 渲染循环的核心逻辑while (!glfwWindowShouldClose(window)){// 1. 处理输入processInput(window);// 2. 清屏glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// 3. 绑定纹理glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, texture1);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, texture2);// 4. 创建变换矩阵每帧都重新计算glm::mat4 transform glm::mat4(1.0f);float time glfwGetTime();float xOffset sin(time);transform glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));// 5. 传递给着色器ourShader.use();unsigned int transformLoc glGetUniformLocation(ourShader.ID, transform);glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));// 6. 绘制glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 7. 交换缓冲glfwSwapBuffers(window);glfwPollEvents();}四、尝试更多变换效果4.1 让图片自己旋转把平移换成旋转transform glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));参数说明第1个参数要变换的矩阵第2个参数旋转角度弧度第3个参数绕哪个轴旋转这里是Z轴即在屏幕平面内旋转4.2 同时平移和旋转transform glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));transform glm::rotate(transform, time, glm::vec3(0.0f, 0.0f, 1.0f));注意顺序矩阵乘法不满足交换律先平移再旋转 vs 先旋转再平移结果完全不同。4.3 缩放效果float scale 0.5f sin(time) * 0.3f; // 在 0.2 到 0.8 之间变化transform glm::scale(transform, glm::vec3(scale, scale, 1.0f));五、常见问题与解决5.1 图片显示不出来检查以下几点纹理图片路径是否正确顶点坐标是否在 [-1, 1] 范围内索引数组是否正确0,1,3 和 1,2,35.2 变换没有效果确认着色器中声明了uniform mat4 transform确认在glDrawElements之前调用了glUniformMatrix4fv确认顶点着色器中使用了这个矩阵gl_Position transform * vec4(aPos, 1.0)5.3 图片移动太快或太慢调整sin函数的系数cppfloat xOffset sin(time * 0.5f); // 速度减半 float xOffset sin(time * 2.0f); // 速度加倍六、进阶思考当你掌握了基本的平移、旋转、缩放后可以尝试组合变换让图片一边旋转一边左右移动多物体变换创建多个VAO分别应用不同的变换矩阵3D变换尝试绕X轴或Y轴旋转观察透视效果总结坐标变换的核心思想其实很简单在每一帧重新计算一个变换矩阵将其传递给GPU让顶点着色器用这个矩阵去改变顶点的位置。通过glfwGetTime()获取时间结合sin()函数就能创造出平滑的动态效果。而GLM库则为我们提供了简洁的矩阵操作接口无需深入理解复杂的矩阵数学就能上手。记住这个公式text最终位置 变换矩阵 × 原始顶点位置掌握了这个你就迈出了从画静态图形到做动态动画的关键一步七、完整代码// 包含头文件#include glad.h // OpenGL 函数指针管理必须在 glfw 之前#include glfw3.h // 窗口管理和输入处理#include stb_image.h // 图片加载库用于纹理#include glm.hpp // GLM 数学库核心#include gtc/matrix_transform.hpp // 矩阵变换函数平移、旋转、缩放#include gtc/type_ptr.hpp // 将 glm 矩阵转换为 OpenGL 数组#include shader.h // 自定义着色器类#include iostream // 标准输入输出// 函数声明void framebuffer_size_callback(GLFWwindow* window, int width, int height);void processInput(GLFWwindow* window);// 窗口大小设置const unsigned int SCR_WIDTH 800; // 窗口宽度const unsigned int SCR_HEIGHT 600; // 窗口高度int main(){// 1. GLFW 初始化 glfwInit(); // 初始化 GLFW 库// 配置 OpenGL 版本使用 3.3 核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 主版本号 3glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 次版本号 3glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 核心模式不兼容旧版#ifdef __APPLE__// MacOS 需要这个标志才能向前兼容glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);#endif// 2. 创建窗口 GLFWwindow* window glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, LearnOpenGL, NULL, NULL);if (window NULL){std::cout Failed to create GLFW window std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window); // 将窗口的上下文设置为当前线程的上下文glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // 注册窗口大小改变的回调函数// 3. GLAD 初始化 // GLAD 用于加载 OpenGL 函数指针必须在使用任何 OpenGL 函数前调用if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout Failed to initialize GLAD std::endl;return -1;}// 4. 构建着色器程序 // 从文件加载并编译顶点/片段着色器链接成程序Shader ourShader(5.1.transform.vs, 5.1.transform.fs);// 5. 设置顶点数据 // 顶点数据位置(x,y,z) 纹理坐标(u,v)// 注意这里只有位置和纹理坐标没有颜色数据float vertices[] {// 位置(x,y,z) 纹理坐标(u,v)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 // 左上角};// 索引数据定义两个三角形组成一个矩形unsigned int indices[] {0, 1, 3, // 第一个三角形右上、右下、左上1, 2, 3 // 第二个三角形右下、左下、左上};// 创建缓冲对象unsigned int VBO, VAO, EBO;glGenVertexArrays(1, VAO); // 生成顶点数组对象glGenBuffers(1, VBO); // 生成顶点缓冲对象glGenBuffers(1, EBO); // 生成索引缓冲对象// 绑定 VAO所有后续顶点属性配置都会被记录在这个 VAO 中glBindVertexArray(VAO);// 绑定并填充 VBOglBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 绑定并填充 EBOglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);// 6. 配置顶点属性 // 位置属性location 0// 参数属性位置、每个分量大小、数据类型、是否归一化、步长、偏移量glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);glEnableVertexAttribArray(0); // 启用位置属性// 纹理坐标属性location 1// 偏移量 3个float位置占用的字节数glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1); // 启用纹理坐标属性// 7. 加载纹理 unsigned int texture1, texture2;// ----- 纹理1雷欧.png -----glGenTextures(1, texture1); // 生成纹理对象glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理后续操作都针对这个纹理// 设置纹理环绕方式超出[0,1]范围时的处理glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // S轴U重复glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // T轴V重复// 设置纹理过滤方式放大/缩小时的处理glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 缩小时线性插值glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大时线性插值int width, height, nrChannels;stbi_set_flip_vertically_on_load(true); // 翻转Y轴OpenGL的纹理原点在左下角图片通常在左上角// 加载图片数据unsigned char* data stbi_load((雷欧.png), width, height, nrChannels, 0);if (data){// 生成纹理参数目标、mipmap级别、内部格式、宽、高、边框、源格式、数据类型、数据glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);glGenerateMipmap(GL_TEXTURE_2D); // 生成多级渐远纹理}else{std::cout Failed to load texture std::endl;}stbi_image_free(data); // 释放图片内存// ----- 纹理2awesomeface.png带透明度-----glGenTextures(1, texture2);glBindTexture(GL_TEXTURE_2D, texture2);// 同样设置纹理参数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);data stbi_load((awesomeface.png), width, height, nrChannels, 0);if (data){// 注意这个图片有透明度所以使用 GL_RGBA 格式glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);glGenerateMipmap(GL_TEXTURE_2D);}else{std::cout Failed to load texture std::endl;}stbi_image_free(data);// 8. 设置纹理单元 ourShader.use(); // 先激活着色器程序// 告诉着色器texture1 使用纹理单元0texture2 使用纹理单元1ourShader.setInt(texture1, 0);ourShader.setInt(texture2, 1);// 9. 渲染循环 while (!glfwWindowShouldClose(window)) // 循环直到窗口关闭{// ----- 9.1 处理输入 -----processInput(window); // 检测按键ESC退出// ----- 9.2 清屏 -----glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清屏颜色深青色glClear(GL_COLOR_BUFFER_BIT); // 清除颜色缓冲// ----- 9.3 绑定纹理 -----glActiveTexture(GL_TEXTURE0); // 激活纹理单元0glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理1glActiveTexture(GL_TEXTURE1); // 激活纹理单元1glBindTexture(GL_TEXTURE_2D, texture2); // 绑定纹理2// ----- 9.4 创建变换矩阵核心实现动态移动-----glm::mat4 transform glm::mat4(1.0f); // 初始化为单位矩阵// 获取程序运行时间秒float time glfwGetTime();// 计算 X 轴偏移量sin(time) 范围 [-1, 1]// 物体会在 X 轴上 -1 到 1 之间来回移动float xOffset sin(time);// 平移变换将物体沿 X 轴移动 xOffsettransform glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));// 可选添加旋转效果绕 Z 轴旋转旋转角度随时间增加// transform glm::rotate(transform, time, glm::vec3(0.0f, 0.0f, 1.0f));// ----- 9.5 传递矩阵到着色器 -----ourShader.use(); // 激活着色器// 获取着色器中 uniform 变量 transform 的位置unsigned int transformLoc glGetUniformLocation(ourShader.ID, transform);// 将矩阵数据传递给着色器// 参数位置、矩阵数量、是否转置、矩阵数据指针glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));// ----- 9.6 绘制物体 -----glBindVertexArray(VAO); // 绑定 VAO// 使用索引绘制模式、索引数量、索引类型、偏移量glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// ----- 9.7 交换缓冲并处理事件 -----glfwSwapBuffers(window); // 交换前后缓冲双缓冲技术glfwPollEvents(); // 处理所有待处理的事件键盘、鼠标等}// 10. 清理资源 glDeleteVertexArrays(1, VAO);glDeleteBuffers(1, VBO);glDeleteBuffers(1, EBO);glfwTerminate(); // 终止 GLFWreturn 0;}// 辅助函数 // 处理键盘输入void processInput(GLFWwindow* window){// 如果按下 ESC 键设置窗口关闭标志if (glfwGetKey(window, GLFW_KEY_ESCAPE) GLFW_PRESS)glfwSetWindowShouldClose(window, true);}// 窗口大小改变时的回调函数void framebuffer_size_callback(GLFWwindow* window, int width, int height){// 重置视口大小OpenGL 绘制的区域glViewport(0, 0, width, height);} 关键概念图解text顶点数据流向 原始顶点 → VBO → VAO → 顶点着色器 → 变换矩阵 → 片段着色器 → 纹理采样 → 最终像素 变换矩阵的作用 单位矩阵 → 平移矩阵(sin(time)) → 传递给着色器 → 顶点位置被改变 → 物体移动❓ 新手常见问题问题解答为什么要用 VAOVAO 记录所有顶点属性配置绘制时只需绑定一次避免重复设置glm::mat4(1.0f)为什么是单位矩阵1.0f 在对角线上表示不进行任何变换sin(time)为什么能实现来回移动sin 函数值在 -1 和 1 之间周期性变化为什么需要glActiveTextureOpenGL 支持多个纹理需要先激活再绑定

更多文章