Skip to content

光栅化算法

光栅化是将连续的几何图元转换为离散像素的过程,是渲染管线的核心步骤。在此之前,顶点经过模型、视图、投影变换,从三维空间投影到了二维屏幕上。光栅化的任务是确定哪些像素被三角形覆盖,并为这些像素生成片段(Fragment),后续的片段着色器将为每个片段计算最终颜色。

投影变换

顶点从模型空间到屏幕空间的完整变换路径:模型空间 → 世界空间 → 观察空间 → 裁剪空间 → 规范化设备坐标(NDC) → 屏幕空间。

  • 模型变换:将顶点从局部坐标系变换到世界坐标系,处理物体的位置、旋转、缩放
  • 视图变换:将顶点从世界坐标系变换到相机坐标系,模拟相机的观察方向
  • 投影变换:将顶点从相机坐标系变换到裁剪坐标系,使用透视投影或正交投影矩阵
  • 透视除法:将裁剪坐标的 x, y, z 分量除以 w 分量,得到 NDC 坐标(范围 [-1, 1])
  • 视口变换:将 NDC 坐标映射到屏幕坐标(像素位置)

透视投影的本质

透视投影模拟真实相机的成像原理:远处的物体看起来更小。投影矩阵将视锥体(Frustum)内的点映射到立方体,同时保留深度信息用于深度测试。

投影后的齐次坐标形式为 (x,y,z,w),其中 w 分量保留了深度信息。透视除法后,x 和 y 坐标反映了物体在屏幕上的位置,z 坐标(经过深度映射)用于深度测试。这就是为什么透视投影会产生"近大远小"的效果——w 分量随距离增大,除法后坐标收缩。

三角形光栅化

三角形是 3D 渲染的基本图元,原因在于:三角形一定是平面图形(四个顶点可能不共面);三角形可以任意细分;任何多边形都可以三角剖分。

边界函数法

边界函数法(Edge Function)是现代 GPU 实现光栅化的核心算法,其思想是使用三角形三条边的隐式方程判断点是否在三角形内。

对于三角形的三个顶点 v0,v1,v2,每条边定义一个边界函数:

E01(x,y)=(x1x0)(yy0)(y1y0)(xx0)

边界函数的几何意义是点 (x,y) 到边的有向距离乘以边长。如果在三角形内部,三个边界函数的符号相同(假设顶点按逆时针顺序)。

  • 算法流程:

    1. 计算三角形的包围盒,只测试包围盒内的像素
    2. 对每个像素中心计算三个边界函数
    3. 如果三个边界函数都非负(或都非正),则像素在三角形内
    4. 计算重心坐标用于属性插值
  • 优化技巧:

    • 增量计算:相邻像素的边界函数值只相差常数,可以增量更新
    • 分块处理:将三角形按 8x8 或 16x16 像素块分块,快速剔除外部块
    • 早出测试:一旦某个边界函数为负,立即跳过该像素

扫描线算法

扫描线算法是经典的光栅化方法,虽然现代 GPU 很少直接使用,但理解它有助于掌握光栅化的基本原理。

  • 算法流程:

    1. 找到三角形的最高和最低 y 坐标,确定扫描范围
    2. 对每条扫描线,计算与三角形两条边的交点,得到左右边界
    3. 在左右边界之间填充像素
  • 边缘方程: 对于边 (x0,y0)(x1,y1),在扫描线 y 处的 x 坐标为:

    x=x0+(yy0)x1x0y1y0

    相邻扫描线的 x 坐标可以增量计算:

    xy+1=xy+x1x0y1y0

优缺点:逻辑清晰,适合软件实现;按顺序遍历,缓存友好;除法运算成本高;处理斜边时可能出现裂缝或重叠;现代 GPU 需要并行处理,扫描线的顺序性不适合。

重心坐标与属性插值

确定像素在三角形内后,需要计算该像素的深度、纹理坐标、颜色等属性。这些属性通过对顶点属性插值得到,插值权重就是重心坐标。

重心坐标

重心坐标(Barycentric Coordinates)表示点相对于三角形三个顶点的位置。对于三角形内任意点 P,可以表示为:

P=λ0v0+λ1v1+λ2v2

其中 λ0+λ1+λ2=1,且 λi0

重心坐标可以通过边界函数计算:

λ0=E12(P)E12(v0),λ1=E20(P)E20(v1),λ2=E01(P)E01(v2)

属性插值

顶点的属性(如法向量、纹理坐标、颜色)可以使用重心坐标插值:

attr=λ0attr0+λ1attr1+λ2attr2

但对于透视投影,这种线性插值是不正确的,因为透视变换产生了深度差异。需要透视校正插值

attr=λ0attr0/w0+λ1attr1/w1+λ2attr2/w2λ0/w0+λ1/w1+λ2/w2

其中 w 是顶点在裁剪空间中的 w 分量。纹理坐标必须使用透视校正插值,否则纹理会产生明显的变形。法向量、颜色等属性在视觉要求不高时可以使用线性插值以节省计算。

深度插值与深度测试

光栅化不仅要生成像素,还要计算每个像素的深度值,用于深度测试判断遮挡关系。

深度值计算

顶点的深度值在裁剪空间中存储为 z 分量,经过透视除法后得到 NDC 深度。在屏幕空间中,深度值通过线性或对数映射到 [0, 1] 范围:

depthscreen=zndc+12

三角形内部的深度值通过重心坐标插值得到:

z=λ0z0+λ1z1+λ2z2

深度测试

每个片段的深度值与深度缓冲区中存储的值比较:

  • 如果 depthfragment<depthbuffer,则片段通过测试,更新深度缓冲区和颜色缓冲区
  • 否则片段被遮挡,丢弃

深度测试的精度受深度缓冲区位数限制(通常 24 位),会出现Z-fighting(深度冲突):两个表面几乎重合时,深度值接近,浮点误差导致闪烁。解决方法是将一个表面稍微偏移,或使用更高精度的深度缓冲。

硬件加速

现代 GPU 对光栅化进行了专门的硬件优化,这些优化使实时渲染成为可能。

并行处理单元

GPU 包含专门的光栅化单元,包含多个子模块协同工作:

  • 三角形设置:计算三角形的边界方程、包围盒、平面方程
  • 扫描转换器:遍历包围盒内的像素,测试是否在三角形内
  • 插值器:计算片段的深度、纹理坐标、颜色等属性
  • Early-Z:在片段着色之前进行深度测试,剔除被遮挡的片段

这些单元可以并行处理多个三角形,充分利用 GPU 的并行计算能力。现代 GPU 可能包含数十甚至上百个光栅化单元。

分块光栅化

传统的光栅化逐像素处理,但现代 GPU 采用分块(Tile-based)光栅化以提高缓存命中率:

  • 将屏幕划分为 16x16 或 32x32 像素的块
  • 三角形覆盖哪些块,只对这些块进行光栅化
  • 每个块内部的光栅化在快速片上缓存(On-chip Cache)中完成
  • 减少对深度缓冲区的访问,降低带宽压力

这种技术在移动 GPU(如 ARM Mali、Qualcomm Adreno)上尤其重要,因为移动设备的内存带宽有限。桌面 GPU(NVIDIA、AMD)也采用类似技术,称为分桶渲染(Bin Rendering)。

Z-Cull 与 Hi-Z

Z-Cull 是一组深度测试优化技术,在光栅化早期阶段快速剔除被遮挡的片段:

  • Hi-Z(Hierarchical Z):为每个像素块维护深度值的范围,如果整个块的深度都大于当前深度缓冲区,则整个块被剔除
  • Early-Z:在片段着色之前进行深度测试,避免对被遮挡的片段执行昂贵的着色计算
  • Fast Z-Clear:快速清除深度缓冲区,使用压缩或延迟清除技术

这些优化可以显著减少片段着色器的调用次数,尤其在高遮挡场景(如室内场景、密集 foliage)中效果明显。但需要注意,如果片段着色器修改了深度值(discard、depth write),则无法使用 Early-Z。

抗锯齿

光栅化产生的锯齿边缘是因为离散采样无法精确表示连续边界。GPU 使用多重采样抗锯齿(MSAA)来缓解这个问题:

  • 在每个像素内多次采样(通常是 4x 或 8x),而不是只采样中心
  • 只在覆盖边界附近的像素执行多次采样
  • 内部像素仍然只采样一次,避免过度计算
  • 最终颜色是所有采样点的平均值

MSAA 的优势是对几何边缘有效,但对纹理锯齿和着色锯齿(高光边缘)无效,这些需要其他技术如 TAA(时间抗锯齿)或超采样。

2D 与 3D 光栅化的差异

2D 渲染引擎也需要光栅化矢量路径,但与 3D 光栅化有本质区别:

  • 几何复杂度:2D 路径可能非凸、多环、自交,包含曲线;3D 三角形总是凸的、简单的
  • 填充规则:2D 需要处理 nonzero 和 even-odd 填充规则;3D 总是填充三角形内部
  • 属性插值:2D 通常只需插值颜色和纹理坐标;3D 需要插值深度、法向量、多种纹理坐标
  • 硬件支持:3D 光栅化有专门的硬件单元;2D 光栅化主要靠 CPU 软件实现或 GPU 通用计算

这些差异使得 2D 光栅化算法更复杂,但场景规模通常更小,对性能的要求也相对较低。