为什么矩阵更方便?—— 直观对比与底层逻辑
💡 核心结论:矩阵不仅是数学工具,更是图形学的"万能压缩包"。它把旋转、缩放、平移等复杂操作打包成 16 个数字,让 GPU 一次性完成所有变换。
1. 直观对比:代码复杂度的天壤之别
假设我们需要对一个 3D 模型做一个常见的复合变换:
- 旋转 45 度
- 缩放 2 倍
- 平移 到 (5, 3, 0)
❌ 方式一:不用矩阵(手动计算)
如果不使用矩阵,我们需要在 Shader 中手动一步步算出顶点的新位置。
// 顶点着色器 (Vertex Shader)
attribute vec3 position;
void main() {
vec3 pos = position;
// -------------------------------------------------
// 1. 手动计算旋转 (容易写错公式)
// -------------------------------------------------
float angle = radians(45.0);
float c = cos(angle);
float s = sin(angle);
// 旋转公式:x' = x*cos - y*sin, y' = x*sin + y*cos
float newX = pos.x * c - pos.y * s;
float newY = pos.x * s + pos.y * c;
vec3 rotated = vec3(newX, newY, pos.z);
// -------------------------------------------------
// 2. 手动计算缩放 (这就还得再乘一遍)
// -------------------------------------------------
vec3 scaled = rotated * vec3(2.0, 2.0, 2.0);
// -------------------------------------------------
// 3. 手动计算平移 (这就还得再加一遍)
// -------------------------------------------------
vec3 finalPos = scaled + vec3(5.0, 3.0, 0.0);
// -------------------------------------------------
// 4. 最后还得手动转齐次坐标...
// -------------------------------------------------
gl_Position = vec4(finalPos, 1.0);
}
痛点总结:
- 代码冗长:简单的操作写了 20 多行。
- 容易出错:三角函数公式
x*c - y*s很容易记反。 - 顺序混乱:如果先平移再旋转,结果完全不同,逻辑很难维护。
- 性能浪费:每个顶点都要在 GPU 上重复做三角函数运算(
sin/cos),非常慢。
✅ 方式二:使用矩阵(一行搞定)
使用矩阵,所有的变换在 CPU 端就被"打包"好了一个矩阵 modelMatrix。
// 顶点着色器 (Vertex Shader)
attribute vec3 position;
uniform mat4 modelMatrix; // CPU 传来的"万能变换包"
void main() {
// -------------------------------------------------
// 💎 核心逻辑:一行代码完成所有变换
// -------------------------------------------------
gl_Position = modelMatrix * vec4(position, 1.0);
}
优势总结:
- 极其简洁:核心逻辑只有 1 行。
- 逻辑清晰:不需要关心具体是先加减还是先乘除,矩阵乘法自动处理。
- 性能极高:矩阵乘法是 GPU 的原生强项,硬件加速。
2. 深度解析:为什么矩阵这么神?
你可能会问:"矩阵不就是把算式换了个写法吗?" 不完全是。矩阵不仅是数学简写,它改变了计算的维度。
2.1 核心概念:矩阵 = "坐标系变换器"
不要把矩阵看作一堆数字,要把它看作三个新的坐标轴。
一个 4x4 矩阵的前三列,实际上就代表了变换后的 X、Y、Z 轴的方向和长度。
mat4 modelMatrix = mat4(
// 第一列:变换后的 X 轴 (New X Axis)
1.414, 1.414, 0.0, 0.0, // 这里的数字包含了"旋转45度"和"放大2倍"的信息
// 第二列:变换后的 Y 轴 (New Y Axis)
-0.707, 0.707, 0.0, 0.0, // 这里的数字包含了"旋转45度"的信息
// 第三列:变换后的 Z 轴 (New Z Axis)
0.0, 0.0, 1.0, 0.0, // Z 轴没变
// 第四列:原点位置 (Translation)
5.0, 3.0, 0.0, 1.0 // 物体被移动到了这里
);
直观理解:
当你做 matrix * position 时,GPU 实际上是在说:
"以前的 X 轴方向不作数了,现在沿着矩阵第一列的方向走
position.x那么多;沿着矩阵第二列的方向走position.y那么多..."
2.2 核心优势:组合性 (Composability)
这是矩阵最强大的地方。如果你需要做一个极其复杂的动作:
- 旋转 30 度
- 往右移 10 米
- 缩小 0.5 倍
- 再绕 X 轴转 90 度
- 再往上移 2 米
不用矩阵:你需要写 5 段复杂的代码,而且如果不小心搞乱了顺序(比如先平移再缩放),结果全错。
使用矩阵: 在 CPU (JavaScript) 端,你只需要做简单的矩阵乘法:
// 伪代码:变换组合
// 最终矩阵 = 变换5 * 变换4 * 变换3 * 变换2 * 变换1
const finalMatrix = T2.multiply(R2).multiply(S).multiply(T1).multiply(R1);
无论过程多复杂,最终传给 GPU 的永远只是一个 4x4 的矩阵(16 个数字)。Shader 代码永远不需要改动!
3. 实际工作流对比 (Three.js / WebGL)
让我们看看从 CPU 到 GPU 的数据流向,彻底理解为什么这种方式高效。
场景:渲染 10,000 个顶点
❌ 低效流程(手动计算)
- CPU:不干活,直接把顶点发给 GPU。
- GPU:
- 拿到第 1 个顶点:算
sin(45), 算cos(45), 乘法, 加法... - ...
- 拿到第 10,000 个顶点:重复算一遍
sin(45)... - 结果:做了 10,000 次重复的三角函数运算。
- 拿到第 1 个顶点:算
✅ 高效流程(矩阵流水线)
- CPU(JavaScript):
- 计算
modelMatrix(只算 1 次)。 - 包含:
Rotate(45) * Scale(2) * Translate(5,3)。 - 把这 16 个数字传给 GPU。
- 计算
- GPU:
- 拿到第 1 个顶点:
Matrix * Vertex(硬件指令,极快)。 - ...
- 拿到第 10,000 个顶点:
Matrix * Vertex。 - 结果:没有重复的三角函数计算,纯粹的线性代数运算。
- 拿到第 1 个顶点:
4. 总结:何时该用矩阵?
| 维度 | 不用矩阵 (Scalar Math) | 使用矩阵 (Matrix Math) |
|---|---|---|
| 代码量 | 几十行,逻辑分散 | 1 行核心代码 |
| 可读性 | 差,充满数学公式 | 好,语义清晰 |
| 灵活性 | 极差,改需求要重写 Shader | 极高,改 CPU 参数即可 |
| 性能 | 差,GPU 重复计算复杂函数 | 优,CPU 预计算,GPU 纯乘法 |
| 适用场景 | 几乎没有(除非做极为特殊的非线性扭曲) | 99.9% 的图形学变换 |
💡 记忆口诀
CPU 负责"配方"(构建矩阵),GPU 负责"炒菜"(矩阵乘顶点)。 无论配方多复杂(旋转缩放平移一百次),传给大厨的永远只是一张纸(一个矩阵)。
这也是为什么 MVP 矩阵(Model-View-Projection)是图形学的基石——它把从物体坐标到屏幕像素的所有复杂数学变换,都浓缩成了三次矩阵乘法。