【空间变换】屏幕映射(Screen Mapping)
目录+
经过 【空间变换】投影变换【空间变换】投影变换经过 [[【空间变换】观察变换]],顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。 先把结论列在前面,推完再回看会更清楚: - 把顶点变换到裁剪空间的目的,是方便做裁剪。 - 正交相机的裁剪空间,就是一个标准立方体(CVV)。 - 推导透视投影时,我们会乘上一个观察空间的 $z$ 值($z_{view}$),这个 $z$ 最终成为裁剪空间的 $w$ 分量。 - 透视相机的裁剪,是判断坐标 $(x, y, z)$ 是否落在 $(-w, w)$ 之内。 裁剪空间 顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪: - 完全落在这块空间内部的图元,保留; - 完全落在外部的,剔除; - 与边界相交的,裁剪掉超出的部分。 这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定,顶点已经在裁剪空间里了。屏幕映射是这条管线的最后一站,负责把裁剪空间的顶点落到屏幕上具体的像素位置。它包含两步:
- 透视除法(Perspective Division)
- 视口变换(Viewport Transform)
透视除法

上图是顶点数据的完整变换流程。局部空间的顶点,依次经过模型矩阵 Model、观察矩阵 View、投影矩阵 Projection,来到裁剪空间,此时满足 。接着做透视除法——每个分量都除以 分量——就得到了标准设备坐标(NDC),它所在的空间也叫标准视体(Canonical View Volume,CVV)。
为什么这一步能实现近大远小? 关键在 。回顾 【空间变换】投影变换【空间变换】投影变换经过 [[【空间变换】观察变换]],顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。 先把结论列在前面,推完再回看会更清楚: - 把顶点变换到裁剪空间的目的,是方便做裁剪。 - 正交相机的裁剪空间,就是一个标准立方体(CVV)。 - 推导透视投影时,我们会乘上一个观察空间的 $z$ 值($z_{view}$),这个 $z$ 最终成为裁剪空间的 $w$ 分量。 - 透视相机的裁剪,是判断坐标 $(x, y, z)$ 是否落在 $(-w, w)$ 之内。 裁剪空间 顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪: - 完全落在这块空间内部的图元,保留; - 完全落在外部的,剔除; - 与边界相交的,裁剪掉超出的部分。 这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定 的推导,投影矩阵刻意把观察空间的 坐标塞进了裁剪空间的 分量。于是透视除法(除以 ,等于除以 )就让离相机越远、 越大的顶点,坐标被压缩得越狠——这正是透视效果的来源。这也回扣了 【空间变换】齐次坐标【空间变换】齐次坐标 空间变换这一系列笔记,讲的是一个顶点从建模软件里的局部坐标,一路变换到屏幕像素的完整过程,也就是渲染管线里常说的 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$,把平移也塞进矩阵里,让平移、旋转、缩放全都变成统一的矩阵乘法。 一旦所有变换都是矩阵,它们就能通过矩阵连乘预先合并成一个矩阵 里"除以 还原坐标"那条性质:使用齐次坐标的深层意义,就是为了在投影前把物体的深度信息( 值)妥善保存到 里,好在这一步正确还原透视。
还有一点值得留意:透视除法由硬件自动执行,正交投影和透视投影都会走这一步。只不过正交投影没有改动 分量(仍然是 ),除以 相当于什么都没做,所以正交下这步没有实际效果。
视口变换

透视除法之后得到的是 NDC 坐标。之所以要有 NDC 这个中间产物,是为了让屏幕坐标的转换与硬件无关——不管目标屏幕多大,先统一到 的标准范围,再由视口变换映射到实际窗口。
视口变换后得到的是窗口坐标(Window Coordinates)。这里要区分两个概念:
- 屏幕坐标(Screen Coordinates) 是 2D 的,只表示屏幕上的 XY 位置;
- 窗口坐标(Window Coordinates) 是 2.5D 的,除了 XY 还带有变换后的 Z 深度信息。
NDC 映射到窗口坐标,只需一次平移加一次缩放。深度 Z 的范围同样有平台差异:OpenGL 映射到 ,DirectX 映射到 ,可以用 glDepthRange() 来设定深度映射。视口本身则用 glViewport() 设定坐标和宽高。
最后一个容易忽略的点:屏幕空间和视口(Viewport)不是一回事。 视口可以只占屏幕的一部分。如果视口小于屏幕空间,就可能有多余的像素被处理——比如 glClear() 会对整个屏幕填色。要避免这种浪费,可以用裁切测试(Scissor Test) 限定实际渲染的区域。
至此,一个顶点从建模软件里的局部坐标,历经模型、观察、投影、透视除法、视口变换,终于落到了屏幕上的像素位置。整条空间变换管线到这里就走完了。