顶点的一生:模型→世界→观察→裁剪→屏幕
顶点着色器最核心的工作是把顶点从一个坐标系搬到另一个坐标系。多数「模型跑出屏幕」「贴图拉伸」「阴影错位」的 bug,根源都是某一步变换用错了空间。这篇把空间链一次讲清,包括透视除法到底除了什么。
五个空间,一条链
一个顶点在渲染过程中依次住在五个空间里:
| 空间 | 原点/朝向 | 谁负责搬 |
|---|---|---|
| 模型空间(object) | 模型自己的轴心 | 建模时决定 |
| 世界空间(world) | 场景原点 | Model 矩阵(Transform 的位置旋转缩放) |
| 观察空间(view) | 摄像机在原点,看向 -Z(Unity) | View 矩阵(摄像机变换的逆) |
| 裁剪空间(clip) | 硬件定义的截头锥体 | Projection 矩阵 |
| 屏幕空间(screen/NDC→像素) | 像素坐标 | 透视除法 + 视口映射(硬件自动) |
顶点着色器里最常见的一行就是这条链的压缩版:
o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // 老写法
// URP 里等价于:
o.positionCS = TransformObjectToHClip(v.positionOS);
MVP 是 Projection × View × Model 三个矩阵预先相乘的结果(矩阵乘法从右往左作用:先 Model、再 View、再 Projection)。三个矩阵各管一件事:
- Model:这个模型摆在场景哪里。所以模型空间的位置在
v.vertex里,世界空间位置要mul(unity_ObjectToWorld, v.vertex)才拿到。 - View:把「摄像机看世界」换算成「世界相对摄像机」。旋转摄像机不改 View 之外的任何东西。
- Projection:把观察空间的视锥体「挤」成一个立方体(裁剪空间),为硬件的统一裁剪判定做准备。
为什么要一路搬到裁剪空间
因为裁剪和光栅化是硬件固定功能,它只认裁剪空间。裁剪空间里判定「在不在视野内」退化成了简单的坐标范围比较(-w 到 w),任何形状的近裁剪面/远裁剪面/视野边界都被矩阵统一成了这个范围。GPU 不为每个引擎定制裁剪逻辑,引擎自己乘好矩阵喂给它。
透视除法:w 是什么,除了 w 得到什么
Projection 矩阵故意把观察空间的深度信息写进了输出顶点的 w 分量(对透视投影,w = -z_view,即顶点到摄像机的距离)。硬件随后自动做一次透视除法:x,y,z 全部除以 w,得到 NDC(各分量约在 [-1, 1])。
这一步是「近大远小」的数学来源:离摄像机越远,w 越大,除完之后横向坐标被压得越靠近屏幕中心。也正因如此:
- 裁剪空间坐标不能提前手动除以 w——硬件会做,你做两次就错了。
- 想在 shader 里拿「离摄像机的距离」,读裁剪空间位置的
w比自己算距离便宜(computeScreenPos等屏幕空间效果都靠这条链,见 /notes/unity/effects-4/)。 - 深度值也在这一步被压进 [0,1](或平台相关范围),为深度测试做准备——见 /notes/unity/principles-3/。
Unity 里的命名习惯
读别人的 shader 时注意后缀约定:positionOS(object space)、positionWS、positionVS、positionCS(clip space);老 CG 代码里 v.vertex 是模型空间、o.pos 是裁剪空间。语义 POSITION 绑的是模型空间位置(SV_POSITION 绑的才是裁剪空间输出)——语义与结构见 /notes/unity/structure-2/。
常见事故与定位
- 模型整体消失:多半是 MVP 少乘/反序乘,顶点全被裁掉。Frame Debugger 里看该 Draw Call 的输出顶点坐标即可确认。
- 法线光照错误但位置正确:法线不能直接乘 MVP——法线的变换规则不同,见 /notes/unity/principles-5/。
- 贴图在斜面上拉伸/抖动:不是变换错,是插值与屏幕空间导数的性质,见 /notes/unity/structure-2/ 与 /notes/unity/performance-1/。
- 顶点动画方向不对:位移加在模型空间还是世界空间,结果完全不同(旋转物体上尤其明显)——套路见 /notes/unity/effects-5/。
自检三问
- 摄像机绕场景旋转时,哪些矩阵变了,哪些没变?
- 透视除法发生在 shader 里还是硬件里?
w里装的是什么? - 想在片元里拿「该像素离摄像机多远」,最便宜的来源是什么?