高效着色 (Efficient Shading)
本章的核心目标是解决当场景中包含大量动态光源和复杂材质时,如何高效地进行着色计算,以维持稳定的高帧率。传统的前向渲染方法在这种情况下会遇到性能瓶颈,因此本章介绍了一系列高级的渲染架构和优化技术。
核心思想:解耦与分治
贯穿本章的一个核心思想是“解耦” (Decoupling):将复杂的渲染流程拆分成多个独立的阶段。具体来说,就是将可见性/几何信息的确定与光照/材质的计算分离开来。这使得每个阶段都可以被专门优化。
20.1 延迟着色 (Deferred Shading)
延迟着色是应对大量光源场景的经典解决方案。它颠覆了传统的渲染顺序,将光照计算“延迟”到所有几何体信息都确定之后。
核心观点
- 
传统前向着色 (Forward Shading) 是“以物体为中心”的,每个物体被绘制时,会遍历所有可能影响它的光源并计算着色。当光源数量增多时,计算量会爆炸式增长。 
- 
延迟着色则是“以屏幕像素为中心”的。它将渲染分为两个主要阶段: - 几何阶段 (Geometry Pass): 遍历场景中的所有不透明物体,但不进行任何光照计算。此阶段的目标是生成一系列屏幕空间的纹理,统称为 G-buffer (Geometric Buffer)。G-buffer 中存储了后续光照计算所需的所有表面属性。
- 关键数据: 深度(位置)、表面法线、反照率颜色 (Albedo)、金属度/粗糙度等材质参数。
 
- 光照阶段 (Lighting Pass): 渲染代表光源影响范围的几何体(如球体或屏幕填充四边形)。在着色器中,从 G-buffer 中采样对应像素的表面属性,然后执行光照计算,并将结果累加到最终的图像上。
 
- 几何阶段 (Geometry Pass): 遍历场景中的所有不透明物体,但不进行任何光照计算。此阶段的目标是生成一系列屏幕空间的纹理,统称为 G-buffer (Geometric Buffer)。G-buffer 中存储了后续光照计算所需的所有表面属性。
关键术语与优势
- G-buffer: 几何缓冲区的简称,是延迟着色的核心数据结构,存储了屏幕上每个像素的表面信息。
- 解耦光照与几何: 这是延迟着色最大的优势。光照计算的成本与屏幕上被光源影响的像素数量成正比,而与场景的几何复杂性无关。这使得在几何体数量极多的场景中也能高效处理成百上千的光源。
- 简化的着色器管理: 无需为“材质A x 光源类型B”的每种组合编写一个着色器。几何阶段和光照阶段的着色器可以独立开发和管理。
- 更短、更快的着色器: 每个阶段的着色器任务单一,通常更短,寄存器使用更少,从而提高GPU的占用率 (Occupancy),实现更好的并行执行效率。
缺点与挑战
- 高带宽和显存消耗: G-buffer 需要同时读写多个高精度渲染目标(MRT),对显存带宽和容量是巨大的考验。数据压缩(例如,对法线使用八面体编码)是常用的优化手段。
- 不支持透明物体 (Transparency): G-buffer 在每个像素位置通常只能存储一个表面的信息,因此无法直接处理半透明物体。通常的解决方案是采用混合渲染管线:先用延迟着色渲染所有不透明物体,再用传统的前向着色渲染透明物体。
- 硬件抗锯齿 (MSAA) 支持困难: 直接对 G-buffer 应用 MSAA 会导致显存和带宽成本成倍增加,通常不可行。解决方案多依赖于后处理抗锯齿技术(如FXAA, TAA)或更复杂的边缘检测MSAA方案。
- 材质多样性受限: 基础的 G-buffer 布局只能支持一种或几种固定的材质模型。为了支持更多种类的材质,通常需要引入材质ID或掩码,但这会增加光照着色器的复杂度。
20.2 贴花渲染 (Decal Rendering)
贴花是一种在已有着色表面上叠加额外细节(如弹孔、涂鸦、血迹)的技术。
核心观点
- 传统的贴花实现方式(如增加顶点数据或为每个贴花重绘一次模型)效率低下,尤其是在贴花数量多或需要跨越多个模型时。
- 现代渲染中,最高效的方法是投影贴花 (Projected Decals)。它将贴花定义在一个**有限体积(通常是包围盒)**内,并像投影仪一样将其投射到该体积内的任何几何表面上。
- 延迟着色是实现投影贴花的理想选择。因为贴花可以被视为对 G-buffer 的一次“修改”操作。例如,一个弹孔贴花可以直接修改 G-buffer 中对应像素的反照率和法线。
- 在光照阶段,着色器只会看到被贴花修改后的最终 G-buffer 数据,然后进行一次光照计算即可。这完美避免了前向渲染中因多 Pass 渲染贴花而导致的过度绘制 (Overdraw) 和复杂的着色逻辑。
关键术语与挑战
- 投影体积 (Projection Volume): 定义贴花影响范围的包围盒。
- G-buffer 修改: 贴花pass在光照pass之前运行,其着色器读取深度缓冲来重建世界坐标,然后计算并输出修改后的G-buffer值。
- 法线混合: 当基础材质和贴花都有法线贴图时,如何正确地混合它们是一个技术难点,处理不当可能会产生瑕疵。
20.3 分块着色 (Tiled Shading)
分块着色是经典延迟着色的进化版,旨在解决当光源数量极多时,G-buffer 带宽成为新瓶颈的问题。
核心观点
- 传统延迟着色中,即使光源只覆盖屏幕的一小部分,也需要发起一次独立的绘制调用(Draw Call),并且每个受影响的像素都需要重新读取 G-buffer。当成百上千个光源重叠时,这种方式效率低下。
- 分块着色 (Tiled Shading) 的核心思想是批量处理光源。
- 首先将屏幕划分为一个个分块 (Tile)(例如,16x16 或 32x32 像素的网格)。
- 然后,通过一次剔除计算,为每个 Tile 创建一个独立的光源列表,该列表仅包含与该 Tile 相交的光源。
- 最后,对每个 Tile 执行一次着色计算(通常用计算着色器 Compute Shader),在这个计算中,着色器会遍历该 Tile 的光源列表,对其中的所有像素一次性完成光照计算。
 
关键术语与变体
- 光源列表 (Light List): 每个 Tile 独有的、经过剔除的、会影响该 Tile 的光源索引列表。
- 分块延迟着色 (Tiled Deferred Shading):
- G-buffer Pass: 同经典延迟着色。
- 光源剔除 Pass: 在 Compute Shader 中,为每个 Tile 构建光源列表。
- 光照 Pass: 在 Compute Shader 中,每个线程组处理一个 Tile。Tile 内的线程读取 G-buffer,遍历光源列表,计算最终颜色。
 
- 分块前向着色 (Tiled Forward Shading / Forward+): 这是分块思想与前向渲染的结合,特别适合需要 MSAA 和透明度的场景。
- Z-Prepass: 深度预渲染,生成完整的深度缓冲,用于后续的光源剔除和避免片元着色的过度绘制。
- 光源剔除 Pass: 与 Tiled Deferred 类似,但可以利用 Z-Prepass 的深度信息进行更精确的剔除。
- 前向渲染 Pass: 正常渲染场景几何体。在像素着色器中,根据当前像素所在的 Tile 位置,获取对应的光源列表,并仅对列表中的光源进行计算。
 
深度剔除优化
- 
为了进一步减少每个 Tile 光源列表的长度,可以在光源剔除时利用深度信息。 
- 
基本思路: - 在光源剔除前,先计算出每个 Tile 内所有像素的最小和最大深度值 ( 和 )。
- 如果一个光源的影响体积(球体)在深度上与 [$z_{min}, z_{max}$]区间没有重叠,那么就可以安全地将它从该 Tile 的光源列表中剔除。
 
- 
深度不连续性问题 (Depth Discontinuity): 当一个 Tile 内同时包含近景物体和远景背景时, 和 的范围会非常大,导致深度剔除几乎失效。 
- 
高级深度剔除技术: - 2.5D Culling: 将每个 Tile 的深度范围 [$z_{min}, z_{max}$]划分为固定数量(如32或64)的切片。
- 位掩码 (Bitmask):
- 创建一个几何位掩码,标记哪些深度切片内存在几何体。
- 为每个光源也创建一个光源位掩码,标记其影响体积覆盖了哪些深度切片。
- 通过对两个掩码进行按位与 (AND) 操作,可以快速判断光源是否与 Tile 内的任何几何体相交。
 
 
- 2.5D Culling: 将每个 Tile 的深度范围 
20.4 聚类着色 (Clustered Shading)
聚类着色是分块着色(Tiled Shading)的自然演进,它将二维的屏幕分块思想扩展到了三维空间,从而提供了更稳定、更普适的光源剔除方案。
核心观点
- 从2D Tile到3D Cluster: 分块着色将屏幕划分为2D网格(Tiles),而聚类着色 (Clustered Shading) 将整个视锥体 (View Frustum) 划分为一个三维的单元格网格,这些三维单元格被称为聚类 (Cluster)。
- 与场景几何无关: 这个三维网格的划分是独立于场景几何的,仅根据摄像机视锥体来定义。这与分块着色中依赖于 Tile 内几何体 min/max深度的优化方式有本质区别。
- 指数深度切片: 为了让远处的 Cluster 形状更接近立方体(而不是因透视效应产生的细长体素),通常会在 Z 轴(深度)方向上进行指数级 (Exponential) 或对数级 (Logarithmic) 的划分。这意味着近处的 Cluster 在深度上很薄,远处的 Cluster 很厚。
- 统一的光照方案: 在像素着色时,通过片元的三维世界坐标可以直接定位到其所在的唯一 Cluster,并获取该 Cluster 的光源列表。由于这个过程不依赖 G-buffer,因此该方法可以统一处理不透明物体、透明物体和体积效果,无需复杂的混合渲染管线。
关键术语与优势
- 聚类 (Cluster): 视锥体被分割成的一个三维子空间单元。
- 更强的鲁棒性: 性能表现不易受摄像机微小移动或特定场景布局的影响。分块着色中常见的“一排路灯恰好落入同一个 Tile”导致光源列表爆炸的问题,在聚类着色中得到了根本性解决,因为这些路灯会自然地分布在不同的深度 Cluster 中。
- 完美处理深度不连续: 分块着色在遇到深度不连续(前景+远景)的 Tile 时,其深度剔除优化会失效。聚类着色则天然地解决了这个问题。
- 前向与延迟皆可应用:
- 聚类前向着色 (Clustered Forward): 配合 Z-prepass,可以高效支持 MSAA 和透明度,是《DOOM (2016)》等游戏的选择。
- 聚类延迟着色 (Clustered Deferred): 依然可以和 G-buffer 结合使用。
 
- 可包含更多元素: 除了光源,贴花 (Decals) 和光照探针 (Light Probes) 也可以被分配到 Cluster 中,形成一个统一的环境效果查询系统。
Z-Bin 方法
- 一种内存优化的变体: Z-Bin 是一种混合方法,它试图在分块着色的低内存占用和聚类着色的高精度剔除之间取得平衡。
- 工作原理:
- 保留分块着色的 2D Tile 光源列表。
- 额外创建一个全局的 1D Z轴切片数组(Z-Bins)。
- 将所有光源按深度排序。每个 Z-Bin 存储覆盖其深度范围的光源ID区间(min_ID,max_ID)。
- 在着色时,一个像素首先获取其所在 Tile 的光源列表,然后根据自身深度获取所在 Z-Bin 的光源ID区间,两个列表求交集,得到最终需要计算的光源。
 
- 权衡 (Trade-off): 用更少的内存和预计算换取着色时更多的逻辑计算。
20.5 延迟纹理 (Deferred Texturing) / 可见性缓冲 (Visibility Buffer)
这项技术将“延迟”的思想推向了极致,不仅延迟光照计算,甚至延迟了材质纹理的读取,其核心目标是彻底消除因过度绘制 (Overdraw) 造成的无效纹理带宽消耗。
核心观点
- 问题的根源: 即使在延迟着色中,G-buffer 的生成过程本身也存在过度绘制。当一个被遮挡的像素被多次写入 G-buffer 时,前面几次写入所发生的纹理采样 (Texture Fetch) 都被浪费了。
- 核心思想:先确定可见性,再获取数据。 将渲染管线改造为两步:
- 可见性 Pass (Visibility Pass): 以极高的速度渲染场景,但像素着色器不进行任何纹理采样。它只输出标识信息,用于唯一确定当前像素最终可见的是哪个物体的哪个三角形。这个输出结果被称为可见性缓冲区 (Visibility Buffer)。
- 关键数据: 三角形ID (Triangle ID) 和 实例ID (Instance ID)。通常一个 32-bit 或 64-bit 的整数足矣。
 
- 着色 Pass (Shading Pass): 使用一个全屏的计算着色器 (Compute Shader)。每个线程负责一个像素,它从可见性缓冲区中读取 ID,然后反向索引到全局的顶点和材质数据,手动进行顶点属性插值、纹理采样和光照计算。
 
- 可见性 Pass (Visibility Pass): 以极高的速度渲染场景,但像素着色器不进行任何纹理采样。它只输出标识信息,用于唯一确定当前像素最终可见的是哪个物体的哪个三角形。这个输出结果被称为可见性缓冲区 (Visibility Buffer)。
关键术语与优势
- 可见性缓冲区 (Visibility Buffer): 一个只存储了标识信息(如TriangleID,InstanceID)的轻量级 G-buffer。
- 带宽换计算: 这种方法是典型的“用计算换带宽”。它避免了昂贵的内存访问,代价是需要在 Compute Shader 中手动实现硬件为我们做的一些工作(如重心坐标计算、属性插值等)。这顺应了GPU计算能力增长速度超过带宽增长速度的硬件发展趋势。
- 零纹理采样浪费: 纹理只会被最终可见的像素读取一次,带宽利用率达到理论上的最高。
- 对微小三角形友好: 传统的四边形着色机制(GPU一次处理2x2像素)在处理微小三角形时效率低下。可见性缓冲区的计算着色器模式可以避免此问题。
20.6 对象空间和纹理空间着色 (Object/Texture-Space Shading)
这类方法受到离线渲染器 Reyes 的启发,提出了一种革命性的思路:不再屏幕空间(Screen Space)进行着色,而是在物体自身的 UV 空间(纹理空间)或对象空间中完成着色。
核心观点
- 解耦着色频率与渲染频率: 这是该技术最核心的优势。
- 纹理空间着色 (Texture-Space Shading): 这是一个昂贵的预计算步骤,可以在一个较低的频率(如 30Hz)下运行。
- 屏幕光栅化 (Screen Rasterization): 这是一个非常廉价的步骤,只需对模型进行变换和纹理采样,可以在极高的频率(如 60Hz, 90Hz+)下运行。
 
- 工作流程 (以《奇点灰烬》为例):
- 分配图集空间: 为每个可见物体,根据其在屏幕上的预估大小,在一个巨大的“主纹理”图集(Atlas)中分配一块区域。
- 在图集中着色: 运行一个计算着色器,将每个物体的完整材质和光照效果直接“绘制”到它在图集里对应的区域上。
- 最终渲染: 正常光栅化场景中的物体。其像素着色器变得极其简单,仅仅是根据顶点的 UV 坐标,从“主纹理”图集中采样出早已计算好的最终颜色。
 
关键术语与优势
- Reyes 渲染器: 皮克斯早期使用的著名离线渲染架构,核心思想是将物体细分为亚像素大小的“微多边形”再进行着色。
- 纹理空间着色 (Texture-Space Shading): 在物体的UV展开图上直接进行着色计算。
- 异步着色 (Asynchronous Shading): 着色计算和最终画面呈现可以以不同的帧率独立运行,即使着色负载很高,也能保持几何渲染的流畅,从而获得极其稳定的帧率。
- 高质量的材质表现: 由于着色是在物体的表面上进行的,可以根据需要分配足够的分辨率,非常适合处理具有高频细节的材质(如复杂的镜面反射),并且可以自然地进行高质量的纹理过滤。
挑战
- 着色浪费: 会对物体的整个表面进行着色,即使其中大部分在最终画面中被遮挡。因此,该技术更适用于深度复杂度较低的场景(如即时战略游戏)。
- 复杂的管理: 图集分配算法、多材质物体的处理、与屏幕空间效果(如 SSAO)的结合都比较复杂。
总结:渲染方法的权衡
下表简要对比了本章讨论的几种核心渲染架构的优缺点:
| 特性 | 前向渲染 (Forward) | 延迟着色 (Deferred) | 分块/聚类前向 (Forward+) | 可见性缓冲 (Visibility Buffer) | 
|---|---|---|---|---|
| 核心思想 | 物体驱动,一次完成 | 像素驱动,延迟光照 | 像素驱动,批量处理光源 | 像素驱动,延迟一切 | 
| 多光源效率 | 差 | 好 (但受带宽限制) | 极好 | 极好 | 
| MSAA 支持 | 简单 | 困难 | 简单 | 困难 | 
| 透明度支持 | 简单 | 困难 | 简单 | 困难 | 
| 材质多样性 | 灵活 | 受限 | 灵活 | 灵活 | 
| 带宽消耗 | 低-中 | 高 | 中 | 低 | 
| 微小三角形效率 | 差 | 中 | 差 | 好 | 
结论:没有银弹。 最佳方案取决于具体应用的需求:硬件平台、场景复杂度、对MSAA/透明度的要求以及开发成本等。现代引擎往往会根据不同物体的特性(不透明、透明、粒子等)混合使用多种技术。