第五章:矩阵与变换——从数学到屏幕的魔法

说实话,矩阵这东西,我第一次接触的时候也是一头雾水。大学线性代数课上,老师讲得天花乱坠,我就在想:这玩意儿到底能干嘛?直到我开始做 OpenGL,才恍然大悟——原来矩阵就是图形学的「通用语言」。今天我们就来聊聊,怎么用矩阵把 3D 世界搬到 2D 屏幕上。

5.1 正交投影 vs 透视投影:你选哪个?

先问个问题:你希望你的 3D 场景看起来像「工程图纸」还是「真实照片」?

正交投影,说白了就是没有近大远小。不管物体离你多远,大小都一样。我最早做 CAD 工具时用的就是它——因为要精确测量尺寸,透视反而碍事。

透视投影,模拟人眼效果。近处的东西大,远处的东西小。游戏、VR、AR 都用它。嗯,这里有个坑:透视投影的矩阵计算比正交复杂不少,但效果也真实得多。

核心区别一句话:

  • 正交投影:平行线永远平行
  • 透视投影:平行线在远处汇聚于一点(灭点)

我在项目中遇到过一件事:有个同事做地图应用,用了透视投影,结果远处的地标缩成一个小点,用户根本看不清。后来改成正交投影,问题就解决了。所以,选哪个,看你的应用场景。

5.2 平移、旋转、缩放:三大基本变换

这三个操作,是 3D 图形学的「三原色」。任何复杂的变换,都可以拆解成它们的组合。

5.2.1 平移矩阵

平移,就是把物体从 A 点挪到 B 点。数学上很简单:

// 平移矩阵(4x4)
[1, 0, 0, tx]
[0, 1, 0, ty]
[0, 0, 1, tz]
[0, 0, 0, 1 ]

tx、ty、tz 就是你在 x、y、z 方向上移动的距离。为什么用 4x4 矩阵?因为 3x3 搞不定平移——这是个历史遗留问题,但也是标准做法。

5.2.2 旋转矩阵

旋转稍微麻烦点。绕 x 轴、y 轴、z 轴旋转,公式都不一样。我记性不好,从来不去背这些公式,直接查表或者用现成的函数。

// 绕 Z 轴旋转 θ 角度
[cosθ, -sinθ, 0, 0]
[sinθ,  cosθ, 0, 0]
[0,      0,    1, 0]
[0,      0,    0, 1]

我的小技巧:旋转顺序很重要!先绕 x 再绕 y,和先绕 y 再绕 x,结果完全不同。我习惯用「XYZ」顺序,但具体看项目需求。

5.2.3 缩放矩阵

缩放最简单:

[sx, 0,  0,  0]
[0,  sy, 0,  0]
[0,  0,  sz, 0]
[0,  0,  0,  1]

sx、sy、sz 是各轴的缩放比例。如果三个值相等,就是均匀缩放;不等,就是拉伸。

注意:非均匀缩放(比如 sx=2, sy=1, sz=1)会导致法向量计算出错。我曾经在这上面栽过跟头——光照效果怎么调都不对,最后发现是缩放矩阵搞的鬼。

5.3 MVP 矩阵:从模型到屏幕的完整流水线

MVP 是 Model、View、Projection 的缩写。说白了,就是三个矩阵的乘积:

MVP = Projection × View × Model

为什么要分开?因为每个矩阵管一件事:

  • Model 矩阵:把物体从「本地坐标」变换到「世界坐标」。比如一个茶壶,它的顶点坐标是相对于茶壶本身的,Model 矩阵把它放到游戏场景中。
  • View 矩阵:把「世界坐标」变换到「观察坐标」。说白了,就是「摄像机」的位置和朝向。
  • Projection 矩阵:把「观察坐标」变换到「裁剪坐标」。这一步决定了你是正交还是透视。

我刚开始学的时候,总搞不清 View 和 Model 的区别。后来想明白了一件事:移动摄像机,和反向移动整个世界,效果是一样的。View 矩阵其实就是「反向的 Model 矩阵」——只不过作用在摄像机上。

5.4 知识体系总览

下面这张图,是我自己总结的 MVP 矩阵完整流程。你看一遍,应该就能串起来了:

MVP 矩阵变换流水线 本地坐标 物体自身的顶点数据 Model 世界坐标 场景中的位置和朝向 View 观察坐标 摄像机视角下的坐标 Proj 裁剪坐标 视锥体裁剪后的结果 透视除法 NDC 坐标 归一化设备坐标 [-1,1] 视口变换 屏幕坐标 最终像素位置 整个流程:本地 → 世界 → 观察 → 裁剪 → NDC → 屏幕 MVP = Projection × View × Model 注意:矩阵乘法顺序从右到左

5.5 实战:构建 MVP 矩阵

理论说完了,来点实际的。假设我们要渲染一个旋转的立方体:

// 伪代码:构建 MVP 矩阵
Matrix4x4 model = Matrix4x4.identity();
model = model * Matrix4x4.translate(0, 0, -5);   // 移到摄像机前方
model = model * Matrix4x4.rotateY(angle);         // 绕 Y 轴旋转

Matrix4x4 view = Matrix4x4.lookAt(
    vec3(0, 2, 5),   // 摄像机位置
    vec3(0, 0, 0),   // 看向原点
    vec3(0, 1, 0)    // 上方向
);

Matrix4x4 projection = Matrix4x4.perspective(
    45.0f,           // 视场角 45°
    16.0f / 9.0f,    // 宽高比
    0.1f,            // 近裁剪面
    100.0f           // 远裁剪面
);

Matrix4x4 mvp = projection * view * model;

避坑指南:我曾经把投影矩阵的 near 值设成了 0,结果画面全黑了。为什么?因为 near 必须大于 0,否则透视除法会出问题。记住:near 和 far 都要是正数,且 near > 0。

5.6 矩阵运算的注意事项

最后,分享几个我踩过的坑:

  1. 矩阵乘法不满足交换律:A × B ≠ B × A。所以顺序一定要搞对。
  2. OpenGL 是列主序:存储矩阵时,先存列再存行。这和数学课本上的写法是转置关系。我刚开始就搞反了,结果图形全乱套了。
  3. 避免频繁计算逆矩阵:逆矩阵计算开销很大。如果只是做变换,用正向矩阵就够了。
  4. 注意浮点精度:连续做几十次矩阵乘法后,误差会累积。我习惯每隔一段时间重新计算一次,而不是一直累乘。

好了,矩阵与变换这部分就聊到这儿。记住一句话:矩阵是工具,不是目的。理解它背后的几何意义,比死记硬背公式重要得多。


公众号:蓝海资料掘金营,微信deep3321