变换
在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); // 单位向量
点积的几何意义
点积可以用来计算两个向量之间的夹角:
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)
平移矩阵的结构:
缩放
缩放改变物体的大小:
// 创建缩放矩阵
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));
缩放矩阵的结构:
旋转
旋转改变物体的方向。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轴旋转矩阵的结构(旋转角度θ):
组合变换
多个变换可以通过矩阵乘法组合。注意矩阵乘法的顺序很重要,应该从右向左读:
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矩阵无法表示平移变换:
无论矩阵值如何,原点(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:
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。在组合变换时,按照"缩放 → 旋转 → 平移"的顺序应用通常是正确的。