深入解析 Forward+ 渲染(Forward Plus / Tiled Forward Shading):原理、Compute Shader 实践与工程优化

深入解析 Forward+ 渲染(Forward Plus / Tiled Forward Shading)

随着现代实时渲染引擎中场景光源数量从几十个激增至成千上万个,传统正向渲染(Forward Rendering)和延迟渲染(Deferred Shading)的局限性逐渐显现。Forward+ Shading(又称 Tiled Forward Shading)由 AMD 工程师 Takahiro Harada 等人提出,它结合了 Tile-Based 光照裁剪正向渲染管线 的双重优势,在支持海量动态光源的同时,保留了材质多样性与硬件抗锯齿(MSAA)等重要特性。

本文将从 Tiled Deferred 演进脉络Forward+ 管线架构Compute Shader 光照裁剪性能对比工程挑战与现代演进 进行深度解构。


一、 演进脉络:从 Tiled-based Deferred 到 Forward+

在探讨 Forward+ 之前,有必要回顾 Intel 在 SIGGRAPH Courses 2010 中提出的 Tiled-based Deferred Shading 算法。

1. Tiled-based Deferred Shading 回顾

传统延迟渲染(Deferred Shading)需要为每个光源绘制一个几何体代理(Light Volume Proxy Mesh,如球体或锥体)来决定受光覆盖的屏幕区域。而 Intel 提出的 Tiled-based 方法将其优化为一个单 Pass 的 Compute Shader 裁剪过程:

  1. 生成 G-Buffer:与传统 Deferred Shading 一样,首先渲染场景几何体,生成存储位置、法线、材质等属性的 G-Buffer 附件。
  2. 屏幕 Tile 划分:将 G-Buffer 划分为许多小块(如 \(16 imes 16\) 像素的 Tile),并根据 Depth Buffer 得到每个 Tile 内像素的最大/最小深度值(Min/Max Depth),从而构建出该 Tile 的视锥体包围盒(Tile Bounding Frustum)。
  3. Tile 级光源裁剪(Light Culling):对每个 Tile,将其包围盒与场景中的光源进行求交测试,筛选出对当前 Tile 产生光照贡献的光源列表(Light Index List)。
  4. 像素级 Shading 累加:对 G-Buffer 中的每个像素,直接读取所在 Tile 的光源列表,累加计算光照。

优点:彻底摆脱了为每个光源提交几何体代理(Light Volumes)的绘制开销(Draw Call),在单个 Compute Shader Pass 中即可高效完成上千光源的求交与光照累加。如果结合 OIT(顺序无关半透明)链表技术,还能进一步优化光照索引存储。


2. Forward+ 的范式转换

有了 Tiled-based Deferred Shading 的基础,理解 Forward+ 的思想就变得十分顺理成章。Forward+ 与 Tiled-based Deferred 的关系,正如传统 Forward Shading 与 Deferred Shading 的关系:

核心变动:Forward+ 丢弃了体积庞大、极耗显存带宽的 G-Buffer,将最后的光照累加过程重新放回标准的正向着色器(Forward Fragment Shader)中执行。


二、 Forward+ 渲染管线三阶段

Forward+ 的标准管线主要包含以下三个核心步骤:

1
2
3
4
5
+------------------+     +-------------------------------+     +------------------+
| Pass 1: Depth | | Pass 2: Tile Light Culling | | Pass 3: Forward |
| Prepass |====>| (Compute Shader) |====>| Shading |
| (Writes Depth Z) | | (Build Tile Light Index List)| | (Read Light List)|
+------------------+ +-------------------------------+ +------------------+

1. 第一阶段:深度预 Pass (Z-Prepass)

在许多传统 Forward Shading 中,Z-Prepass 常被作为优化手段(提前剔除不可见片元)。而在 Forward+ 中,Z-Prepass 变成了必不可少的关键步骤: * 核心作用:它填充了深度缓冲区(Depth Buffer/Z-Buffer),提供每个像素的精确深度数据。只有有了完整的 Z-Buffer,第二阶段的 Compute Shader 才能提取出每个 Tile 的 Min/Max 深度边界。

2. 第二阶段:Tile 划分与光源裁剪 (Light Culling)

该阶段完全在 Compute Shader 中并行执行: 1. 网格切割:将 Z-Buffer 划分为 \(16 imes 16\) 像素网格(Tile)。 2. 深度极值归并:计算该 Tile 内部所有像素的最大深度 \(Z_{ ext{max}}\) 和最小深度 \(Z_{ ext{min}}\)。 3. 创建视锥包围体:利用屏幕 Tile 坐标与 \([Z_{ ext{min}}, Z_{ ext{max}}]\) 构建出视锥裁剪体(Tile Frustum AABB)。 4. 求交测试:将场景中的点光源/聚光灯包围球与该 Tile 视锥体求交。 5. 索引写出:将与当前 Tile 相交的光源索引写入全局显存缓冲区(SSBO / StructuredBuffer)中,形成按 Tile 索引的光源列表。

3. 第三阶段:正向着色阶段 (Forward Shading Pass)

在标准正向绘制 Pass 中: * 逐个绘制场景物体的几何体。 * 片元着色器(Fragment Shader)利用当前像素的屏幕坐标算出其所属的 Tile ID。 * 从 SSBO 中读取该 Tile 对应的光源索引列表,循环累加所有有效光源的光照贡献。


三、 技术特性对比:Forward+ vs Deferred vs Traditional Forward

Forward+ 兼顾了正向渲染的材质灵活性与延迟渲染的多光源扩展性:

评估维度 传统正向 (Forward) 延迟渲染 (Deferred) Forward+ (Tiled Forward)
显存带宽 极低 极高 (至少需 32bit Color + 32bit Depth G-Buffer) 较低 (仅需 Depth Prepass 与帧缓冲)
多光源扩展 极差 (受限于着色器变体与多 Pass) 强 (光照解耦) 极强 (凭借 Compute Shader Cache 索引)
半透明渲染 原生完美支持 无法原生支持 (需正向 Pass 补漏) 原生/扩展支持 (可结合深度范围优化)
硬件 MSAA 原生完美支持 无法原生使用 (耗显存/依赖后处理) 原生完美支持
复杂材质支持 极其灵活 (Shader 可自由编写) 受限 (须将材质参数编码入 G-Buffer) 极其灵活 (支持复杂 BRDF 与动态分支)

带宽效率提升:Forward+ 无需频繁读写体积庞大的 G-Buffer,因此在显存带宽受限的硬件(如移动端 GPU 及中低端 PC 显卡)上,Forward+ 的实际渲染速度往往能超越传统 Deferred Shading。


四、 核心代码实现

1. Compute Shader 光照裁剪示例 (GLSL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#version 450
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

// 深度纹理输入
layout(binding = 0) uniform sampler2D depthMap;

struct PointLight {
vec4 position; // xyz: 位置, w: 半径
vec4 color; // rgb: 颜色, w: 强度
};

layout(std430, binding = 1) buffer LightBuffer {
PointLight lights[];
};

// 输出:每个 Tile 的光源索引列表
layout(std430, binding = 2) buffer TileLightList {
uint tileLightIndices[]; // 结构: [Tile_Count * MAX_LIGHTS_PER_TILE]
};

shared uint minDepthInt;
shared uint maxDepthInt;
shared uint visibleLightCount;
shared uint visibleLightIndices[1024];

uniform mat4 inverseProjection;
uniform vec2 screenSize;

void main() {
ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy);
ivec2 tileID = ivec2(gl_WorkGroupID.xy);
uint localIndex = gl_LocalInvocationIndex;

// 1. 初始化共享内存
if (localIndex == 0) {
minDepthInt = 0xFFFFFFFF;
maxDepthInt = 0;
visibleLightCount = 0;
}
barrier();

// 2. 归并计算当前 Tile 的 Min/Max Depth
vec2 uvs = vec2(pixelCoord) / screenSize;
float depth = texture(depthMap, uvs).r;
uint depthInt = floatBitsToUint(depth);

atomicMin(minDepthInt, depthInt);
atomicMax(maxDepthInt, depthInt);
barrier();

// 3. 由 localIndex 0 计算 Tile 包围视锥体 (Frustum Planes)
float minDepth = uintBitsToFloat(minDepthInt);
float maxDepth = uintBitsToFloat(maxDepthInt);

// 4. 并行求交测试 (每个线程测试一部分光源)
uint totalLights = lights.length();
for (uint i = localIndex; i < totalLights; i += 256) {
vec3 lightPos = lights[i].position.xyz;
float radius = lights[i].position.w;

// 与 Tile Frustum 进行球体/平面求交测试
if (sphereIntersectsTileFrustum(lightPos, radius, tileID, minDepth, maxDepth)) {
uint index = atomicAdd(visibleLightCount, 1);
if (index < 1024) {
visibleLightIndices[index] = i;
}
}
}
barrier();

// 5. 将当前 Tile 的裁剪结果写入全局 SSBO
uint tileFlatIndex = tileID.y * (uint(screenSize.x) / 16) + tileID.x;
uint offset = tileFlatIndex * 1024;

for (uint i = localIndex; i < visibleLightCount; i += 256) {
tileLightIndices[offset + i] = visibleLightIndices[i];
}
}

2. Forward 片元着色器光照计算示例 (GLSL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#version 450
layout(location = 0) out vec4 FragColor;

layout(location = 0) in vec2 inTexCoord;
layout(location = 1) in vec3 inWorldPos;
layout(location = 2) in vec3 inNormal;

// 读取由 Compute Shader 生成的光源索引数据
layout(std430, binding = 2) buffer TileLightList {
uint tileLightIndices[];
};

uniform vec2 screenSize;
uniform vec3 viewPos;

void main() {
// 根据当前像素坐标计算所在的 Tile ID
ivec2 tileID = ivec2(gl_FragCoord.xy) / 16;
uint tileFlatIndex = tileID.y * (uint(screenSize.x) / 16) + tileID.x;
uint offset = tileFlatIndex * 1024;

vec3 N = normalize(inNormal);
vec3 V = normalize(viewPos - inWorldPos);
vec3 totalLighting = vec3(0.0);

// 遍历当前 Tile 缓存的光源索引进行累计计算
// (假定 tileLightIndices 的前几个 Byte 存储了有效光源总数)
uint lightCount = tileLightIndices[offset];
for (uint i = 0; i < lightCount; ++i) {
uint lightIdx = tileLightIndices[offset + 1 + i];

// 读取光源属性并计算 Blinn-Phong 或 PBR 光照
totalLighting += computePointLight(lightIdx, N, inWorldPos, V);
}

FragColor = vec4(totalLighting, 1.0);
}

五、 工程注意事项与进阶优化策略

在实际引擎落地开发中, Forward+ 还需要针对特定渲染特效与空间划分进行针对性设计:

1. 透明物体的渲染策略 (Transparency Handling)

由于透明物体不写入深度预 Pass(Z-prepass),常规根据不透明物体 Z-Buffer 计算出来的 Tile Min/Max Depth 会产生偏窄的深度区间。如果透明物体位于不透明物体的前方,其所在位置的光源可能在 Compute Shader 阶段被误剔除。

主流解决方案: * 双 Z-Buffer 法 (Dual Z-Buffer):在第一步生成 Z-prepass 时维护两个 Z-Buffer,一个记录不透明物体的深度 Z,另一个记录透明物体的最近深度 Z。在第二步计算 Tile 包围盒时,将两者的深度范围合并为一个总的 Bounding Box,后续光照累加步骤保持不变即可原生兼容透明物体。 * 多光源列表分级 (Separate Light Lists):为不透明物体与半透明物体分别维护两份 Light List。透明物体的 Min Depth 直接强制拉近至相机近裁剪面 \(Z_{ ext{near}}\),从而确保前方光源不遗漏。

2. 屏幕空间特效 (Screen-Space Effects) 的融合

在延迟渲染中,由于 G-Buffer 原生存有世界空间法线与位置,SSAO、SSR(屏幕空间反射)、GI(全局光照)等特效可以无缝调用。

而在 Forward+ 中: * 深度重建位置/法线:可以直接通过深度图(Depth Buffer)实时重构视空间位置,并通过深度梯度求偏导反算法线(或增加一个轻量级的 Depth-Normal Prepass)。 * 直接应用:有了完整深度图后,SSAO、SSVO、SSR 等屏幕空间效果均可以顺畅集成到 Forward+ 管线中。


3. 现代架构演进:Clustered Forward+ (集群前向渲染)

虽然 Forward+ 在 2D Tile 层面大大减少了光源遍历,但它依然面临 2D 深度跨度过大(Depth Range Problem) 的缺陷: 例如在漫长走廊视角下,一个 \(16 imes 16\) 的 Tile 内部可能同时包含距离相机 1 米的视锥近端墙壁与距离相机 100 米的远端背景,导致该 Tile 的 \([Z_{ ext{min}}, Z_{ ext{max}}]\) 极其庞大。这会使得很多处于中间空旷区域、实际上并不影响任何像素的光源被误判为“相交”,降低了剔除效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-----------------------------------------------------------------+
| Clustered Forward+ |
| |
| +-------+-------+-------+ |
| | Tile | Tile | Tile | <--- 屏幕 2D 切片 (X, Y) |
| +-------+-------+-------+ |
| / / / |
| / / / <--- 沿 Z 轴深度切片 (Depth Slices) |
| +-------+-------+ |
| / Cluster / / |
| +-------+-------+ |
| |
| 打破 2D 限制,将空间切分为 3D 体素栅格 (X x Y x Z Clusters) |
+-----------------------------------------------------------------+

Clustered Forward+ 在 Forward+ 的基础上,不仅在屏幕 2D 方向切割 Tile,还在 Z 轴深度方向进行对数/指数切片,将视锥体划分为 3D 栅格体素(Clusters)。Compute Shader 将光源裁剪直接与 3D Cluster 求交,彻底消除了深度跨度造成的误判,是目前现代主流引擎(如 UE5 Forward Shading、DOOM Eternal 等)的最佳实时光照方案之一。


六、 总结

Forward+ 渲染通过引入 Compute Shader 的 Tile 光照裁剪机制,成功解耦了前向渲染中光源数量与几何体数量的强绑定关系: * 它避免了传统 Deferred Shading 的高显存带宽与 G-Buffer 限制,使得多光源渲染管线拥有更高的执行效率; * 保留了对半透明物体、复杂材质与硬件 MSAA 的完美支持; * 为后续演进至 Clustered Forward+ 奠定了重要的工程框架基础。

本文作者:Berg Zha

本文链接:http://junglemanpro.com/2026/06/06/forward_plus_shading/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索