Deep Read

【空间变换】齐次坐标

 

  空间变换这一系列笔记,讲的是一个顶点从建模软件里的局部坐标,一路变换到屏幕像素的完整过程,也就是渲染管线里常说的 MVP 变换加上后续的屏幕映射。而这一切的数学地基,是齐次坐标。所以先从它讲起。

为什么需要齐次坐标

顶点变换无非三种:平移、旋转、缩放。旋转和缩放都能写成一个矩阵乘以坐标:

P=MPP' = M \cdot P

但平移不行。在三维笛卡尔坐标下,平移是把每个分量各加一个偏移量:

(x,y,z)(x+tx, y+ty, z+tz)(x, y, z) \rightarrow (x + t_x,\ y + t_y,\ z + t_z)

这是一次加法,没法用一个 3×33\times3 矩阵乘出来。于是就出现了一个别扭的局面:旋转缩放是乘法,平移是加法,两类操作凑不到一起。如果一个顶点既要旋转又要平移,就得写成 P=MP+tP' = M \cdot P + t,既有乘又有加。当变换一多,这种混合写法会变得非常难组合。

齐次坐标就是为了解决这件事:给坐标加上第四个分量 ww,把平移也塞进矩阵里,让平移、旋转、缩放全都变成统一的矩阵乘法。 一旦所有变换都是矩阵,它们就能通过矩阵连乘预先合并成一个矩阵,对每个顶点只做一次乘法即可。这正是 GPU 处理海量顶点时需要的形式。

具体做法,是把三维点写成四维:

(x,y,z)(x,y,z,1)(x, y, z) \rightarrow (x, y, z, 1)

再看平移矩阵:

[100tx010ty001tz0001][xyz1]=[x+txy+tyz+tz1]\begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ z + t_z \\ 1 \end{bmatrix}

多出来的第四列,恰好把偏移量加了进去,平移就此变成了一次矩阵乘法。

w 分量:区分点和向量

引入 ww 之后,还带来一个额外的好处——它能区分一个坐标到底是点还是向量。

image.png

看上面这张图,A=[21]A = \begin{bmatrix} 2 \\ 1 \end{bmatrix} 这个坐标,既可以理解成空间中的一个(位置),也可以理解成一个向量(方向和长度)。但两者的运算规则并不一样:两个向量相加是有意义的(合成),两个点相加却没有意义。光看坐标分不清是哪种,ww 正好用来标记:

A=[xyzw]A = \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix}

  • w=1w = 1:表示一个
  • w=0w = 0:表示一个向量

  这个约定和平移矩阵是自洽的。向量代表纯粹的方向,平移它没有意义(把一个方向"挪个位置",它还是那个方向)。而 w=0w = 0 时,平移矩阵第四列乘进去的偏移量全部被 00 消掉,向量自动免疫平移;点的 w=1w = 1,则会正常接受平移。一个 ww 分量,就让矩阵对点和向量做出了正确的区别对待。

由此也能验证几条运算的合理性,只要盯着 ww 看:

  • 点 − 点 = 向量(11=01 - 1 = 0),两点相减得到从一点指向另一点的方向,符合直觉。
  • 点 + 向量 = 点(1+0=11 + 0 = 1),沿某方向移动一个点,结果还是点。
  • 向量 + 向量 = 向量(0+0=00 + 0 = 0)。
  • 点 + 点:w=2w = 2,落在"既非点也非向量"的状态,这也解释了为什么两点直接相加没有意义。

缩放不变性与透视除法的伏笔

齐次坐标还有一条至关重要的性质:一个 w0w \neq 0 的齐次坐标,整体乘以任意非零常数 kk,表示的是同一个点。

[x,y,z,w]=[kx,ky,kz,kw](k0)[x, y, z, w] = [kx, ky, kz, kw] \quad (k \neq 0)

要从齐次坐标还原回真正的三维坐标,只需让每个分量都除以 ww

[x,y,z,w][xw, yw, zw][x, y, z, w] \rightarrow \left[\frac{x}{w},\ \frac{y}{w},\ \frac{z}{w}\right]

举个例子:

[1,2,3,1]=[2,4,6,2]=[1,2,3][1, 2, 3, 1] = [2, 4, 6, 2] = [1, 2, 3]

后两者除以各自的 ww,都回到同一个点 [1,2,3][1, 2, 3]

  这条"乘一个非零常数不改变所代表的点"的性质,眼下看只是个数学细节,但它是后面推导透视投影的关键钥匙。透视投影里那个难求的矩阵,正是靠着"允许整体乘上一个变量 zz"这一步才凑出来的;而屏幕映射前的透视除法(除以 ww),本质就是在这里做的"除以 ww 还原坐标"。可以说,近大远小的透视效果,最终都落在这一个除法上。这些留到后面的 【空间变换】投影变换【空间变换】投影变换经过 [[【空间变换】观察变换]],顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。 先把结论列在前面,推完再回看会更清楚: - 把顶点变换到裁剪空间的目的,是方便做裁剪。 - 正交相机的裁剪空间,就是一个标准立方体(CVV)。 - 推导透视投影时,我们会乘上一个观察空间的 $z$ 值($z_{view}$),这个 $z$ 最终成为裁剪空间的 $w$ 分量。 - 透视相机的裁剪,是判断坐标 $(x, y, z)$ 是否落在 $(-w, w)$ 之内。 裁剪空间 顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪: - 完全落在这块空间内部的图元,保留; - 完全落在外部的,剔除; - 与边界相交的,裁剪掉超出的部分。 这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定【空间变换】屏幕映射(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$ 越大的顶点,坐标被压缩得越狠——这正是透视效果的来源。这也回扣了 [[【空 再详细展开。