矩阵和线性变换入门:先把点、向量和坐标系说清楚

很多 Unity 程序员第一次碰矩阵,往往是在 Shader、相机或者 Matrix4x4.TRS 这里。

API 能背,代码也能抄,但只要一遇到“为什么乘法顺序一变结果就错了”“为什么局部坐标和世界坐标对不上”,脑子就会乱。

根因通常不是不会写代码,而是前面的基础概念没真正连起来。

我自己最早也是先在项目里撞墙,后面才倒回来看这些基础概念。尤其是做挂点、技能范围和 UI 跟随时,只要点、向量、坐标系三件事没分开,后面查问题会非常慢。

这篇先不急着上 4x4,也不急着讲公式推导,先把最关键的三件事捋顺:点、向量、坐标系。

一、矩阵本质上是在描述“坐标如何变化”

如果只记一句话,我建议记这个:

矩阵不是一张表,它是一个“变换规则”。

同一个点,用不同坐标系描述,会得到不同数字;同一个向量,在旋转、缩放之后,也会得到新的数字。矩阵做的事情,就是把“旧坐标”稳定地映射成“新坐标”。

在图形学和游戏开发里,这件事太常见了:

  • 把模型局部顶点变到世界空间
  • 把世界坐标变到相机空间
  • 把一个方向从角色本地前方变成世界中的真实朝向
  • 把 UI 点位从屏幕空间转换到世界空间

所以,矩阵不是数学课里的额外负担,它其实就是坐标空间之间的翻译器。

二、先分清点和向量

这两个词在代码里经常混着用,但在矩阵语境下必须分清。

1. 点是什么

点表示“位置”。

比如 (3, 2) 表示二维平面上的一个位置,(1, 5, -2) 表示三维空间里的一个位置。

点最重要的特征是:它依赖原点。

原点换了,同一个点的坐标数值就会变化。

2. 向量是什么

向量表示“方向和长度”。

比如“向右走 3 个单位,向上走 2 个单位”,这就是向量 (3, 2)

向量不关心自己从哪里出发,它只关心偏移量本身。

这也是为什么在 Unity 里:

  • transform.position 更像点
  • transform.forwardtransform.rightVector3.up 更像向量

当你理解“点是位置,向量是偏移”,后面再看 MultiplyPointMultiplyVector 的差别,就不会只靠死记硬背了。

三、坐标系决定了数字的含义

很多初学者会误以为一个坐标数字是绝对真实的。

其实不是。

坐标一定是相对于某个坐标系而言的。

例如角色脚下有个武器挂点,它的本地坐标可能是 (0.2, 1.1, 0)。这个数字只在角色自己的局部坐标系里有意义。一旦角色转身、缩放、移动,这个挂点在世界空间里的真实位置就会变化。

于是就有了一个最常见的需求:

把局部坐标转换成世界坐标。

这件事背后用的就是矩阵。

四、为什么 2x2 矩阵已经足够解释线性变换

先看二维情况最直观。

一个二维向量:

$$
\begin{bmatrix}
x \
y
\end{bmatrix}
$$

乘上一个二维矩阵:

$$
\begin{bmatrix}
a & b \
c & d
\end{bmatrix}
\begin{bmatrix}
x \
y
\end{bmatrix}
$$

得到的新结果,其实就是这个向量被“重新组合”之后的坐标。

更实用的理解方式是:

矩阵的每一列,代表原坐标系基向量变换后的去向。

也就是说,矩阵其实在回答两个问题:

  • 原来的 x 轴现在指向哪里?
  • 原来的 y 轴现在指向哪里?

只要这两个基向量确定了,整个平面里所有向量的新位置也就都确定了。

这就是为什么矩阵特别适合描述旋转、缩放、错切这种线性变换。

五、Unity 里最值得建立的一个直觉:列向量就是“新坐标轴”

这是我觉得最有用的一条工程直觉。

在 Unity 里看 localToWorldMatrix 时,不要把它当成一堆数字。你可以先把它理解成:

  • 第一列:局部 x 轴在世界里变成了什么方向
  • 第二列:局部 y 轴在世界里变成了什么方向
  • 第三列:局部 z 轴在世界里变成了什么方向
  • 第四列:物体原点在世界里的位置

一旦这么看,很多问题会突然变简单:

  • 为什么物体旋转后 right/up/forward 变了
  • 为什么父节点缩放会影响子节点方向长度
  • 为什么非等比缩放会让局部坐标系“看起来歪掉”

这些都不是引擎在“偷偷做魔法”,而是矩阵把局部基向量映射到了新空间。

六、线性变换为什么不包含平移

这个点在图形学里很重要。

严格来说,线性变换要求原点不动。

所以旋转、缩放、错切都属于线性变换,但平移不属于。

这也是为什么后面讲 Unity 的 TRS 矩阵时,会引出齐次坐标和 4x4 矩阵。因为我们需要一种办法,把“本来不属于线性变换”的平移也放进统一的矩阵框架里处理。

先记住结论就够了:

  • 纯旋转、缩放、错切可以用线性变换理解
  • 平移需要更进一步的表示方式

下一篇就接着讲这个过渡是怎么发生的。

七、学矩阵时,Unity 里最常见的三个落地点

如果你是做客户端开发,不一定天天手写矩阵,但下面这些地方一定绕不过去:

1. Transform 坐标转换

TransformPointInverseTransformPointTransformDirection 这些接口,本质上都是矩阵乘法的封装。

2. Shader 顶点变换

模型空间到世界空间、观察空间、裁剪空间的连续变换,背后就是 MVP 矩阵链。

3. 相机和 UI 坐标换算

从屏幕点击位置反推世界坐标,或者把 3D 点投影到 UI 上,本质上都是空间变换问题。

也就是说,学矩阵不是为了考试,而是为了以后调这些问题时知道自己到底在调什么。

八、现阶段不用死磕推导,先把这几个认知钉牢

我建议先记住下面 4 条:

  1. 矩阵描述的是变换规则,不只是数字表格。
  2. 点是位置,向量是偏移,两者在变换里不能混看。
  3. 坐标永远依赖坐标系,局部和世界只是两个不同描述方式。
  4. 矩阵的列,可以理解为变换后的基向量。

只要这 4 条稳了,后面再看旋转矩阵、4x4、TRS、MVP,理解速度会快很多。

九、一个很实用的练习方法

如果你现在就在做 Unity 项目,我建议你开一个空场景做这几个实验:

  1. 建一个父物体和一个子物体。
  2. 分别修改父物体的位移、旋转、缩放。
  3. 实时打印子物体的 localPositionpositionlocalToWorldMatrix
  4. 对照 Scene 视图看 right/up/forward 的变化。

如果你懒得自己搭太多对象,可以先用一段最小代码直接感受“点”和“方向”进入世界空间之后的区别:

1
2
3
4
5
6
7
8
Vector3 localPoint = new Vector3(0f, 0f, 2f);
Vector3 localDir = Vector3.forward;

Vector3 worldPoint = transform.TransformPoint(localPoint);
Vector3 worldDir = transform.TransformDirection(localDir);

Debug.Log($"worldPoint = {worldPoint}");
Debug.Log($"worldDir = {worldDir}");

这个例子很简单,但很适合拿来建立第一层直觉:

  • 点会跟着物体位置一起走
  • 方向会跟着朝向变化,但不是一个“有落点的位置”

只做一遍,你对“矩阵在改什么”会比看半天公式更有感觉。

十、总结

矩阵最难的地方,不是算,而是抽象。

但一旦你把它和 Unity 的空间变换、Transform、Shader 顶点流转对应起来,它就不再是纯理论,而是非常实用的工程工具。

这篇先打地基。

下一篇我会继续讲二维旋转、缩放、错切分别是怎么落到矩阵上的,以及为什么“矩阵乘法顺序”会直接决定结果对不对。