URP Forward+

在 URP 中,Forward+(前向加) 渲染路径的实现核心在 ForwardLights.cs 脚本中,并借助了 Unity Jobs (Burst 编译) 实现了基于屏幕空间 Tile 和 Z-Bin 的多线程剔除与灯光分配。

1. 数据结构与缓冲区初始化

在 Forward+ 模式下 (m_UseForwardPlus == true),不再像传统前向渲染那样仅依赖全局的逐物体灯光列表。ForwardLights 会调用 CreateForwardPlusConstantBuffers() 申请专门的 GPU 数据结构:

  • m_ZBins 和 m_ZBinsBuffer:用于按深度 (Z) 切分屏幕空间,记录不同深度范围内的灯光信息。
  • m_TileMasks 和 m_TileMasksBuffer:用于按 2D 网格切分屏幕(即 Tile),掩码(Mask)位用于标记每个屏幕分块里有哪些局部光源或反射探针。
  • 额外实例化了 ReflectionProbeManager 用于统一管理反射探针。

2. 多线程屏幕空间灯光分配 (Clustering Jobs)

在每帧的准备阶段 (PreSetup 中调用 ScheduleClusteringJobs),URP 避免了在主线程进行昂贵的光照求交测试,而是派发了几个高效的 C# Jobs:

  • TilingJob:并行遍历当前摄像机内的可见光和反射探针。它会将光源的作用范围(点光球体或聚光锥体)投影到相机的视锥体面上,并计算其在 Y 轴和深度上的 Tile 覆盖边界。
  • TileRangeExpansionJob:接收上一步计算的范围,将其展开并合并。它为每个 Tile 生成 uint 位掩码 (tileMasks)。掩码上的每一“位(bit)”代表该 Tile 是否受到某盏特定局部光的影响。

3. 跳过传统逐物体光照索引

传统的前向渲染 (Forward) 会在 CPU 端计算每一个物体具体受哪几盏灯光影响,并把这些索引写进一个 LightIndexMap。

在 ForwardLights.cs 的 SetupPerObjectLightIndices 函数中可以看到:

if (lightData.additionalLightsCount == 0 || m_UseForwardPlus)
    return lightData.additionalLightsCount;

如果启用了 Forward+,引擎直接跳过了生成 GetLightIndexMap 的逻辑。这正是 Forward+ 的核心优势——它将光照分配问题从“逐物体 (Per-Object)”转移到了“屏幕空间分块 (Per-Tile)”上,解决了多光源下由于物体交错导致的光照上限和 CPU 性能瓶颈。

4. GPU 数据上传与宏切换

渲染命令真正执行前(SetupLights 函数中):

  • 首先调用 m_CullingHandle.Complete() 等待多线程分块剔除任务跑完。
  • 使用 SetData 把 CPU 算好的 m_ZBins 和 m_TileMasks 数组全量压入 GPU 的 Constant Buffer(在 Shader 中体现为 _ZBinBuffer 和 _TileBuffer)。
  • 通过 CommandBuffer 为全局启用 Shader 变体宏。主要关键字为 _CLUSTER_LIGHT_LOOP(旧版兼容标记为 _FORWARD_PLUS)。

URP 的 Forward+ 实现将场景的光照管理变成了类似计算着色器(Compute Shader)中的 Tile-based Light Culling 逻辑,但是它是利用 CPU (Unity Job System + Burst Compiler) 来并行完成灯光视锥体求交的,然后将生成的 Tile 掩码和深度 Bin 以 Constant Buffer 形式提交给 GPU。最后,片段着色器 (Fragment Shader) 只需要根据当前像素的屏幕坐标和深度,查表获取当前 Tile 中受影响的灯光 Mask,只循环处理有影响的光源即可,从而大幅提升了多局部光源场景的渲染性能。

在 Forward+ 实现中,没有使用 Compute Shader (.compute) 来做灯光的裁剪与分块 (Clustering)。它的分块算法(Tile 与 Z-Bin 划分)纯粹是靠 CPU 端的 C# Job System(配合 Burst 编译器)来完成的,正如我们在上一步中看到的 TilingJob 和 TileRangeExpansionJob。

虽然没有使用 Compute Shader 做剔除,但在渲染着色器 (Rendering Shaders) 这一侧,Forward+ 的读取逻辑十分巧妙,这套机制类似于一些现代引擎中通过 Compute Shader 写入的数据结构。以下是渲染 Shader 端如何配合 Forward+ 工作的深度解析:

1. 宏开关:_CLUSTER_LIGHT_LOOP

几乎所有的 URP 标准着色器(如 Lit.shader,ComplexLit.shader,植被和粒子 Shader 等)都包含下面这句变体声明:

#pragma multi_compile _ _CLUSTER_LIGHT_LOOP

当 C# 端启用了 Forward+,就会全局开启这个宏,从而改变 Shader 内部遍历额外光源(Additional Lights)的方式。

2. 核心迭代器:Clustering.hlsl

在 Shader 端解析 Forward+ 数据结构的核心逻辑存放在 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Clustering.hlsl 中。这里定义了一个非常高效的 ClusterIterator:

A. 空间映射定位

在 ClusterInit 函数中,Shader 需要知道当前正在渲染的像素属于哪个 Tile 和哪个 Z-Bin:

  • 求 Tile Index:根据当前像素的屏幕空间 UV (normalizedScreenSpaceUV) 乘以 Tile 的横纵数量,直接算出当前的 2D 索引。
  • 求 Z-Bin Index:根据当前片元在观察空间 (View Space) 中的深度值 viewZ,映射出一个对数深度的索引 zBinIndex。

B. 数据查表与位运算掩码

CPU 传过来的灯光数据存在全局 Buffer 里面 (urp_Tiles 和 urp_ZBins),每个灯光分配占据掩码中的一位(1 bit)。 Shader 中将 Tile 的位掩码和 Z-Bin 的位掩码进行**按位与(Bitwise AND)**操作:

it.tileMask = Select4(asuint(urp_Tiles[tileWordIndex / 4]), ...) & 
              Select4(asuint(urp_ZBins[zBinWordIndex / 4]), ...);

得出的 tileMask 中,只要为 1 的位,就代表这盏灯光既投影到了当前像素的 2D 屏幕块内,也落在了当前像素深度的切片范围内,意味着该灯光确实照亮了当前像素。

C. 高效遍历 (ClusterNext)

得到掩码后,Shader 利用内置硬件指令 firstbitlow() 快速找到掩码中最低位的 1(即下一个影响该像素的灯光索引):

uint bitIndex = firstbitlow(it.tileMask);
it.tileMask ^= (1u << bitIndex); // 清除该位,准备找下一盏灯
entityIndex = ... + bitIndex;

这一步完全避免了无效循环。

3. 光照计算循环 (RealtimeLights.hlsl / Lighting.hlsl)

在传统的前向渲染中,片元着色器 (Fragment Shader) 是通过直接循环当前物体的灯光列表来计算光照的:

// 传统做法(伪代码)
int pixelLightCount = GetAdditionalLightsCount();
for (int i = 0; i < pixelLightCount; ++i) {
    Light light = GetAdditionalLight(i, positionWS);
    // ... 累加光照 ...
}

但在 Forward+ 模式(定义了 USE_CLUSTER_LIGHT_LOOP)下,上面的代码会被替换为:

// Forward+ 做法(伪代码)
ClusterIterator it = ClusterInit(screenUV, positionWS, 0);
uint lightIndex;
while (ClusterNext(it, lightIndex)) {
    Light light = GetAdditionalLight(lightIndex, positionWS);
    // ... 累加光照 ...
}

在 URP 的 Forward+ 架构中,引擎刻意避开了在 GPU 端使用 Compute Shader 做剔除。这是因为:

  1. 多端兼容性考量: Compute Shader 的支持在低端移动端或部分 WebGL 平台上依然有限。
  2. 利用多核 CPU 的闲置算力:Unity Job System 和 Burst 的性能极高,可以将视锥体剔除工作并行分发给闲置的 CPU 核心,而 GPU 专心做渲染。

最终的形态是:CPU (Burst) 负责灯光分块 -> 将 Mask 编码成 Constant Buffer (UBO) 发送 -> 片段着色器 (Fragment Shader) 查表解码掩码 -> 仅针对有效光源进行渲染循环。这套系统既拿到了 Forward+ 的性能收益,又保持了极好的向下兼容性。