这篇是 【空间变换】投影变换【空间变换】投影变换经过 [[【空间变换】观察变换]],顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。
先把结论列在前面,推完再回看会更清楚:
- 把顶点变换到裁剪空间的目的,是方便做裁剪。
- 正交相机的裁剪空间,就是一个标准立方体(CVV)。
- 推导透视投影时,我们会乘上一个观察空间的 $z$ 值($z_{view}$),这个 $z$ 最终成为裁剪空间的 $w$ 分量。
- 透视相机的裁剪,是判断坐标 $(x, y, z)$ 是否落在 $(-w, w)$ 之内。
裁剪空间
顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪:
- 完全落在这块空间内部的图元,保留;
- 完全落在外部的,剔除;
- 与边界相交的,裁剪掉超出的部分。
这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定 里正交投影缩放矩阵的详细推导。核心问题很简单:观察空间里一段区间 [l,r],要线性映射到裁剪空间的 [−1,1],这个缩放系数怎么来。x、y、z 三个方向各推一遍,其中 z 还要区分 OpenGL 和 DirectX 的平台差异。
缩放矩阵的目标

要找一个矩阵,把观察空间坐标 (xview,yview,zview,1) 变换到裁剪空间 (xp,yp,zp,1):
???0???0???0???1xviewyviewzview1=(xp, yp, zp, 1)
已知 x 在相机空间的范围是 (l,r),变换后要落到裁剪空间的 (−1,1)。下面用一串不等式变形把系数逼出来。
x 方向推导
从 l<x<r 出发,目标是凑出 −1<(⋯)<1 的形式:
l<x<r0<x−l<r−l0<(x−l)⋅r−l2<20<r−l2x−2l<2−1<r−l2x−2l−1<1−1<r−l2x−2l−r−lr−l<1−1<r−l2x−l−r<1
在对称视锥体(r=−l,即 l+r=0)下,分子的 −l−r 项消去:
−1<r−l2x<1⟹−1<x⋅r−l2<1
于是 x 方向的缩放系数就是 r−l2。
y 方向推导
和 x 完全一样,把 r 换成 t、l 换成 b,得到系数 t−b2。
z 方向推导
z 要特别小心两点:相机空间是右手坐标系、看向 −z;而不同图形 API 对裁剪空间 z 的目标范围约定不同:
- OpenGL:(−1,1)
- DirectX:(0,1),若反转 z 值则为 (1,0)
OpenGL:映射到 (-1, 1)
−n≤z≤−f0≤z+n≤n−f0≤(z+n)⋅n−f2≤20≤n−f2z+2n≤2−1≤n−f2z+2n−n−fn−f≤1−1≤n−f2z+2n−n+f≤1−1≤n−f2z+n+f≤1−1≤n−f2z+n−fn+f≤1
DirectX:反转 z 值,映射到 (1, 0)
−n≤z≤−ff−n≤z+f≤01≤(z+f)⋅f−n1≤01≤f−nz+f≤01≤f−nz+f−nf≤0
DirectX:不反转 z 值,映射到 (0, 1)
−n≤z≤−f0≤z+n≤n−f0≤(z+n)⋅n−f1≤10≤n−fz+n≤10≤n−fz+n−fn≤1
三个平台的缩放矩阵
把上面各方向的系数填回矩阵,就得到三种约定下的正交缩放矩阵。
OpenGL:
r−l20000t−b20000n−f2000n−fn+f1
DirectX,反转 z 值:
r−l20000t−b20000f−n1000f−nf1
DirectX,不反转 z 值:
r−l20000t−b20000n−f1000n−fn1
z 那一行的常数项(第四列),正是上面不等式变形里那个与 z 无关的偏移量——它把缩放后的区间平移到目标范围的正确位置。