跳到主要内容

变换

在3D图形编程中,变换是将物体从一个位置移动到另一个位置、改变其大小或方向的核心技术。本章将介绍向量和矩阵运算,以及如何在OpenGL中实现平移、旋转和缩放变换。

向量基础

向量是具有方向和大小的量,在图形编程中表示位置、方向、速度等。

向量表示

// GLM中的向量
glm::vec2 v2(1.0f, 2.0f); // 2D向量
glm::vec3 v3(1.0f, 2.0f, 3.0f); // 3D向量
glm::vec4 v4(1.0f, 2.0f, 3.0f, 1.0f); // 4D向量(齐次坐标)

向量运算

glm::vec3 a(1.0f, 2.0f, 3.0f);
glm::vec3 b(4.0f, 5.0f, 6.0f);

// 加减
glm::vec3 sum = a + b; // (5, 7, 9)
glm::vec3 diff = a - b; // (-3, -3, -3)

// 标量乘法
glm::vec3 scaled = a * 2.0f; // (2, 4, 6)

// 点积
float dot = glm::dot(a, b); // 1*4 + 2*5 + 3*6 = 32

// 叉积(只适用于3D向量)
glm::vec3 cross = glm::cross(a, b); // 垂直于a和b的向量

// 长度
float len = glm::length(a); // sqrt(1 + 4 + 9) = 3.74...

// 归一化
glm::vec3 normalized = glm::normalize(a); // 单位向量

点积的几何意义

点积可以用来计算两个向量之间的夹角:

ab=abcosθ\vec{a} \cdot \vec{b} = |a| \cdot |b| \cdot \cos\theta

float angle = glm::acos(glm::dot(glm::normalize(a), glm::normalize(b)));

叉积的几何意义

叉积产生一个垂直于两个输入向量的新向量,常用于:

  • 计算法线
  • 判断方向
  • 计算旋转轴

矩阵基础

矩阵是变换的数学表示。在3D图形中,我们主要使用4x4矩阵。

矩阵表示

// 单位矩阵
glm::mat4 identity = glm::mat4(1.0f);

// 自定义矩阵
glm::mat4 m(
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);

矩阵运算

glm::mat4 a = glm::mat4(1.0f);
glm::mat4 b = glm::mat4(1.0f);

// 矩阵加法
glm::mat4 sum = a + b;

// 矩阵乘法(注意顺序)
glm::mat4 product = a * b;

// 矩阵与向量相乘
glm::vec4 v(1.0f, 0.0f, 0.0f, 1.0f);
glm::vec4 result = a * v;

// 矩阵转置
glm::mat4 transposed = glm::transpose(a);

// 矩阵求逆
glm::mat4 inversed = glm::inverse(a);

基本变换

平移

平移将物体从一个位置移动到另一个位置:

// 创建平移矩阵
glm::mat4 translation = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 2.0f, 3.0f));

// 应用平移
glm::vec4 position(0.0f, 0.0f, 0.0f, 1.0f);
glm::vec4 newPosition = translation * position; // (1, 2, 3, 1)

平移矩阵的结构:

[100tx010ty001tz0001]\begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}

缩放

缩放改变物体的大小:

// 创建缩放矩阵
glm::mat4 scaling = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f, 2.0f, 2.0f));

// 均匀缩放(各方向相同)
glm::mat4 uniformScale = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f));

// 非均匀缩放(各方向不同)
glm::mat4 nonUniformScale = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f, 1.0f, 0.5f));

缩放矩阵的结构:

[sx0000sy0000sz00001]\begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

旋转

旋转改变物体的方向。GLM使用角度(需要转换为弧度):

// 绕X轴旋转
glm::mat4 rotX = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(1.0f, 0.0f, 0.0f));

// 绕Y轴旋转
glm::mat4 rotY = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));

// 绕Z轴旋转
glm::mat4 rotZ = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 0.0f, 1.0f));

// 绕任意轴旋转
glm::mat4 rotAxis = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(1.0f, 1.0f, 0.0f));

绕Z轴旋转矩阵的结构(旋转角度θ):

[cosθsinθ00sinθcosθ0000100001]\begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

组合变换

多个变换可以通过矩阵乘法组合。注意矩阵乘法的顺序很重要,应该从右向左读:

glm::mat4 model = glm::mat4(1.0f);

// 先缩放,再旋转,最后平移
model = glm::translate(model, glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));

// 等价于:model = translate * rotate * scale

变换顺序的影响:

原始位置 → 缩放 → 旋转 → 平移
(变小) (转动) (移动)

在OpenGL中使用变换

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}

C++代码

// 创建变换矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.5f, 0.0f, 0.0f));
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

// 传递给着色器
glUseProgram(shaderProgram);
int modelLoc = glGetUniformLocation(shaderProgram, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

动画示例

通过在渲染循环中更新变换矩阵,可以实现动画效果:

while (!glfwWindowShouldClose(window)) {
// 计算时间
float time = (float)glfwGetTime();

// 创建旋转矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, time, glm::vec3(0.0f, 0.0f, 1.0f));

// 更新着色器uniform
glUseProgram(shaderProgram);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

// 绘制
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glfwSwapBuffers(window);
glfwPollEvents();
}

齐次坐标

为什么使用4x4矩阵和4D向量?这涉及到齐次坐标的概念。

3D坐标的问题

3x3矩阵无法表示平移变换:

[m00m01m02m10m11m12m20m21m22][xyz]=[xyz]\begin{bmatrix} m_{00} & m_{01} & m_{02} \\ m_{10} & m_{11} & m_{12} \\ m_{20} & m_{21} & m_{22} \end{bmatrix} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} x' \\ y' \\ z' \end{bmatrix}

无论矩阵值如何,原点(0,0,0)变换后仍然是(0,0,0),无法实现平移。

齐次坐标的解决方案

通过添加第4个分量w,可以统一表示所有变换:

  • w = 1:表示点(位置)
  • w = 0:表示向量(方向)
glm::vec4 point(1.0f, 2.0f, 3.0f, 1.0f);   // 点
glm::vec4 direction(1.0f, 0.0f, 0.0f, 0.0f); // 向量

向量不受平移影响,因为w=0:

[100tx010ty001tz0001][xyz0]=[xyz0]\begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 0 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z \\ 0 \end{bmatrix}

GLM库常用函数

// 初始化
glm::mat4 mat = glm::mat4(1.0f); // 单位矩阵

// 平移
glm::mat4 translate = glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z));

// 旋转
glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(axis));

// 缩放
glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(x, y, z));

// 组合
glm::mat4 combined = translate * rotate * scale;

// 逆矩阵
glm::mat4 inv = glm::inverse(mat);

// 转置
glm::mat4 trans = glm::transpose(mat);

// 获取数据指针
float* ptr = glm::value_ptr(mat);

完整示例:旋转的立方体

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>

int main() {
// 初始化
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

GLFWwindow* window = glfwCreateWindow(800, 600, "Transformations", NULL, NULL);
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

glEnable(GL_DEPTH_TEST);

// 立方体顶点数据
float vertices[] = {
// 前面
-0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f,
// 后面
-0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f,
// 其他面...
};

// 创建VAO、VBO
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 着色器
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
)";

const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.5f, 0.2f, 1.0f);
}
)";

// 编译着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

// 获取uniform位置
glUseProgram(shaderProgram);
int modelLoc = glGetUniformLocation(shaderProgram, "model");
int viewLoc = glGetUniformLocation(shaderProgram, "view");
int projLoc = glGetUniformLocation(shaderProgram, "projection");

// 投影矩阵(只设置一次)
glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

// 渲染循环
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 视图矩阵
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));

// 模型矩阵(旋转)
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.5f, 1.0f, 0.0f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

// 绘制
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);

glfwSwapBuffers(window);
glfwPollEvents();
}

glfwTerminate();
return 0;
}

小结

变换是3D图形编程的基础:

  • 向量表示位置和方向
  • 矩阵表示变换操作
  • 平移、旋转、缩放是最基本的变换
  • 变换顺序很重要(从右向左应用)
  • 齐次坐标统一了所有变换

下一章我们将学习 坐标系统,理解物体如何从3D空间投影到2D屏幕。

数学提示

变换矩阵的顺序非常重要。记住:矩阵乘法不满足交换律,A×B ≠ B×A。在组合变换时,按照"缩放 → 旋转 → 平移"的顺序应用通常是正确的。