从静态贴图到动态动画,理解矩阵变换是OpenGL学习的关键一步。本文将用C++和GLM库,带你一步步实现纹理图片的平滑移动,并掌握平移、旋转、缩放等核心变换。

从静态到动态:我们添加了什么?

相比纯纹理贴图代码,坐标变换主要增加了三个核心模块:GLM数学库变换矩阵创建、以及着色器中的uniform变量

  • GLM库:OpenGL Mathematics,专为OpenGL设计,模仿GLSL语法,让你在C++中轻松操作矩阵和向量。
  • 变换矩阵:在渲染循环中每帧重新计算,实现动态效果。
  • uniform变量:在顶点着色器中声明,接收C++传来的变换矩阵。

以下代码展示了如何创建变换矩阵并传递给着色器:

// 创建变换矩阵
glm::mat4 transform = glm::mat4(1.0f); // 单位矩阵
float time = glfwGetTime();
float xOffset = sin(time); // 在 -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));

顶点着色器中需要声明uniform mat4 transform:

// 5.1.transform.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main() {
    gl_Position = transform * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

理解变换矩阵

矩阵在图形学中就是一个“数字表格”,我们常用4×4矩阵表示平移、旋转、缩放。单位矩阵就像数字“1”,任何向量乘以它结果不变:

[1, 0, 0, 0]
[0, 1, 0, 0]
[0, 0, 1, 0]
[0, 0, 0, 1]

平移变换通过 glm::translate 实现:

transform = glm::translate(transform, glm::vec3(xOffset, 0.0f, 0.0f));

这行代码在当前变换基础上,沿X轴移动xOffset个单位。xOffset为正数向右移动,xOffset为负数向左移动。

动态效果的关键是sin函数

float time = glfwGetTime();
float xOffset = sin(time);

glfwGetTime() 返回程序运行时间(秒),sin() 让偏移量在 -1 到 1 间来回振荡,形成自然的往复运动。图片会在X轴上从 -1 到 1 来回滑动,周期约6.28秒。

完整代码结构解析

顶点数据从之前的“位置+颜色+纹理坐标”简化为“位置+纹理坐标”,每个顶点只包含5个float:

float vertices[] = {
    // positions    // texture coords
     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  // 左上
};

顶点属性指针也需要调整:

  • 位置属性:偏移0,步长5×sizeof(float)
  • 纹理坐标属性:偏移3×sizeof(float),步长5×sizeof(float)

渲染循环的核心逻辑如下:

while (!glfwWindowShouldClose(window)) {
    processInput(window);
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture2);
    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));
    ourShader.use();
    unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glfwSwapBuffers(window);
    glfwPollEvents();
}

尝试更多变换效果

掌握了基础平移后,可以尝试旋转、缩放和组合变换:

  • 旋转transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f)); 绕Z轴旋转。
  • 组合变换:先平移再旋转,注意顺序!矩阵乘法不满足交换律。
  • 缩放float scale = 0.5f + sin(time) * 0.3f; transform = glm::scale(transform, glm::vec3(scale, scale, 1.0f)); 让图片在0.2到0.8之间缩放。

想调整移动速度?修改sin函数的系数即可:

float xOffset = sin(time * 0.5f);  // 速度减半
float xOffset = sin(time * 2.0f);  // 速度加倍

[AFFILIATE_SLOT_1]

⚠️ 常见问题与解决

  • 图片显示不出来:检查纹理路径、顶点坐标范围、索引数组是否正确。
  • 变换没有效果:确认着色器中声明了uniform mat4 transform,在glDrawElements之前调用了glUniformMatrix4fv,顶点着色器中使用gl_Position = transform * vec4(aPos, 1.0)
  • 图片移动太快或太慢:调整sin函数的系数,如 sin(time * 0.5f) 减速。

进阶思考

掌握基本变换后,可以尝试:

  • 组合变换:一边旋转一边左右移动。
  • 多物体变换:创建多个VAO,分别应用不同矩阵。
  • 3D变换:绕X轴或Y轴旋转,观察透视效果。

坐标变换的核心思想很简单:

在每一帧,重新计算一个变换矩阵,将其传递给GPU,让顶点着色器用这个矩阵去改变顶点的位置。

通过glfwGetTime()获取时间,结合sin()函数,就能创造平滑动态效果。GLM库提供了简洁的矩阵操作接口,无需深入复杂数学就能上手。

[AFFILIATE_SLOT_2]

记住这个公式:

最终位置 = 变换矩阵 × 原始顶点位置
掌握了这些,你就迈出了从“画静态图形”到“做动态动画”的关键一步!

完整代码包含头文件、GLFW初始化、窗口创建、GLAD初始化、着色器程序构建、顶点数据设置、纹理加载等步骤,详见原文。