光栅化器在做的事
一句话总结
光栅化器接收三个顶点,挑出落在其中的像素,再用重心坐标把顶点上的属性值混合后逐像素传递下去。前后关系由深度缓冲按像素决定。
为什么需要它
做完前面的步骤,顶点就有了屏幕坐标。但屏幕是像素网格,三角形的边并不恰好对齐网格。需要一条规则来决定某个像素算不算这个三角形的。
此外,颜色只存在于顶点上。三角形内部像素的颜色从哪里来?需要按距离把三个顶点的值混合,而把“按距离”精确定义出来的,就是重心坐标。
最后,如果有多个物体,还要决定哪个在前。按三角形排序并从后往前绘制的方法(画家算法),在三角形互相穿透或循环重叠时无解。所以硬件选择了让每个像素记住自己的深度。
工作原理
重心坐标是把点 P 写成三个顶点的加权和 P = u*A + v*B + w*C 时得到的 (u, v, w)。三个值之和始终为 1,三个值都不小于 0 时,P 位于三角形内部。这同时解决了内外判断和属性值插值。
d = (By-Cy)(Ax-Cx) + (Cx-Bx)(Ay-Cy) # 삼각형 넓이의 두 배(부호 있음)
u = ((By-Cy)(Px-Cx) + (Cx-Bx)(Py-Cy)) / d
v = ((Cy-Ay)(Px-Cx) + (Ax-Cx)(Py-Cy)) / d
w = 1 - u - v
实际实现只遍历三角形的包围盒。与遍历整个屏幕相比要快得多,GPU 也是按图块做同样的事。
不仅是颜色,深度也用同样的方式插值。求出像素的深度后,与该位置深度缓冲中的值比较,只有更近时才同时写入颜色和深度。这就是 z-buffer,无论绘制顺序如何,它都能得到相同的图像。顺序无关才是这种方法真正的价值——只要不是半透明物体,就无需排序。
背面剔除(back-face culling)在更早的阶段就把它们筛掉。三个顶点在屏幕坐标中的环绕方向是顺时针还是逆时针,可以通过有符号面积得知。
signed_area = ((Bx-Ax)(Cy-Ay) - (Cx-Ax)(By-Ay)) / 2
对于封闭物体,背面反正会被正面遮住,所以仅凭这个符号,就能在光栅化之前丢掉一半的三角形。这是白捡的两倍速度。
插值还有一个陷阱。如果直接用屏幕坐标中求出的重心坐标去混合值,在带有透视的场景里会得到错误的结果。因为屏幕上均匀划分的间隔,在三维空间中并不均匀。颜色不太显眼,但纹理一定会扭曲。正确的做法是先把值除以 w 再插值,最后还原,这称为透视校正插值。在着色器课程中,你会亲眼看到这种扭曲。
还有一点值得了解:GPU 不是逐个处理像素,而是以 2x2 为一组处理。片段着色器要选择纹理的细节层级,就需要知道与相邻像素的差值,而这个差值就在组内求取。因此,当三角形非常细长时,一组中有一半以上的计算会被丢弃,三角形切得越碎,这种浪费就越大。
在现场相遇的样子
有一种缺陷是在相邻的两个三角形之间看到一条细缝。如果两侧都判定共享边上的像素不属于自己,就会出现这种情况。反过来,如果两侧都判定属于自己,那么这个像素会被绘制两次,在半透明合成时颜色会变深。所以规范规定了填充规则(top-left rule),让边界像素只有一个归属。
另一个是 z-fighting。如果两个面的深度几乎相同,前后关系就会在深度缓冲的精度范围内来回摆动,画面闪烁不定。解决办法是把两个面实际分开,或者加深度偏移,或者增大 near 平面。
此外,背面剔除只对封闭物体是安全的。对于纸张或树叶这类需要从两面观看的东西,如果开启它,在某些角度整个物体会消失。
最后补充一点关于性能的直觉。光栅化的开销近似与填充的像素数成正比,而不是三角形的个数。一个铺满屏幕的大三角形,比屏幕之外的上千个小三角形更贵。因此优化方向分为两路:一是提前丢掉不会出现在屏幕上的内容(视锥体剔除、背面剔除、遮挡判定),二是不要对同一个像素重复着色。后者的问题称为过度绘制(overdraw),按从前到后的顺序绘制不透明物体时,深度判定会先把它们过滤掉,片段计算也就随之减少。也就是说,深度缓冲虽然让排序不再必要,但出于性能考虑,大致排序仍然有益。
下一项实验要做什么
亲手构造重心坐标,用它填充三角形,并插值顶点颜色。接着给两个相互重叠的三角形指定深度,用 z-buffer 区分前后,再只对调深度,用眼睛看看图像有什么变化。最后用有符号面积挑出背面并丢弃。