光栅化与深度测试:三角形怎么变成像素
顶点着色器交出的三角形,和片元着色器收到的像素之间,隔着一段「不写代码但决定大量现象」的固定流程:光栅化、深度测试、背面剔除、混合。透明排序、Z-fighting、阴影毛刺这些问题的答案都藏在这一段里。
光栅化:按像素中心判定覆盖
三角形是数学上的连续图形,屏幕是离散的像素格。光栅化器的规则简单到近乎粗暴:像素中心落在三角形内部,这个像素就归这个三角形(具体是顶边/右边的边界归属规则保证相邻三角形不重不漏)。这带来两个直接后果:
- 片元着色器收到的每个像素,其 UV、法线等数据是从三角形三个顶点插值来的,不是采样来的——插值规则见 /notes/unity/structure-2/。
- 斜边上「半个像素」的闪烁不是 bug,是中心判定的必然;MSAA 抗的就是这个。
深度缓冲与 Z 测试
每个像素在帧缓冲里除了颜色还有一份深度值。输出合并阶段比较:新片元的深度与已有深度,按 ZTest 配置(默认 LEqual,即更近者胜)决定保留谁。这就是不透明物体互相遮挡的全部机制——不需要任何排序,画序无关,靠深度决胜。
early-Z:现代 GPU 会在片元着色器执行之前先做深度测试,输了直接丢弃、不跑片元程序——省掉的可能就是几百万次执行。但两个东西会破坏它:
- 片元里
discard(alpha 裁剪):GPU 还没算出颜色就无法丢弃,early-Z 失效或退化。 ZWrite Off或ZTest Always:深度信息不可靠,硬件保守处理。
移动端上这条省得极多,相关取舍见 /notes/unity/performance-1/。
背面剔除:三角形有正反
三角形顶点有绕序(Unity 默认顺时针为正面)。光栅化前可按 Cull Back(默认,剔背面)/ Cull Front / Cull Off 处理。默认剔背面直接省掉约一半的片元执行。Cull Front 不是摆设——描边的「倒模法」就是故意只画背面,见 /notes/unity/effects-3/。
混合:透明为什么必须排序
不透明物体 Blend One Zero:新颜色直接覆盖旧颜色,画序无所谓。
透明物体典型配置 Blend SrcAlpha OneMinusSrcAlpha:新颜色和帧缓冲里已有的颜色按 alpha 加权叠加。
「已有的颜色」取决于之前画了什么——所以透明队列必须从远到近画,且通常 ZWrite Off(透明像素写深度会把后面的透明物体整块裁掉)。即便如此仍有排序毛刺:Unity 按物体包围盒中心排透明 Draw Call,两个互相穿插的粒子系统怎么排都会错——这类毛刺的成因与缓解见 /notes/unity/performance-3/。
深度测试对透明也不是全没用:透明物体仍要读深度来被不透明物体正确遮挡,只是不写深度。
Z-fighting:深度精度的账
两个几乎共面的表面(贴花与地面、两个叠在一起的 UI 面)互相闪烁,是深度值差异小于深度缓冲精度。深度被压进 [0,1] 后近处精度高、远处精度指数级下降,所以:
- 摄像机 Near 别设太小(0.3 比 0.01 给远处多出大量精度),Far 别设太大。
- 贴花/贴面材质用 Polygon Offset 或深度偏置。
具体数值影响见 /notes/unity/performance-3/。
自检三问
- 为什么
discard用得多的材质在手机上明显更贵? - 一个
ZWrite Off, ZTest LEqual的透明物体,会被先画的不透明物体挡住吗?会被后画的透明物体挡住吗? - 远处两个共面贴图闪烁,先检查摄像机哪个参数?