深入解析集群前向渲染(Clustered Forward Shading)
在实时图形渲染管线演进的过程中,处理海量实时动态光源一直是最核心的挑战之一。虽然 Tiled Forward(Forward+)通过屏幕 2D 网格切割有效降低了光照计算复杂度,但它在面对深度跨度极大或深度不连续的场景时依然存在严重的算力浪费。
集群前向渲染(Clustered Forward Shading) 通过将相机的视锥体(Frustum)在 X、Y 轴以及 Z 轴(深度)三个维度上同时切块,形成三维体素集群(Clusters)。本文将详细解构 Clustered Shading 的数学原理、五步管线工作流、Compute Shader 代码实现 以及工业级性能调优方向。
一、 核心痛点与范式演进
1. Tiled Forward (Forward+) 的深度跨度缺陷
在 Tiled Forward(Forward+)管线中,屏幕被分割为 \(16 \times 16\) 像素的 2D Tile。每个 Tile 依靠内部像素的最小与最大深度值 \([Z_{\text{min}}, Z_{\text{max}}]\) 构建视锥包围体。
然而,当场景存在深度不连续(Depth Discontinuity)时(例如前景有一个电线杆,背景是几百米外的天空或大楼),Tile 的 \([Z_{\text{min}}, Z_{\text{max}}]\) 范围会被拉得极长。这会导致位于近景与远景之间空旷区域的大量光源被误判为“与 Tile 相交”,从而强行参与片元着色器的光照累加,造成极大的算力浪费。
2. Clustered Shading 的三维体素解耦
Clustered Shading 将视锥体在三维空间中划分成若干个小立方体网格(Cluster / Voxel)。
1 | 相 机 视 锥 体 3 D 切 割 (Clusters) |
- 彻底解决深度不连续问题:因为 Z 轴也被切分成多个区间,近景与远景被划分至不同的 Z-Slice(Z 切片)。光源仅会被记录在它真正覆盖的 3D Cluster 中,不会影响同一 2D Tile 下其他深度级别的像素。
- 解耦渲染与场景复杂度:与观察物体的几何结构解除强关联,大幅提升了多光源场景下的渲染帧率稳定性。
二、 基本步骤:Clustered 渲染五步管线
Clustered Forward Shading 的完整执行流程可以划分为以下五个核心阶段:
1 | +--------------------+ +--------------------+ +--------------------+ |
- 深度预计算 (Pre-Depth Pass):仅渲染不透明物体生成 Depth Buffer。在此基础上标记出屏幕中真正包含有效片元的 3D Clusters(此步骤为可选优化项,搭配 SSAO、MSAA 使用效果更佳)。
- 计算光照边界 (Light Bounds & Count):根据每个光源的位置与作用半径计算其包围体,遍历与之重叠的 Clusters,递增累加每个 Cluster 命中的光源数量(Lighting Count)。
- 累加偏移量 (Prefix Sum Offset):遍历所有 Cluster 的 Lighting Count 进行前缀和累加,计算出全局总光源数(Lighting Total),并记录每个 Cluster 在全局数组中的起始写入偏移量(Offset)。
- 记录光源列表 (Build Light List):再次遍历光源包围体覆盖的有效 Cluster,结合该 Cluster 的 Count 与 Offset,将其对应的 Light Index 依次存入全局 Light List 中。
- 片元光照计算 (Forward Shading Pass):在正向渲染 Pass 中,片元着色器根据当前像素计算出所属的 Cluster ID,依据 Offset 和 Count 读取对应 Light List 并完成光照累加。
三、 具体实现细节与坐标转换
1. 视图空间转换与 Z 轴对数分片
在着色器中,首先将片元的世界坐标转换至相机视图空间(View Space): \[\mathbf{P}_{\text{view}} = \mathbf{V} \cdot \mathbf{P}_{\text{world}}\]
由于透视投影的非线性特性,为了让近处拥有更高的空间切割精度、远处分割相对稀疏,Z 轴方向通常采用对数切割(Logarithmic Splitting)。
根据屏幕坐标 frag_pos.xy 与视图空间深度
view_z 计算 Cluster 三维网格坐标 \((c_x, c_y, c_z)\) 的 GLSL 实现如下:
1 | uvec3 view_pos_to_grid_coord(vec2 frag_pos, float view_z) |
Tips:关于
gl_FragCoord,当 Viewport 范围为 \((0, 0, 800, 600)\) 时,x, y的中心点取值范围为 \((0.5, 0.5)\) 至 \((799.5, 599.5)\)。因此计算时需要减去0.5f偏移。
2. 3D 坐标折叠与有效 Cluster 存储
为了高效存储与读取,需要将三维网格坐标 \((c_x, c_y, c_z)\) 转换为一维展平索引(1D Grid Index):
1 | uint grid_coord_to_grid_idx(uvec3 c) |
在 Pre-Depth 阶段,通过 Compute Shader 或片段着色器将包含有效像素的 Cluster 标记写入位图或 Image 缓冲区:
1 | // 保存当前 Cluster 为有效状态 (Active Flag) |
1 | 有效 Cluster 标记 (Grid Flags Storage) |
3. 光源求交与前缀和 Offset 计算
在 Compute Shader 阶段计算光源影响范围: 1.
将光源位置变换至相机视图空间,结合 Light Range 得到包围盒(AABB)。 2.
剔除完全位于相机视锥体之外的光源。 3. 转换为对应的 Cluster
坐标范围,递增对应 Cluster 的 grid_light_counts。 4. 设置
Compute Shader 的线程组大小(numgroup 的 X, Y, Z 设置每个
Group Size 为 16)以并行计算前缀和 Offset。
1 | // Compute Shader 伪代码:前缀和累加与 Light List 生成 |
四、 工业级工程优化方向
在实际引擎工程开发中,针对内存访问效率与硬件特性,Clustered Shading 有以下重要的优化手段:
1. 资源类型与 Cache 命中率优化
- 空间连续性与缓存:光照数据的访问具有极强的空间局部性(Spatial Locality)。使用 Buffer 或 Texture 能够提供更高的 L1/L2 Cache 命中率。
- UBO vs SSBO:对于随机访问特性更强的通用数据,使用
UBO(Uniform Buffer Object)可以减少纹理采样的开销(如存储光源基础向量
light vec)。
2. 贴图格式与显存带宽压缩
- 为了降低显存带宽消耗,应根据实际数据取值范围选用最小的贴图格式。
- 例如,存储
Offset与Count的uvec2变量,可以使用压缩格式如uint r16g16或uint r8g8来替代全精度的 32-bit 格式。
3. 阴影分支处理权衡 (Shadow Branching Strategy)
在 Clustered 架构中,处理带阴影(Shadowed)与无阴影(Unshadowed)的光源有两个主流方向,开发者需要根据硬件瓶颈进行权衡:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 光源信息标记分支 | 在统一的光源列表中增加
bool isShadowed 属性 |
带宽占用小,数据结构简单 | 片段着色器中引入动态分支(Dynamic Branching),可能导致 Thread Divergence |
| 物理分表存储 | 在构建 Light List 时拆分为“无阴影光源列表”与“带阴影光源列表” | 着色器无需动态分支判断,执行效率高 | 需要更大的内存与显存带宽 |
五、 渲染管线对比总结
| 渲染管线特性 | Forward (正向) | Deferred (延迟) | Forward+ (Tiled Forward) | Clustered Forward |
|---|---|---|---|---|
| 空间划分维度 | 无划分 | 全屏像素 | 2D Tile (\(16 \times 16\)) | 3D Cluster (\(X \times Y \times Z\)) |
| 深度跨度问题 | 严重 Overdraw | 严重 Overdraw | 产生光源误判与无效计算 | 彻底解决 |
| 半透明/MSAA 支持 | 原生支持 | 限制极大 | 原生支持 | 原生完美支持 |
| 显存带宽消耗 | 极低 | 极高 (G-Buffer) | 较低 | 低 (仅需 Light List) |
六、 总结
集群前向渲染(Clustered Forward Shading)通过 3D 体素化视锥切割,成功破解了 Tiled Forward 面对深度不连续场景时的光照误判难题: * 核心突破:利用对数 Z 轴切割(Logarithmic Z-Splitting)与三维网格映射,实现了高效的 \(O(1)\) 级空间光照查找; * 工程落脚点:依靠 Pre-Depth Pass 剔除无效 Cluster,结合 Compute Shader 完成前缀和 Offset 累加与 Light List 填充; * 优化精髓:通过压榨带宽格式(R16G16)、合理规划 UBO/SSBO 缓存以及平衡阴影分支,为现代引擎提供了支持成千上万个实时动态光源的强大支撑。