光栅化算法
光栅化是将连续的几何图元转换为离散像素的过程,是渲染管线的核心步骤。在此之前,顶点经过模型、视图、投影变换,从三维空间投影到了二维屏幕上。光栅化的任务是确定哪些像素被三角形覆盖,并为这些像素生成片段(Fragment),后续的片段着色器将为每个片段计算最终颜色。
投影变换
顶点从模型空间到屏幕空间的完整变换路径:模型空间 → 世界空间 → 观察空间 → 裁剪空间 → 规范化设备坐标(NDC) → 屏幕空间。
- 模型变换:将顶点从局部坐标系变换到世界坐标系,处理物体的位置、旋转、缩放
- 视图变换:将顶点从世界坐标系变换到相机坐标系,模拟相机的观察方向
- 投影变换:将顶点从相机坐标系变换到裁剪坐标系,使用透视投影或正交投影矩阵
- 透视除法:将裁剪坐标的 x, y, z 分量除以 w 分量,得到 NDC 坐标(范围 [-1, 1])
- 视口变换:将 NDC 坐标映射到屏幕坐标(像素位置)
透视投影的本质
透视投影模拟真实相机的成像原理:远处的物体看起来更小。投影矩阵将视锥体(Frustum)内的点映射到立方体,同时保留深度信息用于深度测试。
投影后的齐次坐标形式为
三角形光栅化
三角形是 3D 渲染的基本图元,原因在于:三角形一定是平面图形(四个顶点可能不共面);三角形可以任意细分;任何多边形都可以三角剖分。
边界函数法
边界函数法(Edge Function)是现代 GPU 实现光栅化的核心算法,其思想是使用三角形三条边的隐式方程判断点是否在三角形内。
对于三角形的三个顶点
边界函数的几何意义是点
算法流程:
- 计算三角形的包围盒,只测试包围盒内的像素
- 对每个像素中心计算三个边界函数
- 如果三个边界函数都非负(或都非正),则像素在三角形内
- 计算重心坐标用于属性插值
优化技巧:
- 增量计算:相邻像素的边界函数值只相差常数,可以增量更新
- 分块处理:将三角形按 8x8 或 16x16 像素块分块,快速剔除外部块
- 早出测试:一旦某个边界函数为负,立即跳过该像素
扫描线算法
扫描线算法是经典的光栅化方法,虽然现代 GPU 很少直接使用,但理解它有助于掌握光栅化的基本原理。
算法流程:
- 找到三角形的最高和最低 y 坐标,确定扫描范围
- 对每条扫描线,计算与三角形两条边的交点,得到左右边界
- 在左右边界之间填充像素
边缘方程: 对于边
到 ,在扫描线 处的 x 坐标为: 相邻扫描线的 x 坐标可以增量计算:
优缺点:逻辑清晰,适合软件实现;按顺序遍历,缓存友好;除法运算成本高;处理斜边时可能出现裂缝或重叠;现代 GPU 需要并行处理,扫描线的顺序性不适合。
重心坐标与属性插值
确定像素在三角形内后,需要计算该像素的深度、纹理坐标、颜色等属性。这些属性通过对顶点属性插值得到,插值权重就是重心坐标。
重心坐标
重心坐标(Barycentric Coordinates)表示点相对于三角形三个顶点的位置。对于三角形内任意点
其中
重心坐标可以通过边界函数计算:
属性插值
顶点的属性(如法向量、纹理坐标、颜色)可以使用重心坐标插值:
但对于透视投影,这种线性插值是不正确的,因为透视变换产生了深度差异。需要透视校正插值:
其中
深度插值与深度测试
光栅化不仅要生成像素,还要计算每个像素的深度值,用于深度测试判断遮挡关系。
深度值计算
顶点的深度值在裁剪空间中存储为 z 分量,经过透视除法后得到 NDC 深度。在屏幕空间中,深度值通过线性或对数映射到 [0, 1] 范围:
三角形内部的深度值通过重心坐标插值得到:
深度测试
每个片段的深度值与深度缓冲区中存储的值比较:
- 如果
,则片段通过测试,更新深度缓冲区和颜色缓冲区 - 否则片段被遮挡,丢弃
深度测试的精度受深度缓冲区位数限制(通常 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 光栅化算法更复杂,但场景规模通常更小,对性能的要求也相对较低。