/ Ch07 变换矩阵

第7章:变换矩阵 — 零基础讲义

讲义说明
本讲义基于 Steve Marschner & Peter Shirley 所著《虎书5》第5版第7章(p.145-166)。
变换矩阵是计算机图形学的"语法"——它是描述物体如何移动、旋转、缩放的统一数学语言。不理解变换矩阵,就无法理解观察、投影、裁剪等后续所有内容。
本版插画采用 Guizang 材质插画风格重新绘制。

学习目标

  1. 掌握2D和3D的线性变换矩阵
  2. 理解齐次坐标的作用——为什么需要它
  3. 掌握旋转矩阵的五步推导过程——核心重点
  4. 理解法线变换与顶点变换的区别——极易出错
  5. 掌握从模型到屏幕的完整变换管线
  6. 学会通过矩阵组合实现任意变换

7.1 什么是变换矩阵?

7.1.1 核心概念

变换矩阵 = 用乘法统一描述几何变换的数学工具

基本思想极其直观:

v' = M × v

把变换前的坐标向量 v 乘以变换矩阵 M,得到变换后的坐标 v'。就这样——所有的几何变换(移动、旋转、缩放、扭曲)都可以用矩阵乘法来描述。

变换类型矩阵形式例子
缩放对角矩阵diag(sx, sy, sz)
旋转正交矩阵绕x/y/z轴旋转θ角
平移齐次矩阵第四列包含位移量
剪切非对角元素改变形状但不改变面积

7.2 2D线性变换基础

7.2.1 2D缩放

S(sx, sy) = [sx 0; 0 sy]
// 缩放变换应用
[x']   [sx  0 ] [x]
[y'] = [0  sy ] [y]

x' = sx · x
y' = sy · y
符号含义例子
sxx方向缩放因子sx=0.5 → 水平缩小一半
syy方向缩放因子sy=2 → 垂直放大一倍
均匀缩放sx = syS(2,2) → 整体放大2倍,保持比例
非均匀缩放sx ≠ syS(2,1) → 水平拉长,垂直不变
// 数值例子:将矩形缩小一半
S(0.5, 0.5) = [0.5  0]
              [0   0.5]

点 (2, 4) → (2×0.5, 4×0.5) = (1, 2)  // 坐标减半
缩放与错切——单位正方形的变换
图7-1:缩放与错切——单位正方形经过缩放(拉伸)和错切(倾斜)后的变化

7.2.2 2D旋转(标准公式)

R(θ) = [cosθ -sinθ; sinθ cosθ]
// 绕原点逆时针旋转θ角
x' = x·cosθ - y·sinθ
y' = x·sinθ + y·cosθ

这个公式看起来简单,但它不是凭空产生的。下一节我们完整推导它。

关键记忆:cosθ在对角线,sinθ在反对角线带符号。第一行是cosθ和-sinθ,第二行是sinθ和cosθ。

7.3 旋转矩阵的五步推导(极坐标法)

这是本章最重要的内容。我们从零开始,逐步推导出旋转矩阵。这不是一个要死记硬背的公式——理解推导过程后,你可以随时"重新发明"它。

2D旋转的极坐标推导
图7-2:旋转矩阵的极坐标推导——从(x,y) = (r·cosα, r·sinα)出发,旋转θ后坐标由三角恒等式导出

第一步:用极坐标表示点

任意向量 (x, y) 可以用极坐标表示:

x = r·cosα, y = r·sinα
    y
    ↑
    │     / (x, y)
    │    /
    │   /    r = √(x²+y²)  ← 向量长度
    │  /     α = atan2(y, x) ← 与x轴夹角
    │ /
    │/α
    └────────────→ x

解释:向量 (x, y) 就像从原点射出的箭,长度 r,与x轴夹角 α。

第二步:写出旋转后的坐标

将向量逆时针旋转 θ 角度后,新向量与x轴的夹角变为 α + θ:

x' = r·cos(α + θ), y' = r·sin(α + θ)

第三步:三角恒等式展开

使用和角公式(三角恒等的核心):

cos(α + θ) = cosα·cosθ - sinα·sinθ
sin(α + θ) = sinα·cosθ + cosα·sinθ

代入:

x' = r·(cosα·cosθ - sinα·sinθ)
   = r·cosα·cosθ - r·sinα·sinθ

y' = r·(sinα·cosθ + cosα·sinθ)
   = r·sinα·cosθ + r·cosα·sinθ

第四步:代回原始坐标

因为 x = r·cosα, y = r·sinα,所以:

x' = x·cosθ - y·sinθ
y' = x·sinθ + y·cosθ

第五步:写成矩阵形式

R(θ) = [cosθ -sinθ; sinθ cosθ]
// 验证矩阵形式
[x']   [cosθ  -sinθ] [x]
[y'] = [sinθ   cosθ] [y]

x' = x·cosθ + y·(-sinθ) = x·cosθ - y·sinθ  ✓
y' = x·sinθ + y·cosθ                       ✓

验证:旋转90°

θ = 90° ⇒ cos90°=0, sin90°=1:

R(90°) = [0  -1]
         [1   0]

点 (1, 0) → (0·1 + (-10, 1·1 + 0·0) = (0, 1)  ✓  // x轴→y轴
点 (0, 1) → (0·0 + (-11, 1·0 + 0·1) = (-1, 0) ✓  // y轴→-x轴

验证通过!(1,0) 逆时针转90° 到 (0,1),再转到 (-1,0)——完美符合直觉。

🤔 想一想:为什么顺时针旋转的矩阵是逆时针的转置?
顺时针转θ = 逆时针转(-θ),而 cos(-θ)=cosθ, sin(-θ)=-sinθ。所以 R_cw(θ) = [cosθ sinθ; -sinθ cosθ] = R_ccw(θ)ᵀ。对于旋转矩阵,转置 = 逆矩阵——这就是为什么顺时针旋转是逆时针旋转的逆!

3D旋转矩阵

从2D扩展到3D:每个基本旋转保持一个轴不变,在其他两个轴上做2D旋转。

旋转轴矩阵不变量
绕x轴[1 0 0; 0 cosθ -sinθ; 0 sinθ cosθ]x坐标不变
绕y轴[cosθ 0 sinθ; 0 1 0; -sinθ 0 cosθ]y坐标不变
绕z轴[cosθ -sinθ 0; sinθ cosθ 0; 0 0 1]z坐标不变
规律记忆:保持哪个轴,那行/列就是单位向量。其余4个元素就是2D旋转矩阵。注意绕y轴时sinθ符号是反的(因为右手坐标系中y轴方向特殊)。

7.4 齐次坐标与平移

7.4.1 线性变换的致命局限

2×2矩阵(以及3×3矩阵)只能表示线性变换——旋转、缩放、剪切。它不能表示平移

没办法用2×2矩阵写出:
x' = x + tx
y' = y + ty

因为这是加法(+tx),不是乘法(×某系数)。
线性变换要求输出是输入的线性组合(只有乘法,没有加法)。
平移不是线性变换!它需要加法,而矩阵只能做乘法。解决方法是"升维"——引入齐次坐标。

7.4.2 齐次坐标的定义

齐次坐标的核心思想:给向量增加一个维度 w,把平移"伪装"成矩阵乘法。

齐次坐标——点w=1,向量w=0的几何意义
图7-3:齐次坐标——点(w=1)可被平移,向量(w=0)不受平移影响
类型2D表示3D表示原因
(x, y, 1)(x, y, z, 1)w=1表示这是一个位置,平移时应移动
向量(x, y, 0)(x, y, z, 0)w=0表示这是一个方向,平移不影响方向

为什么向量w=0?因为平移一个向量不应该改变它:

[1 0 tx] [x]   [x + tx·0]   [x]
[0 1 ty] [y] = [y + ty·0] = [y]   ← 向量不变!
[0 0  1] [0]   [   1    ]   [0]

注意 w 分量乘以平移量 tx 和 ty 后,因为 w=0,平移贡献被"清零"——向量不受平移影响。

7.4.3 齐次形式的变换矩阵

2D平移矩阵:

T(tx, ty) = [1 0 tx; 0 1 ty; 0 0 1]

3D平移矩阵:

T(tx, ty, tz) = [1 0 0 tx; 0 1 0 ty; 0 0 1 tz; 0 0 0 1]

3D缩放矩阵(齐次形式):

S(sx, sy, sz) = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1]
// 数值例子:平移(5, 3) 应用到点 (2, 1)
[1 0 5] [2]   [2 + 5]   [7]
[0 1 3] [1] = [1 + 3] = [4]
[0 0 1] [1]   [   1  ]   [1]  ← w=1保持不变

7.4.4 仿射变换的性质

所有仿射变换(线性变换+平移)都可以用齐次矩阵表示:

M_affine = [A t; 0 1]

其中A是3×3线性部分,t是3×1平移向量。

性质说明是否保持
保直性直线变换后仍是直线
保平行性平行线变换后仍是平行线
保距离两点之间距离不变否(缩放会改变距离)
保角度向量间夹角不变否(缩放和剪切会改变角度)

7.5 坐标变换:从局部到世界到相机

7.5.1 从局部到世界(模型变换)

物体在自己的局部坐标系中定义,通过模型矩阵放入世界坐标系:

p_world = M_model · p_local
// 例子:立方体局部坐标中的顶点 (1,1,1)
// 变换到世界坐标(缩放2倍 + 平移到(5,0,0))

M_model = [2  0  0 5]    p_local = [1]
          [0  2  0 0]              [1]
          [0  0  2 0]              [1]
          [0  0  0 1]              [1]

p_world = [2×1 + 5]   [7]
          [2×1 + 0] = [2]
          [2×1 + 0]   [2]
          [   1    ]   [1]

7.5.2 从世界到相机(视图变换)

视图变换将场景从世界坐标转换到以相机为原点的坐标:

p_camera = M_view · p_world
// 相机定义
camera_pos = (ex, ey, ez)      // 相机在世界的坐标
target     = (tx, ty, tz)      // 相机注视的位置
up         = (ux, uy, uz)      // 相机的"头顶"方向

// 构建相机坐标系基向量
w = normalize(camera_pos - target)          // -视线方向(z轴)
u = normalize(cross(up, w))                 // 右方向(x轴)
v = cross(w, u)                             // 上方向(y轴)

// 视图矩阵 = 旋转到相机坐标系 + 平移到原点
M_view = [ux  uy  uz  -dot(u, camera_pos)]
         [vx  vy  vz  -dot(v, camera_pos)]
         [wx  wy  wz  -dot(w, camera_pos)]
         [ 0   0   0       1           ]

7.6 三维变换矩阵的完整推导

7.6.1 点绕任意轴旋转(Rodrigues公式)

不是所有旋转都绕坐标轴。绕任意单位向量 a 旋转 θ 角的公式称为Rodrigues旋转公式

v_rot = v·cosθ + (a×v)·sinθ + a·(a·v)·(1-cosθ)

这个公式由三部分组成:

  • v·cosθ:v在垂直于旋转轴方向的分量,旋转后的"直接投影"
  • (a×v)·sinθ:垂直方向旋转产生的分量
  • a·(a·v)·(1-cosθ):v沿旋转轴的分量不变,这里补充了变化的部分

矩阵形式:

R(a, θ) = I·cosθ + [a]ₓ·sinθ + a·aᵀ·(1-cosθ)
// 其中 [a]ₓ 是a的叉积矩阵(反对称矩阵)
[a]ₓ = [  0  -az   ay]
       [  az   0  -ax]
       [ -ay   ax    0]

7.6.2 旋转矩阵的性质验证

性质公式含义
正交性Rᵀ·R = I旋转矩阵的逆 = 转置
行列式det(R) = +1旋转是保定向的(不包含镜像)
长度保持||Rv|| = ||v||旋转不改变向量长度
角度保持(Ru)·(Rv) = u·v旋转保持向量间夹角不变
// 验证2D旋转的正交性
R(θ) = [cosθ  -sinθ]
       [sinθ   cosθ]

Rᵀ·R = [cosθ  sinθ] [cosθ  -sinθ] = [cos²θ+sin²θ     0      ] = [1  0] = I  ✓
        [-sinθ cosθ] [sinθ   cosθ]   [   0      cos²θ+sin²θ]   [0  1]

7.6.3 欧拉角与万向锁

欧拉角用三个绕基本轴的旋转组合表示任意旋转:

R_euler = Rx(α) · Ry(β) · Rz(γ) (ZYX顺序,最常用)
旋转顺序应用场景备注
ZYX(偏航-俯仰-翻滚)飞行器/相机最常见的约定
XYZ一般物体欧拉角顺序不同,结果不同
ZXZ机器人避免万向锁问题
万向锁(Gimbal Lock):当第二次旋转为±90°时,第一次和第三次的旋转轴对齐,导致自由度从3丢失到2。这是欧拉角的固有问题,用矩阵组合或四元数可以避免。
🤔 想一想:为什么欧拉角有万向锁但矩阵组合没有?
欧拉角的三个旋转是"顺序依赖的级联"——先做A再做B再做C。当B=90°时,A和C的旋转轴重合了。矩阵组合直接使用Rodrigues公式表示单一旋转,不存在分解成三个基本旋转的问题——整个旋转就是一个矩阵乘法。

7.7 法线变换——初学者最容易犯的错误

⚠️ 核心警告:千万不能用模型矩阵直接变换法线!
法线变换——非均匀缩放导致法线错误
图7-4:法线变换错误——非均匀缩放后,直接使用模型矩阵变换的法线不再垂直于表面

7.7.1 错误示范

// ❌ 错误!这是初学者最常见的错误
incorrect_normal = model_matrix * vertex_normal

为什么错了?因为法线是方向,不是位置。更准确地说:法线是垂直于表面的方向,但顶点变换后,表面的朝向可能改变。特别是非均匀缩放时,直接用模型矩阵变换法线会得到错误的结果。

7.7.2 正确推导

假设模型矩阵 M 包含缩放和旋转:

  • 表面上两点构成切线 t,变换后 t' = M · t(因为t是方向向量,w=0不受平移影响)
  • 法线 n 满足 n · t = 0(法线垂直于表面,即垂直于切线)
  • 变换后的法线 n' 应满足 n' · t' = 0
n' · t' = 0
n' · (M·t) = 0                     // t' = M·t

假设 n' = X·n(存在某个矩阵X将法线变换到正确方向):
(X·n) · (M·t) = 0
(X·n)ᵀ · (M·t) = 0                  // 点积写成矩阵乘法
nᵀ · Xᵀ · M · t = 0

// 因为 nᵀ · t = 0(变换前法线垂直于切线),所以:
Xᵀ · M = I  →  X = (M⁻¹)ᵀ
正确法线变换 = 模型矩阵的逆转置矩阵
// ✅ 正确做法
correct_normal = normalize(transpose(inverse(model_matrix)) * vertex_normal)

// 简化形式(仅当M只有旋转和平移,没有非均匀缩放时)
// 旋转矩阵的逆 = 转置,所以逆转置 = 原始矩阵
correct_normal = model_matrix * vertex_normal  // 仅限纯旋转或均匀缩放!

7.7.3 经典例子

NVIDIA开发者文档中的经典例子:一个斜面(45°倾角),在x方向拉伸2倍。

原始法线 (归一化前):n = (1, 1, 0)
模型矩阵 M = S(2, 1, 1)  // 非均匀缩放

// ❌ 直接用M变换法线
n'_wrong = M·n = (2, 1, 0)  ← 不再垂直于新表面!

// ✅ 用逆转置变换法线
(M⁻¹)ᵀ = [1/2  0; 0  1]
n'_correct = (M⁻¹)ᵀ·n = (0.5, 1, 0) ← 正确垂直于新表面!

7.7.4 实践技巧

变换类型法线矩阵可否简化
纯旋转R= R(旋转矩阵逆=转置,所以逆转置=R)✓
旋转+平移R(3×3部分)= R(平移不影响方向)✓
均匀缩放+旋转S·R= (1/s)·R(归一化后可抵消缩放)✓
非均匀缩放+旋转(M⁻¹)ᵀ必须完全计算 ✗
// 在CPU上预计算法线矩阵
mat3 normal_matrix = transpose(inverse(mat3(model_matrix)));

// 在GPU着色器中
vec3 world_normal = normalize(normal_matrix * vertex_normal);

7.8 窗口变换(完整变换管线)

7.8.1 总览

从3D场景最终显示到屏幕窗口,经历了多步变换。这就是图形学中的"变换管线":

p_screen = M_viewport · M_projection · M_view · M_model · p_local
窗口变换管线——从局部坐标到屏幕像素
图7-5:窗口变换管线——局部坐标 → 世界坐标 → 相机视野 → NDC → 屏幕像素

7.8.2 各步骤详解

第一步:模型变换(局部 → 世界)

M_model = T(tx, ty, tz) · R(θ) · S(sx, sy, sz)

将物体的局部坐标变换到世界坐标。通常按"先缩放、再旋转、再平移"的顺序组合。

第二步:视图变换(世界 → 相机)

M_view = [u⃗, v⃗, w⃗, -e](将相机移到原点,视线对齐到-z轴)

第三步:投影变换(相机 → 标准化设备坐标NDC)

透视投影(将视锥体映射到 [-1, 1]³ 立方体):

M_perspective = 
[2n/(r-l)    0      (r+l)/(r-l)       0    ]
[   0      2n/(t-b)  (t+b)/(t-b)       0    ]
[   0         0      -(f+n)/(f-n)  -2fn/(f-n)]
[   0         0          -1           0    ]
符号含义示例值
l, r近平面左右边界-0.1, 0.1
b, t近平面上下边界-0.1, 0.1
n近裁剪面距离0.1
f远裁剪面距离100

为什么投影矩阵第4行是 (0, 0, -1, 0)?

因为透视需要"除以z"来完成近大远小。矩阵将z值存入w分量,后续透视除法时:w' = -z,远物(|z|大)→ w'大 → 屏幕坐标小(近大远小效果)。这就是透视投影的"魔法"。

正交投影(不产生透视效果):

M_orthographic =
[2/(r-l)    0        0     -(r+l)/(r-l)]
[   0     2/(t-b)     0     -(t+b)/(t-b)]
[   0        0     2/(n-f)  -(n+f)/(n-f)]
[   0        0        0          1      ]

第四步:视口变换(NDC → 屏幕坐标)

将 [-1, 1]³ 的NDC坐标映射到屏幕像素坐标:

M_viewport = 
[nx/2    0     0   (nx-1)/2]
[ 0   ny/2    0   (ny-1)/2]
[ 0     0    1/2     1/2   ]
[ 0     0     0       1     ]
符号含义示例值
nx屏幕宽度(像素)1920
ny屏幕高度(像素)1080

7.8.3 完整变换例子

点 (2, 3, 4) 在局部坐标系,经过各步变换到屏幕:

// 步骤0:局部坐标
p_local = (2, 3, 4, 1)ᵀ

// 步骤1:模型变换(平移(1,0,0) + 缩放(2,2,2))
M_model = [2 0 0 1; 0 2 0 0; 0 0 2 0; 0 0 0 1]
p_world = (5, 6, 8, 1)ᵀ

// 步骤2:视图变换(相机在(10,0,0),注视原点)
p_camera = (-5, 6, 8, 1)ᵀ

// 步骤3:透视投影(l=-0.1,r=0.1,b=-0.1,t=0.1,n=0.1,f=100)
p_clip = (... , ..., ..., 5)ᵀ  // w=-z ⇒ w=5
// 透视除法:p_ndc = p_clip / w
p_ndc = (.../5, .../5, .../5)ᵀ

// 步骤4:视口变换(1920×1080)
p_screen = (..., ..., ..., 1)ᵀ

7.9 矩阵组合与顺序

7.9.1 变换的顺序很重要

M_total = M_n · ... · M_2 · M_1

应用顺序:从M₁开始(最右边),到M_n结束(最左边)。先做的变换在右边,后做的在左边。

非常重要:矩阵乘法不交换!先旋转再平移 ≠ 先平移再旋转。
// 先旋转再平移:点绕原点旋转后再移动
M = T(5, 0) · R(90°)
  = [1 0 5] [0 -1 0]   [0 -1 5]
    [0 1 0] [1  0 0] = [1  0 0]
    [0 0 1] [0  0 1]   [0  0 1]

// 先平移再旋转:点平移后在新的位置绕原点旋转
M' = R(90°) · T(5, 0)
   = [0 -1 0] [1 0 5]   [0 -1 0]
     [1  0 0] [0 1 0] = [1  0 5]
     [0  0 1] [0 0 1]   [0  0 1]

// 两者结果不同!
// 先旋转再平移:点(0,0)→旋转后还在(0,0)→平移后到(5,0)
// 先平移再旋转:点(0,0)→平移后到(5,0)→旋转后到(0,5)

7.9.2 标准组合模式

// 模式1:围绕自身中心旋转(自转)
// 1. 移动到原点   2. 旋转    3. 移动回去
M = T(position) · R(θ) · T(-position)

// 模式2:围绕任意点 p 旋转(公转)
M = T(p) · R(θ) · T(-p)    // 公式和上面一样!

// 模式3:在指定方向缩放
// 1. 旋转对齐轴   2. 缩放      3. 旋转回去
M = R(-θ) · S(1, sy, 1) · R(θ)
🤔 想一想:为什么"围绕自身中心旋转"要先平移到原点?
旋转矩阵总是绕原点旋转。如果物体不在原点,直接旋转会使物体围绕原点做圆弧运动(公转)而不是自转。先平移到原点→旋转→再平移回去=自转。这是图形学中最常用的变换技巧之一。

7.10 变换矩阵总结

矩阵3D齐次形式逆矩阵
平移[I t; 0 1][I -t; 0 1]
缩放[S 0; 0 1][1/s 0; 0 1]
旋转[R 0; 0 1][Rᵀ 0; 0 1](正交矩阵逆=转置)
透视投影见7.8节复杂
核心洞察:变换矩阵是计算机图形学的"语法"——它用数学表达式统一描述了所有几何变换。从物体建模(局部坐标)到最终屏幕显示,每一步都是矩阵乘法。
变换阶段从 → 到矩阵作用
模型变换局部 → 世界M_model放置物体到场景中
视图变换世界 → 相机M_view从相机视角观察
投影变换相机 → NDCM_projection透视/正交投影(近大远小)
视口变换NDC → 屏幕M_viewport映射到像素坐标

一句话总结:变换 = 矩阵乘法组合,法线变换要用逆转置(非均匀缩放时必须),旋转矩阵来自极坐标→三角恒等式的推导,窗口变换从局部到屏幕经历四次矩阵乘法。

目录 X
章节 X