二维旋转、缩放、错切:为什么矩阵能表示线性变换

上一篇把点、向量、坐标系这些基础概念捋了一遍,这一篇开始真正进入“矩阵到底在变什么”。

如果说上一篇是在清理术语,这一篇更像是在补 Unity 开发里一条很关键的底层直觉:你对轴的理解一旦错了,后面看旋转、缩放和顺序就只会越看越乱。

如果只停留在“矩阵乘向量会得到一个新向量”,其实还是比较空。

更重要的问题是:

  • 为什么旋转能写成矩阵?
  • 为什么缩放也能写成矩阵?
  • 为什么矩阵乘法顺序一换,结果就完全不同?

把这些问题弄清楚,你后面再看 Unity 里的 TRS、相机矩阵、MVP 链,就不会只是在背 API。

一、线性变换的关键特征是什么

所谓线性变换,可以先粗暴理解成两件事:

  1. 直线还是直线
  2. 原点保持不动

所以像旋转、缩放、错切,都满足这个特点。

但平移不满足,因为平移会把原点从 (0, 0) 挪走。

这也是为什么二维的 2x2 矩阵,能很好地表示旋转、缩放、错切,却不能直接表示平移。

二、缩放矩阵最好理解

如果一个二维点 (x, y),我们想让它在 x 方向放大 sx 倍,在 y 方向放大 sy 倍,那么目标结果就是:

$$
(x’, y’) = (sx \cdot x, sy \cdot y)
$$

写成矩阵就是:

$$
\begin{bmatrix}
sx & 0 \
0 & sy
\end{bmatrix}
\begin{bmatrix}
x \
y
\end{bmatrix}

\begin{bmatrix}
sx \cdot x \
sy \cdot y
\end{bmatrix}
$$

这说明矩阵并不神秘,它只是把“每个轴怎么变”写成统一形式。

sx = sy 时,就是等比缩放。

sx != sy 时,就是非等比缩放。

这个区别在 Unity 里很重要,因为非等比缩放后,很多方向、法线、碰撞和子节点表现都会开始变微妙。

三、旋转矩阵为什么长那个样子

二维旋转通常是很多人第一次觉得“公式开始抽象”的地方。

绕原点逆时针旋转角度 $\theta$ 后,结果写成矩阵是:

$$
\begin{bmatrix}
\cos\theta & -\sin\theta \
\sin\theta & \cos\theta
\end{bmatrix}
$$

如果硬背,很快就忘。

更好的记法还是上一篇提过的那条:看矩阵的列向量。

原来的 x 轴 (1, 0) 旋转后,会变成:

$$
(\cos\theta, \sin\theta)
$$

原来的 y 轴 (0, 1) 旋转后,会变成:

$$
(-\sin\theta, \cos\theta)
$$

把这两个新基向量按列摆进去,就是旋转矩阵。

这个理解方式比背公式稳得多,因为它直接告诉你:

旋转矩阵,本质上是在重定义一组新的坐标轴。

四、错切矩阵最容易在 UI 和 2D 工具里见到

错切不像旋转和缩放那么常见,但它很适合帮助理解“矩阵不一定只是转和缩”。

比如一个简单的 x 方向错切:

$$
\begin{bmatrix}
1 & k \
0 & 1
\end{bmatrix}
$$

它会让点 (x, y) 变成:

$$
(x + ky, y)
$$

也就是说,y 越大,x 被推得越多。

图形会像被侧向拉斜一样。

在 Unity 日常业务里你不一定直接手写错切矩阵,但当你处理一些自定义 UI 网格、2D 变形、特殊动效时,这种思路是会用到的。

五、矩阵乘法顺序为什么不能乱

这是工程里最容易出错的点之一。

先旋转再缩放,和先缩放再旋转,结果通常不一样。

原因很简单:

前一个变换的输出,会成为后一个变换的输入。

如果你先把物体拉宽,再去旋转,旋转的是“已经被拉宽过的局部基向量”。

如果你先旋转,再缩放,缩放发生在旋转后的轴方向上。

这两个过程不是一回事。

所以矩阵乘法虽然看起来只是把几个矩阵连乘起来,但它其实是在描述一条有先后顺序的变换流水线。

六、Unity 里最常见的误解:以为旋转永远绕世界轴发生

很多人一开始看 transform.Rotate,或者自己做插值动画时,会把“旋转”理解成一个单一动作。

但真正影响结果的是:

  • 你绕的是局部轴还是世界轴
  • 变换是在父节点之前还是之后
  • 当前对象是否已经有缩放

只要把矩阵乘法的顺序意识带进来,就会知道:

同样叫“旋转 30 度”,放在不同变换链位置里,结果可以完全不同。

七、用一个二维例子把顺序差异看直观

假设一个点先执行:

  1. x 方向放大 2 倍
  2. 再逆时针旋转 90 度

和先:

  1. 逆时针旋转 90 度
  2. 再 x 方向放大 2 倍

结果不会一样。

因为“x 方向”这个概念,在旋转前后本来就变了。

这个例子对应到 Unity 里,就是为什么父节点缩放 + 子节点旋转,经常让人觉得“不符合直觉”。

其实不是引擎错,是你正在对一组已经变化过的坐标轴继续操作。

如果你想把这个差异在 Unity 里直接打出来,可以写一个最小实验:

1
2
3
4
5
6
7
8
9
10
Vector3 point = new Vector3(1f, 0f, 0f);

Matrix4x4 scaleFirst = Matrix4x4.Rotate(Quaternion.Euler(0f, 0f, 90f))
* Matrix4x4.Scale(new Vector3(2f, 1f, 1f));

Matrix4x4 rotateFirst = Matrix4x4.Scale(new Vector3(2f, 1f, 1f))
* Matrix4x4.Rotate(Quaternion.Euler(0f, 0f, 90f));

Debug.Log(scaleFirst.MultiplyPoint3x4(point));
Debug.Log(rotateFirst.MultiplyPoint3x4(point));

哪怕只是看两行输出,也会比背“顺序不同结果不同”这句话更有体感。

八、学到这里,应该建立什么工程直觉

这篇最重要的不是记住所有矩阵形式,而是形成下面几条直觉:

  1. 矩阵描述的是基向量怎么变。
  2. 缩放、旋转、错切都是在线性框架里重排坐标轴。
  3. 变换顺序不同,结果通常不同。
  4. 只要原点移动了,就已经不再是单纯的线性变换。

这最后一点,正好会把我们带到下一篇。

九、为什么下一步一定要学齐次坐标

你会发现,到目前为止,我们能描述很多事情,但还有一个最常见的动作没纳入统一框架:平移。

而游戏开发里,平移又偏偏是最高频的。

所以图形学里才会引入齐次坐标和 3x3、4x4 矩阵,把位移也装进一套统一表达方式里。

下一篇就专门讲这个桥是怎么搭起来的。

十、总结

如果只看公式,旋转矩阵、缩放矩阵、错切矩阵很容易越学越散。

但如果你始终盯着“坐标轴怎么变、顺序怎么串”,这些矩阵就会开始统一起来。

后面进入 Unity 的 Matrix4x4 之前,这个理解是必须有的。否则你看到 4x4 只会更晕,不会更清楚。