经过 【空间变换】观察变换【空间变换】观察变换经过 [[【空间变换】基本变换]] 的模型变换,物体已经摆放在世界空间里。接下来要换个视角看世界——把相机放到合适的位置和角度,这一步叫观察变换(View Transformation),也叫视图变换。
打个拍照的比方:先把要拍的东西摆放好,这是模型变换;再挑一个角度架好相机,这是观察变换;最后按下快门把三维场景拍成二维照片,那是后面的 [[【空间变换】投影变换]]。
观察空间
观察空间(View Space)就是以相机原点为坐标原点建立起来的坐标系。它的三个基向量在世界空间里的取值,描述的正是相机的朝向。
按照惯例,观察空间是右手坐标系,相机看向 $-Z$ 方向。也就是说,相机的视线方向对应的是观察空间的 $-Z$ 轴。如果记相机位置为 $ViewPos$、注视目标为 $ViewTarget$,那么视线方向(gaze)指向目标,而观察空间的 $+Z$ 轴与视线相反:
$$Z_{view} = -(ViewTarget - ViewPos) = ViewPos - ViewTarget$$
要唯一确定一个相机,需要三个量:
1. 相机的位置。
2. 相机看向的方向(gaz,顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。
先把结论列在前面,推完再回看会更清楚:
- 把顶点变换到裁剪空间的目的,是方便做裁剪。
- 正交相机的裁剪空间,就是一个标准立方体(CVV)。
- 推导透视投影时,我们会乘上一个观察空间的 z 值(zview),这个 z 最终成为裁剪空间的 w 分量。
- 透视相机的裁剪,是判断坐标 (x,y,z) 是否落在 (−w,w) 之内。
裁剪空间
顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪:
- 完全落在这块空间内部的图元,保留;
- 完全落在外部的,剔除;
- 与边界相交的,裁剪掉超出的部分。
这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定了相机能看到的范围。视锥体分两类,对应两种投影:
- 正交投影(Orthographic Projection)
- 透视投影(Perspective Projection)
正交投影
正交投影下,相机在原点看向 −Z、顶部朝 +Y。它的做法是把视锥体(一个长方体)里的物体,平移并缩放到 [−1,1]3 的标准立方体中。分两步:
- 先把长方体中心平移到原点;
- 再缩放到 [−1,1]3 的标准立方体。

Mortho=r−l20000t−b20000n−f200001100001000010−2r+l−2t+b−2n+f1
右边是平移矩阵,左边是缩放矩阵。
平移量取的是每根轴的中心坐标,取负号是因为把任意点挪回原点都是负向平移。缩放量则是用标准长度 2(即 [−1,1] 的跨度)去除以物体在每根轴上的实际长度。要注意由于相机看向 −Z,近平面 n 比远平面 f 更大。
更细致的缩放推导、以及 OpenGL 与 DirectX 平台在 z 值范围上的差异,见 正交投影矩阵推导过程正交投影矩阵推导过程这篇是 [[【空间变换】投影变换]] 里正交投影缩放矩阵的详细推导。核心问题很简单:观察空间里一段区间 $[l, r]$,要线性映射到裁剪空间的 $[-1, 1]$,这个缩放系数怎么来。$x$、$y$、$z$ 三个方向各推一遍,其中 $z$ 还要区分 OpenGL 和 DirectX 的平台差异。
缩放矩阵的目标
image.png
要找一个矩阵,把观察空间坐标 $(x_{view}, y_{view}, z_{view}, 1)$ 变换到裁剪空间 $(x_p, y_p, z_p, 1)$:
$$
\begin{bmatrix}
? & ? & ? & ? \\
? & ? & ? & ? \\
? & ? & ? & ? \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix} x_{view} \\ y_{view} \\ z_{view} \\ 1 \end{bmatrix}
= (x_p,\ y_p,\ z_p,\ 1)
$$
已知 $x$ 在相机空间的范围是 $(l, r)$,变换后要落到裁剪空间的 $(-1, 1)$。下面用一串。
透视投影
透视投影要还原"近大远小",它的视锥体是一个上宽下窄的锥台。核心技巧是分两步:
- 先把视锥体"挤"成一个长方体;
- 再对这个长方体做上面的正交投影。

在"挤压"之前,先约定三条不变量,它们是后面解方程的依据:
- 近平面上所有点的坐标不变;
- 远平面上所有点的 z 值不变,都是 f;
- 远平面中心点的坐标不变,为 (0,0,f)。
推导挤压矩阵

取视锥体内任意一点 (x,y,z),挤压后它的 xy 坐标应当与它在近平面上的投影点重合。由相似三角形可得:
y′=zny,x′=znx
所以点 (x,y,z,1) 挤压后的坐标是 (znx, zny, ?, 1),z 分量暂时未知:
xviewyviewzview1⇒nx/zny/zunknown1
现在要找一个矩阵 Mpersp→ortho,让上面这个变换成立。
难点在哪: 假设矩阵第一行是 A,B,C,D,那么要满足 Ax+By+Cz+D=nx/z。这个等式很难凑——若让 A=n/z、其余为 0 固然成立,但矩阵元素必须是常数,n/z 却是个变量。第二行 Ex+Fy+Gz+H=ny/z 也是同样的麻烦。
破解办法,用的正是 【空间变换】齐次坐标【空间变换】齐次坐标
空间变换这一系列笔记,讲的是一个顶点从建模软件里的局部坐标,一路变换到屏幕像素的完整过程,也就是渲染管线里常说的 MVP 变换加上后续的屏幕映射。而这一切的数学地基,是齐次坐标。所以先从它讲起。
为什么需要齐次坐标
顶点变换无非三种:平移、旋转、缩放。旋转和缩放都能写成一个矩阵乘以坐标:
$$P' = M \cdot P$$
但平移不行。在三维笛卡尔坐标下,平移是把每个分量各加一个偏移量:
$$(x, y, z) \rightarrow (x + t_x,\ y + t_y,\ z + t_z)$$
这是一次加法,没法用一个 $3\times3$ 矩阵乘出来。于是就出现了一个别扭的局面:旋转缩放是乘法,平移是加法,两类操作凑不到一起。如果一个顶点既要旋转又要平移,就得写成 $P' = M \cdot P + t$,既有乘又有加。当变换一多,这种混合写法会变得非常难组合。
齐次坐标就是为了解决这件事:给坐标加上第四个分量 $w$,把平移也塞进矩阵里,让平移、旋转、缩放全都变成统一的矩阵乘法。 一旦所有变换都是矩阵,它们就能通过矩阵连乘预先合并成一个矩阵 里那条性质:(x,y,z,1) 与 (kx,ky,kz,k)(k=0)表示同一个点。于是让 k=zview,把目标坐标 (nx/z, ny/z, unknown, 1) 整体乘以 z,变成 (nx, ny, still unknown, z)——分母上的 z 就被消掉了:
Mpersp→ortho(4×4)xviewyviewzview1=nxny?zviewzview
对照左右两边,矩阵的第一、二、四行已经可以填出来,只剩第三行未知:
Mpersp→ortho=n0?00n?000?100?0
即:
n0?00n?000?100?0xviewyviewzview1=nxny?zviewzview
求第三行
先用约定近平面上所有点坐标不变。近平面上的点形如 (x,y,n,1),代入应得回自身:
n0?00n?000?100?0xyn1=xyn1
看第一、二、四行:
⎩⎨⎧nx=xny=yn=1
这显然有问题,只有当 n=1 时才成立,而 n 应是任意常数。解决办法还是老套路——把等号右边的 (x,y,n,1) 整体乘上 n,等价地写成 (nx,ny,n2,n):
n0?00n?000?100?0xyn1=nxnyn2n
现在第一、二、四行都对上了:
⎩⎨⎧nx=nxny=nyn=n
设第三行为 A,B,C,D,则由第三行得到 Ax+By+Cn+D=n2。由于结果 n2 与 x,y 无关,可知 A=0、B=0,剩下 Cn+D=n2。
再用约定远平面中心点 (0,0,f) 不变。这个点齐次化为 (0,0,f,1),同样整体乘 f,第三行应得 f2:
n0000n0000C100D000f1=00f2f
联立近、远平面两个方程:
{Cn+D=n2Cf+D=f2⟹{C=n+fD=−nf
于是挤压矩阵 Mpersp→ortho 为:
Mpersp→ortho=n0000n0000n+f100−nf0
它把透视投影的锥台压成了正交投影的长方体。
合并
把压好的长方体再乘上正交投影矩阵,就得到完整的透视投影矩阵:
Mpersp=r−l20000t−b20000n−f200001100001000010−2r+l−2t+b−2n+f1n0000n0000n+f100−nf0
经过投影矩阵后,顶点落在裁剪空间里,w 分量保留着观察空间的 z 信息。接下来交给硬件做透视除法和视口变换,见 【空间变换】屏幕映射(Screen Mapping)【空间变换】屏幕映射(Screen Mapping)经过 [[【空间变换】投影变换]],顶点已经在裁剪空间里了。屏幕映射是这条管线的最后一站,负责把裁剪空间的顶点落到屏幕上具体的像素位置。它包含两步:
- 透视除法(Perspective Division)
- 视口变换(Viewport Transform)
透视除法
image.png
上图是顶点数据的完整变换流程。局部空间的顶点,依次经过模型矩阵 Model、观察矩阵 View、投影矩阵 Projection,来到裁剪空间,此时满足 $-w_{clip} \leq x, y, z \leq w_{clip}$。接着做透视除法——每个分量都除以 $w$ 分量——就得到了标准设备坐标(NDC),它所在的空间也叫标准视体(Canonical View Volume,CVV)。
为什么这一步能实现近大远小? 关键在 $w$。回顾 [[【空间变换】投影变换]] 的推导,投影矩阵刻意把观察空间的 $z$ 坐标塞进了裁剪空间的 $w$ 分量。于是透视除法(除以 $w$,等于除以 $z$)就让离相机越远、$z$ 越大的顶点,坐标被压缩得越狠——这正是透视效果的来源。这也回扣了 [[【空。
参考