Base-前向渲染与延迟渲染

渲染管线中,光照计算阶段的组织方式决定了渲染器的整体架构。前向渲染(Forward)和延迟渲染(Deferred)是两种最主流的方案,它们各自衍生出一系列变体。

本文梳理它们的核心思路、演进分支、以及在不同场景下的取舍。


1. Forward Rendering(前向渲染)

1.1 基本原理

前向渲染是最直观的渲染方式:遍历每个物体 → 对该物体计算所有影响它的光源 → 得到最终颜色 → 写入 Framebuffer

伪代码逻辑:

1
2
3
4
for each object:
for each light affecting this object:
shading += calculate_lighting(object, light)
write shading to framebuffer

每渲染一个物体,就要把所有光源照一遍。场景中 N 个物体 × M 个光源 = O(N × M) 的复杂度。

1.2 优点

  • 实现简单直观
  • 透明渲染原生支持 — 一个 Pass 完成所有计算,Alpha Blend 直接工作
  • MSAA 友好 — 逐样本计算的颜色在光栅化阶段自然抗锯齿
  • 带宽低 — 只需要当前物体的数据,不需要读写 G-Buffer
  • 硬件兼容性好 — 不需要 MRT 等高级特性

1.3 缺点

  • 光源数量受限 — 每物体需要对所有光源逐一计算,性能随光源数线性增长
  • 无效计算多 — 即便物体只受极少数光源影响,也可能要遍历全部光源(虽然有 per-object light culling 优化,但效率有限)
  • 复杂度 O(N × M) — N(物体数)和 M(光源数)任何一个增长都会带来性能悬崖

1.4 传统 Forward 的优化思路

为了避免 O(N × M) 的失控,传统做法是限制光源数:

  • 严格限制每物体影响光源数量(通常 4~8 个)
  • 超出部分通过低精度的 Lightmap / SH(球谐光照)作为间接补充
  • 依赖引擎的 per-object light culling(如 Unity 在前向渲染中按物体裁减光源列表)

但这些优化只能延迟瓶颈,不能从根本上突破 O(N × M) 的约束。


2. Forward+ Rendering

Forward+(Tiled Forward Rendering)是对传统 Forward 的重要改进,将复杂度从 O(N × M) 降为 O(N + M × TileCount)

2.1 核心思想

把屏幕划分为多个 Tile(网格块,通常 16×16 或 32×32 像素),在渲染前先用 Compute Shader 计算每个 Tile 内实际影响该区域的光源列表。渲染物体时,Shader 通过屏幕坐标查询当前像素所在 Tile 的光源列表,只计算列表中的光源。

1
2
3
4
5
6
7
8
9
10
// CPU or Compute Shader 阶段:逐 Tile 求光源影响关系
for each tile:
tile_light_list[tile] = frustum_cull(lights, tile)

// 渲染阶段:每个物体通过屏幕坐标查光源
for each object:
for each fragment:
tile = get_tile(fragment.screenPos)
for light in tile_light_list[tile]:
shading += calculate_lighting(fragment, light)

2.2 关键改进

  • 共享了光源裁减的结果 — 同一 Tile 内的所有像素共享同一个光源列表
  • 不再需要 per-object light culling,计算量更稳定
  • 光源数量可以大幅提升(数十到上百个),只要在同一 Tile 内的光源数可控

2.3 缺点

  • Z 轴浪费 — Tile 是 2D 划分的,不区分深度。一个 Tile 中可能同时包含近处和极远处的光源,它们在深度方向上互不影响,但 2D Tile 无法区分,导致光源列表过重
  • 仍需逐物体渲染 — 和传统 Forward 一样,物体数多时仍然有 CPU 提交压力
  • 透明物体仍需处理

3. Clustered Forward+(Clustered Forward Rendering)

3.1 核心思想

在 Forward+ 的基础上再加一个维度:将视锥体在 Z 方向(深度方向)也分层,形成 3D 空间中的 Cluster(簇)。每个物体/像素通过屏幕坐标 + 深度值确定自己属于哪个 Cluster,只查询该 Cluster 内的光源。

1
2
3
4
5
6
7
8
// 预处理阶段:将视锥划分为 (TileX × TileY × SliceZ) 个 Cluster
// 每个 Cluster 有该空间范围内的光源列表

// 渲染阶段
for each fragment:
cluster = get_cluster(fragment.screenPos, fragment.depth)
for light in cluster_light_list[cluster]:
shading += calculate_lighting(fragment, light)

Cluster 划分示意:

1
2
3
4
5
6
7
8
9
10
视锥体侧视图:
近平面
┌────┬────┬────┬────┐
│ │ │ │ │ Z-slice 1
├────┼────┼────┼────┤
│ │ │ │ │ Z-slice 2
├────┼────┼────┼────┤
│ │ │ │ │ Z-slice 3
└────┴────┴────┴────┘
Tile X 方向 →

Z 轴分层通常不是均匀划分的,而是按 log-depth(对数深度) 或指数方式划分,使近处的 Cluster 更密集(因为近处的光源对视差更敏感),远处的 Cluster 更稀疏。

3.2 解决了什么问题

  • 消除了 Forward+ 的 Z 轴浪费 — 不再把远处的光源和近处的放在同一个列表里
  • 同样场景下,每个像素要计算的光源数更少
  • 支持的光源上限进一步上升(可达数百个动态光源)

3.3 代价

  • 需要更多的 GPU 内存来存储 3D Cluster 的光源索引
  • Compute Shader 预处理逻辑更复杂
  • 仍然是逐物体渲染,不减少 DrawCall 数

4. Deferred Rendering(延迟渲染)

4.1 基本原理

延迟渲染的核心思路是把光照计算推迟到所有物体都画完以后,分为两个 Pass:

  • Pass 1(Geometry Pass / G-Buffer Pass):将场景的几何信息逐像素写入多个 Render Target(MRT)。常见 G-Buffer 包含:

    • 法线、Albedo(漫反射颜色)
    • 金属度、粗糙度/光泽度
    • 深度
    • 运动向量、Subsurface Mask 等(按需求扩展)
  • Pass 2(Lighting Pass / Shading Pass):读取 G-Buffer,对每个像素计算所有影响它的光照。

1
2
3
4
5
6
7
8
9
// Pass 1: 几何信息写入
for each opaque object:
write gbuffer: normal, albedo, roughness, metallic, depth, ...

// Pass 2: 逐像素光照计算
for each light:
for each pixel covered by this light:
read gbuffer(pixel)
shading += calculate_lighting(gbuffer, light)

复杂度:O(N × M’) — N 是物体数(仅 Pass 1),M’ 是实际参与计算的光源 × 受影响的像素。物体数和光源数解耦。

4.2 优点

  • 光源数不受限 — 因为光照 Pass 是在屏幕空间逐像素做的,渲染 N 个光源的代价 ≈ 每光源一个全屏或局部光照 Pass
  • 物体数与光源数解耦 — Pass 1 只关心有多少物体要画,Pass 2 只关心有多少光源要算
  • 过度绘制少 — 每个像素只计算一次光照,不会有 Forward 中同一像素被多个半透明物体覆盖时多次计算的问题

4.3 缺点

  • G-Buffer 带宽大 — MRT 写 4~6 张 RT,带宽和内存消耗远高于 Forward
  • 不支持 MSAA — G-Buffer 存的是逐像素信息,MSAA 的逐样本处理在延迟渲染中非常棘手(虽然可以结合 MSAA 做,但实现复杂且效果差)
  • 透明渲染不原生支持 — 透明物体无法在 G-Buffer 中表达(需要额外的 Forward Pass 兜底)
  • 硬件要求高 — 需要 MRT(Multiple Render Targets)、较高显存带宽
  • 难以处理 Subsurface、Clear Coat 等复杂材质 — G-Buffer 只能存有限的信息,复杂材质需要额外的编码/压缩技巧

5. Deferred 分类与变体

5.1 Classic Deferred(标准延迟渲染 / Deferred Shading)

  • 上述标准的两 Pass 架构
  • G-Buffer 中存储所有着色需要的信息(法线、Albedo、材质参数)
  • Lighting Pass 一次性计算出最终颜色
  • 最早出现在《杀戮地带 2》的 Mark 论文中
  • 代表:UE4 Deferred、Unity URP/HDRP Deferred

5.2 Deferred Lighting(Light Pre-Pass / 延迟光照)

也称为 Deferred LightingLight Pre-Pass,将 G-Buffer 拆成了两个阶段:

  • Pass 1(Light Pre-Pass):只写深度和法线
  • Pass 2(Light Accumulation Pass):用深度+法线计算光照,输出光照缓冲
  • Pass 3(Material Pass):读光照缓冲 + 材质属性计算最终颜色

优点:G-Buffer 更小(只需要深度+法线),带宽节省约 50%。材质属性在最后 Pass 才读取,可以支持更多的材质种类,不受固定 G-Buffer 编码限制。

缺点:需要三个 Pass,Light Accumulation Pass 和 Material Pass 之间需要额外的上下文切换。

5.3 Tile-based Deferred(TBDR)

结合 Forward+ 思路的延迟渲染:

  • 将屏幕分为 Tile
  • 在 Lighting Pass 前,用 Compute Shader 逐 Tile 裁减光源
  • Lighting Pass 按 Tile 调度,只处理影响当前 Tile 的光源

适合移动端 GPU(如 PowerVR TBDR),利用 Tile-based 架构的 Local Memory 避免多次带宽消耗。

5.4 Clustered Deferred

类似 Clustered Forward+ 的思路应用于 Deferred:

  • 在 Z 方向分层 + 屏幕 Tile,构成 3D Cluster
  • 每个 Cluster 有独立的光源列表
  • Lighting Pass 逐像素查询所在 Cluster 的光源

5.5 变体对比总览

变体 G-Buffer Passes 光源 Culling 带宽 透明支持 MSAA
Classic Deferred 1 需 Forward 兜底
Deferred Lighting 2(法线+光照+材质) 需 Forward 兜底
Tile Deferred 1 2D Tile 需 Forward 兜底
Clustered Deferred 1 3D Cluster 需 Forward 兜底

6. 整体对比:Forward vs Deferred

6.1 核心维度

维度 Forward Forward+ Clustered Forward+ Deferred
光照计算时机 物体绘制时 物体绘制时 物体绘制时 物体绘制后
复杂度 O(N × M) O(N + M × Tile) O(N + M × Cluster) O(N + M × Pixel)
光源上限 受限(~4-8/物体) 中等(~数十) 高(~数百) 几乎无限
DrawCall 数 高(逐物体) 高(逐物体) 高(逐物体) 低(逐光源 Pass)
物体数上限 视 CPU 开销 视 CPU 开销 视 CPU 开销 仅受 G-Buffer 带宽

6.2 功能维度

维度 Forward Forward+ / Cluster Forward+ Deferred
MSAA ✅ 原生支持 ✅ 原生支持 ❌ 复杂 / 不支持
透明渲染 ✅ 原生支持 ✅ 原生支持 ❌ 需 Forward 兜底
复杂材质 ✅ 灵活 ✅ 灵活 ⚠️ 受 G-Buffer 编码限制
带宽
半透明 Overdraw 严重 严重 极少(不透明部分)

6.3 场景适用性

场景 推荐方案 原因
移动端(GPU 带宽受限) Forward / Forward+ 带宽低,MSAA 有用
PC 端多光源场景 Deferred / Clustered Deferred 光源数不受限
主机端(高画质) Clustered Forward+ 混合 Deferred 兼顾光源数和 MSAA
VR(需要 MSAA + 高帧率) Clustered Forward+ MSAA 刚需
大量半透明物体 Forward 为主,Deferred 兜底透明 Forward 支持透明混合
复杂材质(SSS、Clear Coat) Forward / Clustered Forward+ 不受 G-Buffer 编码限制

7. 现代引擎的混合策略

很少有引擎只用一种方案。常见的是 混合 Forward + Deferred 的策略:

1
2
3
4
渲染流程示例:
1. 不透明物体 → Deferred Shading(多光源高效)
2. 透明物体 → Forward Rendering(透明混合)
3. 特殊材质 → Forward Rendering(SSS、Clear Coat 等 G-Buffer 难以表达的材质)

Unity URP 在 Deferred 模式下就是这样的混合管线:不透明物体走 G-Buffer + Lighting Pass,透明物体退回到 Forward。HDRP 更是支持在同一个相机中按 Material 级别切换 Forward / Deferred。


8. 总结

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
┌─────────────────────────────┐
│ Forward 家族 │
│ ┌─────────────────────┐ │
│ │ Forward │ │ O(N × M), 简单, MSAA ✓
│ ├─────────────────────┤ │
│ │ Forward+ │ │ + Tile Culling
│ ├─────────────────────┤ │
│ │ Clustered Forward+│ │ + Z 方向分层
│ └─────────────────────┘ │
└─────────────────────────────┘

┌─────────────────────────────┐
│ Deferred 家族 │
│ ┌─────────────────────┐ │
│ │ Classic Deferred │ │ G-Buffer → Lighting Pass
│ ├─────────────────────┤ │
│ │ Deferred Lighting│ │ Light Pre-Pass(分拆法线)
│ ├─────────────────────┤ │
│ │ Tile Deferred │ │ + 2D Tile Culling
│ ├─────────────────────┤ │
│ │ Clustered Deferred│ │ + Z 方向分层
│ └─────────────────────┘ │
└─────────────────────────────┘

关键选择依据:
- 光源多 → 走 Deferred
- MSAA 刚需 / 复杂材质 / 透明多 → 走 Forward
- 现代方案:两者混合

Forward 演进路线:传统 Forward(逐物体逐光源)→ Forward+(+2D Tile Culling)→ Clustered Forward+(+Z 方向 Cluster)。每次演进都是增加空间剖分的精度,减少无效光照计算。

Deferred 演进路线:Classic Deferred → Deferred Lighting(分拆法线减少带宽)→ Tile/Clustered Deferred(+空间 Culling)。演进方向同样是减少带宽浪费和提升光源管理效率。

两种方案没有绝对的优劣——选择取决于场景特征和硬件目标。移动端受带宽限制时 Forward+ 往往更好,PC/主机多光源场景 Deferred 更有优势,而 VR 因 MSAA 刚需通常倾向 Clustered Forward+。


参考

  • 《Real-Time Rendering》4th Edition, Chapters 19-20
  • Forward+: 基于 Tile 的光源裁减方案
  • Clustered Deferred and Forward: Ola Olsson 等人的论文
  • Unity URP / HDRP 官方文档
  • UE4 Deferred Shading 文档