引言

前置阅读:建议先阅读向量,本文假设你已理解向量的加减法、点积和叉积。

如果说向量是游戏世界的”积木”,那么矩阵 (Matrix) 就是”胶水”。 矩阵的主要作用是变换 (Transformation)。它能把一个物体从一个位置移动到另一个位置,旋转它,缩放它,甚至把它从 3D 世界投影到 2D 屏幕上。

1. 什么是矩阵?

在 3D 游戏开发中,我们最常用的是 4x4 矩阵。它是一个 4 行 4 列的数字网格。

\[ M = \begin{bmatrix} m_{00} & m_{01} & m_{02} & m_{03} \\ m_{10} & m_{11} & m_{12} & m_{13} \\ m_{20} & m_{21} & m_{22} & m_{23} \\ m_{30} & m_{31} & m_{32} & m_{33} \end{bmatrix} \]

为什么是 4x4?

因为我们需要处理 3D 坐标 \((x, y, z)\)。 但是,3x3 矩阵只能表示线性变换(旋转、缩放),无法表示平移 (Translation)。 为了统一处理平移、旋转和缩放,我们引入了齐次坐标 (Homogeneous Coordinates),即增加第四个分量 \(w\)。 一个 3D 点表示为 \((x, y, z, 1)\),一个 3D 向量表示为 \((x, y, z, 0)\)

齐次坐标的妙处:向量的 \(w=0\) 意味着平移矩阵对方向向量不起作用——把方向”平移”没有意义,矩阵自动帮你处理了这个语义区分。

2. 基础变换

Matrix Transforms

平移矩阵 (Translation)

将物体移动 \((tx, ty, tz)\)\[ \begin{bmatrix} 1 & 0 & 0 & tx \\ 0 & 1 & 0 & ty \\ 0 & 0 & 1 & tz \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

缩放矩阵 (Scale)

将物体在各轴上缩放 \((sx, sy, sz)\)\[ \begin{bmatrix} sx & 0 & 0 & 0 \\ 0 & sy & 0 & 0 \\ 0 & 0 & sz & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

旋转矩阵 (Rotation)

绕各轴旋转。例如绕 Z 轴旋转 \(\theta\)\[ \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Unity 中的 TRS 代码实践

在 Unity 中,Matrix4x4 提供了直接构造 TRS 矩阵的 API:

// 构造 TRS 矩阵:先缩放,再旋转,最后平移
Vector3 pos = new Vector3(3f, 0f, 5f);
Quaternion rot = Quaternion.Euler(0f, 90f, 0f);
Vector3 scale = new Vector3(2f, 2f, 2f);

Matrix4x4 trs = Matrix4x4.TRS(pos, rot, scale);

// 将本地空间的点变换到世界空间
Vector3 localPoint = new Vector3(1f, 0f, 0f);
Vector3 worldPoint = trs.MultiplyPoint3x4(localPoint);
// worldPoint ≈ (3, 0, 7)  — 先放大2倍得(2,0,0),绕Y轴转90°得(0,0,2),再平移得(3,0,7)

MultiplyPoint3x4 vs MultiplyPoint:前者跳过透视除法(适用于仿射变换,更快),后者处理 \(w \neq 1\) 的情况(投影矩阵需要)。变换普通顶点位置时用 MultiplyPoint3x4 即可。

3. 矩阵乘法与组合变换

矩阵最强大的地方在于组合。 如果你想先把物体放大 2 倍,然后绕 Y 轴旋转 90 度,再向前移动 10 米。你不需要分别对每个顶点做三次计算。 你可以将这三个矩阵相乘,得到一个模型矩阵 (Model Matrix),然后只用这个矩阵乘以顶点即可。

乘法顺序很重要!

矩阵乘法不满足交换律\(A \times B \neq B \times A\)。 在大多数游戏引擎(如 Unity,使用列向量)中,变换顺序是从右向左读的: \[ M_{final} = M_{translate} \times M_{rotate} \times M_{scale} \] 这意味着:先缩放,再旋转,最后平移。(这是最常用的顺序,也就是 TRS)。

为什么是 TRS 顺序? 如果先平移再缩放,平移量也会被放大;如果先平移再旋转,物体会绕世界原点”公转”而非”自转”。TRS 确保缩放和旋转作用于物体本身,平移最后将它放到世界中。

// 手动组合与 TRS 等价
Matrix4x4 T = Matrix4x4.Translate(pos);
Matrix4x4 R = Matrix4x4.Rotate(rot);
Matrix4x4 S = Matrix4x4.Scale(scale);
Matrix4x4 manual = T * R * S;  // 等价于 Matrix4x4.TRS(pos, rot, scale)

4. MVP 变换与坐标系转换

矩阵的本质是坐标系转换。 在渲染管线中,顶点要经历一系列”空间跳跃”:

  1. 本地空间 (Local/Object Space): 模型的原始坐标,由建模软件定义。
  2. 世界空间 (World Space): 乘以 Model Matrix(即 TRS)。物体在游戏世界中的位置。
  3. 观察空间 (View/Eye Space): 乘以 View Matrix。以摄像机为原点、摄像机前方为 \(-Z\) 轴的坐标系。
  4. 裁剪空间 (Clip Space): 乘以 Projection Matrix。应用透视或正交投影,准备投影到屏幕。

\[ P_{clip} = M_{projection} \times M_{view} \times M_{model} \times P_{local} \]

这就是著名的 MVP 变换 (Model-View-Projection)。

Model Matrix

就是上面说的 TRS——将顶点从本地空间送到世界空间。Unity 中 transform.localToWorldMatrix 就是它。

View Matrix

View Matrix 将整个世界”搬到”摄像机前面。本质上,它是摄像机 Model Matrix 的逆矩阵(参见第 5 节:逆矩阵)。摄像机在世界空间有自己的位置和朝向,对它取逆就等于”把世界反向移动和旋转”,让摄像机回到原点看向 \(-Z\)

// Unity 中获取 VP 矩阵
Matrix4x4 view = camera.worldToCameraMatrix;
Matrix4x4 proj = camera.projectionMatrix;
Matrix4x4 vp = proj * view;

Projection Matrix

将 3D 的视锥体 (Frustum) 映射到标准化设备坐标 (NDC)。两种常见类型: - 透视投影 (Perspective):近大远小,用于大多数 3D 游戏。 - 正交投影 (Orthographic):无近大远小,用于 2D 游戏、UI 或建筑可视化。

5. 逆矩阵

什么是逆矩阵?

矩阵 \(M\) 的逆矩阵 \(M^{-1}\) 满足: \[ M \times M^{-1} = M^{-1} \times M = I \]

其中 \(I\) 是单位矩阵 (Identity Matrix)。直觉上,逆矩阵就是”撤销”变换。如果 \(M\) 把物体从 A 移到 B,那么 \(M^{-1}\) 就把它从 B 移回 A。

游戏中的典型用途

  • View Matrix:如前所述,View Matrix = 摄像机世界矩阵的逆。\(M_{view} = M_{camera}^{-1}\)
  • 世界坐标 → 本地坐标:点击检测时,需要把鼠标射线从世界空间变换回物体的本地空间:
// 将世界空间的点转换到物体的本地空间
Matrix4x4 worldToLocal = transform.worldToLocalMatrix; // = localToWorldMatrix 的逆
Vector3 localHitPoint = worldToLocal.MultiplyPoint3x4(worldHitPoint);
  • 骨骼动画:绑定姿态 (Bind Pose) 的逆矩阵用于将顶点从模型空间转换到骨骼空间,然后再由当前骨骼姿态矩阵变换回来。

Unity API

Matrix4x4 inv = myMatrix.inverse;  // 通用求逆(计算量较大)

性能提示:通用 4x4 矩阵求逆涉及行列式和伴随矩阵,计算开销不小。对于纯旋转矩阵,逆等于转置 (\(R^{-1} = R^T\));对于 TRS 矩阵,可以分别对 T、R、S 求逆再反序组合,比通用算法更快。Unity 内部会做类似优化。

6. 法线变换

为什么法线不能直接用 Model Matrix 变换?

这是一个经典的图形学”坑”。对于位置和切线向量,直接乘以 Model Matrix 没有问题。但法线 (Normal) 不行——当物体有非均匀缩放 (Non-uniform Scale) 时,直接变换法线会导致方向错误。

直觉理解:假设一个球体沿 X 轴压扁变成椭球。表面法线本来垂直于表面,如果和顶点做相同的缩放,法线就不再垂直于压扁后的表面了。

正确做法:逆转置矩阵

法线应该用 Model Matrix 的逆转置矩阵 (Inverse-Transpose) 来变换: \[ \vec{n}_{world} = (M^{-1})^T \cdot \vec{n}_{local} \]

数学证明(简略):法线 \(\vec{n}\) 垂直于切线 \(\vec{t}\),即 \(\vec{n}^T \cdot \vec{t} = 0\)。变换后我们需要 \(\vec{n'}^T \cdot \vec{t'} = 0\)。切线变换为 \(\vec{t'} = M \cdot \vec{t}\),代入推导可得 \(\vec{n'} = (M^{-1})^T \cdot \vec{n}\)

代码实践

// C# — 计算法线变换矩阵
Matrix4x4 model = transform.localToWorldMatrix;
Matrix4x4 normalMatrix = model.inverse.transpose;

Vector3 worldNormal = normalMatrix.MultiplyVector(localNormal).normalized;

在 Shader 中,Unity 内置变量 unity_WorldToObject 就是 Model Matrix 的逆。将它转置后乘以法线即可:

// URP Shader 中的法线变换
float3 worldNormal = normalize(
    mul((float3x3)UNITY_MATRIX_I_M, objectNormal)  // UNITY_MATRIX_I_M = transpose(inverse(M))
);
// 或使用内置函数
float3 worldNormal = TransformObjectToWorldNormal(objectNormal);

优化:如果你确定模型只有均匀缩放 (Uniform Scale),可以直接用 Model Matrix 变换法线再归一化——此时逆转置等于原矩阵的标量倍数。只有非均匀缩放时逆转置才真正有区别。

7. 旋转表示方式对比

在游戏开发中,表示旋转有三种主要方式。它们各有优劣,选择取决于使用场景。

特性 欧拉角 (Euler Angles) 旋转矩阵 (3x3) 四元数 (Quaternion)
存储 3 个浮点数 9 个浮点数 4 个浮点数
直观性 ✅ 最直观,人类可读 ❌ 不直观 ❌ 不直观
万向节死锁 ❌ 有 ✅ 无 ✅ 无
插值 ❌ 不平滑 ❌ 困难且开销大 ✅ Slerp 完美插值
组合旋转 ❌ 不方便 ✅ 矩阵乘法 ✅ 四元数乘法
变换顶点 需先转矩阵 ✅ 直接矩阵乘法 三明治乘法 \(qvq^{-1}\)
GPU 友好 需先转矩阵 ✅ Shader 原生支持 需先转矩阵

实际工程中的选择: - 编辑器/UI:欧拉角——策划和美术人员最容易理解。 - 运行时存储和插值:四元数——紧凑、无万向节死锁、插值平滑。 - Shader 和批量顶点变换:矩阵——GPU 的矩阵乘法硬件加速。

// 三种表示之间的转换 (Unity)
Vector3 euler = new Vector3(0f, 90f, 0f);
Quaternion quat = Quaternion.Euler(euler);
Matrix4x4 mat = Matrix4x4.Rotate(quat);

// 反向转换
Quaternion backToQuat = mat.rotation;
Vector3 backToEuler = quat.eulerAngles;

关于四元数的详细原理、Slerp 插值和万向节死锁的深入分析,请看 下一篇:四元数

总结

  • 矩阵用于存储和组合变换(平移、旋转、缩放),4x4 矩阵配合齐次坐标统一处理一切。
  • TRS 是最常用的组合顺序:先缩放,再旋转,最后平移。
  • MVP 变换将顶点从模型空间经世界空间、观察空间送到裁剪空间。View Matrix 是摄像机矩阵的逆。
  • 逆矩阵用于”撤销”变换,在 View Matrix、坐标转换、骨骼动画中广泛使用。
  • 法线变换必须使用逆转置矩阵,否则非均匀缩放下法线方向会出错。
  • 欧拉角、旋转矩阵、四元数各有所长——理解它们的取舍是 3D 开发的基本功。

< 上一篇: 向量 | 回到目录 | 下一篇: 四元数 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。