矩阵与线性变换
本篇位置:模块一 · 第 2 篇 | 前置:向量与坐标系 | 预计阅读:25 分钟
学习目标
- 把矩阵读成「对整个空间做的一次变换」,而不是一张摆数字的表格。
- 记住旋转、缩放、剪切矩阵的形状,并能从「基向量变成了什么」直接推导出它们,而不是死记硬背。
- 会组合多个变换,并能准确解释矩阵乘法顺序为什么会影响结果。
- 说清楚逆矩阵与转置在图形学里的两个高频用途:撤销一个变换、以及正确地变换法线。
矩阵:变换后的基向量按列摆放
上一篇讲向量时提到,任何一个二维向量 都可以写成 ——沿 轴基向量(basis vector) 走 步,再沿 轴基向量 走 步。这个看似平淡的拆分,正是理解矩阵的钥匙。
一个线性变换(linear transformation)——你可以先粗略理解成「保持网格线平行且等距、原点不动的空间变形」——完全由它把 、 变到哪里决定。假设变换后 落到了 , 落到了 ,那么根据线性性(可以拆开、可以按比例缩放),原来的向量 变换后就落在 这个位置。把 、 当作两列摆在一起,写成矩阵 ,上面这句话就变成了矩阵乘法的定义本身:。换句话说,矩阵乘法的计算规则不是谁拍脑袋定的,它就是「把变换后的基向量按坐标加权求和」这件事的记账方式。
这个视角(常被称为 3Blue1Brown 视角)带来一个立刻可用的读图技巧:拿到任意一个 矩阵,只看它的第一列,就知道 被变成了什么方向和长度;只看第二列,就知道 被变成了什么。反过来,想构造一个变换,只需要想清楚「 应该去哪、 应该去哪」,把答案按列摆好,矩阵就写出来了——下一节的旋转、缩放、剪切矩阵全部用这个方法推导。
下图把这个直觉具象化了:每个子图先画出变换前的网格(等距、互相垂直的坐标线),再把同一个矩阵作用到一个大写字母 F 的多边形上(选 F 是因为它不对称,能看出旋转方向和是否被镜像)。四个子图分别是恒等变换、旋转 30°、非均匀缩放 、以及剪切系数 的剪切变换;网格线弯折或倾斜的方式,直接对应着矩阵的两列把 、 送到了哪里。
图:四种矩阵对网格与字母 F 的作用。灰色细线是变换后的网格(原本等距垂直的坐标线),橙色多边形是变换后的字母 F;对比网格的形变方式就能读出矩阵在做什么。
三种基本 2D 变换
有了「矩阵的列 = 基向量的落点」这个工具,三种最常用的 2D 变换都能几笔推出来,而不必死记公式。
旋转(rotation):把 绕原点逆时针转过角度 ,落点是 ;把 同样转过 ,落点是 (把 看成 再转 90°,容易验证)。两列摆在一起就是
对照上图第二个子图:网格线整体转了 30°,长度和夹角都没变——这正是旋转矩阵的特征,马上第 6 节会看到它的行列式恒为 1。
缩放(scale):沿 、 轴独立拉伸, 只在长度上变成 , 变成 ,方向都不变。矩阵是对角矩阵
图中第三个子图用的是 :横向被拉长,纵向被压扁,网格依然是矩形(因为坐标轴方向没被扰动),但格子不再是正方形。
剪切(shear): 保持不动, 沿 方向偏移 个单位,变成 ,矩阵是
图中第四个子图()网格从矩形变成了平行四边形——竖线全部倾斜了,横线完全没动,这正对应「只有 那一列变了」。
值得专门指出一件事:平移(translation)不是线性变换。线性变换的定义要求原点不动(),而平移恰恰是把每个点都挪动一个固定的偏移量 ,原点会被挪到 ——只要 ,这个映射就不满足线性性,因此不存在任何 矩阵能表示它。这不是一个可以绕过的小麻烦:旋转、缩放、剪切能用矩阵乘法优雅地表示和组合,平移却格格不入,逼得图形学要引入第三篇要讲的齐次坐标(homogeneous coordinates)——用多加一维的技巧把平移也塞进矩阵乘法的框架里。这里先留下这个悬念。
变换组合与顺序
多个矩阵可以像函数一样复合。先对向量 施加变换 ,再施加变换 ,结果是 ;矩阵乘法满足结合律,所以这等于 ——把「先 后 」这句话翻译成矩阵语言时, 写在右边(离向量近), 写在左边,乘出来的合成矩阵 才是「先 后 」这个组合动作本身。读矩阵乘法链时,永远要从最靠近向量的那一项开始读起,这个习惯在后面几乎每一篇涉及 MVP 矩阵(Model-View-Projection)的地方都会用到。
矩阵乘法不满足交换律,「先旋转后缩放」和「先缩放后旋转」是两个不同的变换——不是记不住顺序的小瑕疵,而是几何上确实不一样。用一组具体数字算一遍就很清楚:取旋转 90° 的矩阵 ,缩放矩阵 ,作用在向量 上。
先缩放后旋转(矩阵写作 , 靠近 先生效):先算 ,再对这个结果转 90°,得到 。
先旋转后缩放(矩阵写作 ):先算 ( 转 90° 后指向 轴正方向),再对这个结果做 缩放,得到 。
同一个起点 , 把它送到 , 把它送到 ——长度差了一倍, 一目了然。直觉上也说得通:先缩放会把向量在 方向拉长到 2,随后整体旋转会把这个「已经被拉长的量」转到 轴上,于是 分量继承了那个 2;而先旋转会把向量转到 轴上,此时它已经离开了被拉伸的 轴,后续的缩放对它的 分量只按 缩放,量值自然留在 1。这个例子也解释了为什么图形引擎里「先缩放模型、再旋转、再平移」和「先旋转、再缩放、再平移」会画出两个完全不同的物体——顺序错了,矩阵不会报错,画面会。
行主序与列主序
这里最容易把两件不同的事情混在一起:数学写法决定向量是列向量还是行向量、矩阵写在左边还是右边;内存布局决定矩阵元素在一段连续内存里怎么摆放。二者是正交的两个问题,工程上却经常被打包讨论,值得拆开说清楚。
本篇的推导都遵循数学书最常见的写法:向量是列向量,矩阵写在左边、右乘向量,即 ——这也是矩阵「列」直接对应基向量落点(第 1 节)的原因。但矩阵要拍平成一段连续内存交给 GPU 时,还有第二个问题:先存第一行还是先存第一列?
行主序(row-major):连续存放第一行、再存第二行……C/C++ 里 float m[4][4] 默认就是这个布局。列主序(column-major):连续存放第一列、再存第二列……GLSL 与配套的 GLM 库都用这个约定,一个 glm::mat4 在内存里就是 16 个 float,前 4 个就是第一列。
两种布局没有对错之分,纯属编码约定;但若从行主序习惯的自研矩阵库取出数据,原样交给期待列主序的 GLSL uniform,GPU 拿到的其实是被转置过的矩阵——非对称变换(旋转、剪切)会算错,而恰好对称的矩阵(比如均匀缩放)看不出问题,这正是这类 bug 常在"某些特定物体或角度下"才暴露的原因。
glUniformMatrix4fv(location, count, transpose, value) 的第三个参数就是为此准备的:数据若是行主序,把 transpose 设成 GL_TRUE,驱动会在上传时帮你转置。但这里有个 Android/GLES 工程师尤其要记住的限制:桌面 OpenGL 允许 transpose 取 GL_TRUE 或 GL_FALSE,而 OpenGL ES 只接受 GL_FALSE——传 GL_TRUE 会直接触发 GL_INVALID_VALUE 错误。也就是说在 GLES 上,行主序矩阵必须自己先转置好再上传,不能指望驱动代劳;这也是 GLM 默认按列主序存储的原因——glm::value_ptr() 配 GL_FALSE 就能直接传给 GLES。
逆与转置
逆矩阵(inverse matrix) 满足 ,几何意义是「撤销」 这个变换。一般求逆代价不低,但旋转矩阵有个便宜的捷径:旋转矩阵是正交矩阵(orthogonal matrix,各列单位长度且两两垂直),对正交矩阵而言
转置(transpose)只需沿对角线翻一下,几乎零成本,比通用求逆快得多。直觉上也容易验证:把 转置,两个 位置互换、符号相当于取反,结果恰好等于 ——「反着转回去」正是撤销旋转该做的事。
转置的第二个高频用途更隐蔽:变换法线(normal)。假设模型顶点用矩阵 变换了,一个自然但错误的想法是「法线也用同一个 变一下」——这在纯旋转、均匀缩放时凑巧是对的,但只要 含非均匀缩放就会把法线搞歪。用一个能亲手验算的例子看清楚:取切线方向 ,对应的法线是与它垂直的 (验证:)。用 变换表面:切线变成 。若直接用 变换法线,得到 ;检验垂直关系,——法线已不再垂直于表面。
正确做法是用逆转置矩阵:
代入例子,(对角矩阵转置等于自身),得 ;再检验,,垂直关系保住了。直觉上, 怎么拉伸空间,法线就要反着补偿——逆转置矩阵恰好抵消了非均匀缩放带来的角度畸变。当 是纯旋转(正交矩阵)时 , 退化成 本身,这也解释了为什么纯旋转、均匀缩放场景里「法线直接乘 」凑巧是对的。反过来,这正是排查「模型非均匀缩放后光照突然不对」这类 bug 时第一个要检查的地方:法线矩阵有没有单独算成 ,而不是偷懒直接复用顶点变换矩阵。
行列式:面积缩放与方向
行列式(determinant) 有一个很直接的几何含义:单位正方形(或者说 、 围成的那块面积为 1 的区域)经过 变换后,面积变成了 ——它就是这个变换的「面积缩放因子」。三种取值情形分别对应三种不同的几何后果。
:面积按 缩放,绕序(winding order,即顶点按顺时针还是逆时针排列)保持不变。旋转矩阵是一个很好的验证:,说明旋转既不缩放面积也不改变绕序——这符合直觉,旋转只是刚性地转个方向。剪切矩阵的行列式 也恒为 1,是个稍反直觉但值得记住的事实:剪切看起来把图形"拉斜"了,但严格保面积。
:面积仍按 缩放,但绕序被翻转——这就是镜像(mirror)。图形学里一个经典翻车场景是:为了做左右镜像的角色模型,直接把模型矩阵的 设成 ,这个矩阵的行列式变成负数,所有三角形的顶点绕序(原本约定逆时针为正面)全部反了过来。如果渲染管线的背面剔除(backface culling)设置没有跟着调整(比如 OpenGL 里对应的 glFrontFace 状态),本该朝向摄像机的面会被当成背面剔除掉,模型看起来大片镂空或者内部结构透出来——这正是第 1 篇结尾提到的「绕序反了,法线就反向」在行列式语言下的另一种表述。
:矩阵不可逆,空间被压扁成了更低的维度——比如整张平面被压成一条线,或者某个方向的缩放系数变成了 0。这时候上一节的求逆操作没有解:数值上会表现为除以一个等于(或极接近)0 的行列式,产生 inf 或 NaN。在实际调试中,一个模型矩阵的行列式突然变成 0(或极小),几乎总是意味着某个缩放分量被意外置零了——这是排查「物体突然消失或者变换后计算爆炸」这类问题时值得优先检查的一个数值。
🎯 岗位关联
超分辨率(super-resolution)与插帧算法里大量出现的图像 warp(按运动场或参考帧把像素重新采样到新位置),本质上就是对每个像素坐标做一次矩阵变换(往往还叠加了透视或光流场的局部近似)——这篇讲的旋转、缩放、剪切组合,正是构造和理解这些 warp 矩阵的基础语言。做渲染分析时,RenderDoc 里能看到的 Transform 面板、每个 draw call 的 Model/View/Projection 矩阵,全都是本篇这套矩阵在起作用——读懂一个矩阵的列在做什么,是快速判断"这一步变换到底把顶点送去了哪里"的最短路径。而法线逆转置公式 ,是排查"模型做了非均匀缩放之后光照突然不对"这一类 bug 时最直接的钥匙:只要看到场景里有非均匀缩放(角色装备、拉伸的地形、程序化生成的物体),法线矩阵有没有单独算成逆转置,就是第一个该问的问题。
小结
| 变换 | 矩阵形状(2D) | 逆的性质 |
|---|---|---|
| 恒等 identity | 自逆, | |
| 旋转 rotation | 正交矩阵, | |
| 缩放 scale | 对角矩阵,(需 ) | |
| 剪切 shear | ,行列式恒为 1 | |
| 平移 translation | 非线性,2×2 矩阵无法表示 | 需齐次坐标扩展一维(见下一篇) |
延伸阅读
- 3Blue1Brown《线性代数的本质》第 3–7 集(基向量、矩阵乘法、行列式、逆矩阵):https://www.bilibili.com/video/BV1ys411472E
- songho 的 OpenGL Matrix(行主序/列主序、glUniformMatrix 细节):https://www.songho.ca/opengl/gl_matrix.html
- GLM 官方手册:https://github.com/g-truc/glm
