一、前向渲染回顾
你在 DX11 笔记里画三角形的流程是前向渲染(Forward Rendering):
对场景中每个物体: 对每个光源: 跑一次像素着色器(光照计算)物体 A(100 个三角形) → VS → PS: 光源1 + 光源2 + 光源3 = 300 次 PS物体 B(50 个三角形) → VS → PS: 光源1 + 光源2 + 光源3 = 150 次 PS问题是:场景有 N 个物体、M 个光源时,PS 执行次数 = N × M。而且很多像素计算最终因为深度测试被覆盖了——白白算了。
你竞拍游戏的 3D 藏馆如果只有两三盏灯,前向渲染完全够用。但如果是开放世界夜市场景有上百盏灯笼,前向渲染就炸了。
二、延迟渲染:先存属性,后算光
1 核心思路
把渲染拆成两趟:
Pass 1(GBuffer Pass):渲染所有物体的几何属性到多张纹理 → 不计算任何光照 → 输出:GBuffer(一组 Render Target)
Pass 2(Lighting Pass):对屏幕上的每个像素,用 GBuffer 里的属性 + 所有光源,计算最终颜色 → 这是一个全屏四边形(Fullscreen Quad)+ 一个 Pixel Shader2 GBuffer 里存什么
UE 的 GBuffer 布局(简化版):
| GBuffer | 存储内容 | 格式 |
|---|---|---|
| A | BaseColor + AO | RGBA8 |
| B | Metallic + Specular + Roughness | RGBA8 |
| C | World Normal(世界空间法线) | RGBA16F(需要高精度) |
| D | 自定义数据(ClearCoat、Subsurface 等) | RGBA8 |
| Depth/Stencil | 深度缓冲 | D24S8 |
5 张纹理,每张和屏幕一样大。 GBuffer 的显存占用是 1920×1080 × 5 × 4 bytes ≈ 40MB。不高——但对移动端可能已经是预算瓶颈。
3 Lighting Pass 的优势
每个像素只需要跑一次像素着色器(不是 N×M 次)。在这一次里,遍历所有光源:
float3 GBuffer_Decode(Texture2D GBufferA, Texture2D GBufferB, ...) { float3 BaseColor = GBufferA.Sample(Sampler, UV).rgb; float Metallic = GBufferB.Sample(Sampler, UV).r; float Roughness = GBufferB.Sample(Sampler, UV).g; float3 Normal = GBufferC.Sample(Sampler, UV).rgb; float Depth = SceneDepth.Sample(Sampler, UV).r; // 从 Depth 重建世界空间位置 float3 WorldPos = ReconstructWorldPos(UV, Depth); return PBR_DirectLight(Normal, CameraDir, LightDir, ...);}
// 对屏幕上的每个像素(全屏 Quad):float3 color = GBuffer_Decode(...);
for (每个光源) { color += CalcLight(GBufferData, Light);}关键收益:光照计算只发生在最终可见的像素上。被深度测试淘汰的像素(被挡住的物体),根本不会进 Lighting Pass。前向渲染可能为一个最终被挡住的物体跑了全套光照计算,都浪费了。
三、延迟渲染的代价
1 半透明物体不兼容
GBuffer 一个像素只能存一个面的属性。半透明物体需要混合多个面——不能只靠 GBuffer。UE 的解决:半透明走独立的前向渲染 Pass。
当一个画面同时有不透明和半透明物体时,引擎实际上在跑两套管线。
2 材质多样性受限
因为 GBuffer 格式是固定的(BaseColor + Metallic + Roughness + Normal),无法为每种特殊材质(皮肤、头发、布料)定制额外的属性存储。Engine 的 Shading Model 枚举本质上就是”你选哪种解码 GBuffer 的方式”——UE 支持 DefaultLit、ClearCoat、Subsurface 等几种,但无法无限扩展。
3 MSAA 不兼容
延迟渲染无法使用硬件 MSAA(因为 GBuffer 是多张纹理,硬件 MSAA 是按像素为单位工作的,不好对多张纹理同时做 resolve)。UE 统一用 TAA(时间抗锯齿)。
4 带宽
1920×1080 的 GBuffer 写入(5 张纹理 × 每个像素 16-32 bytes ≈ 40-80MB 每帧)在高端 PC 上问题不大,但在移动端的 Tile-Based GPU 上——把这么多数据写出到显存再读回——是巨大的带宽浪费。这就是移动端普遍不用延迟渲染的原因。
四、前向+(Forward+):取长补短
1 为什么要有 Forward+
移动端没法接受 GBuffer 的带宽。能不能保留前向渲染的简洁性,同时解决”每个物体遍历所有光源”的问题?
Forward+ 的做法:在渲染前,先把屏幕切成 Tile(如 16×16 像素的格子),对每个 Tile 预计算”哪些光源影响这一 Tile”:
CPU/Compute Shader: 对每个 Tile: → 用 Tile 的深度范围构建一个子视锥体 → 检查哪些光源的包围球和这个子视锥体相交 → 输出:每个 Tile 的光源列表(Light Grid)
前向渲染: → 每个像素执行时,只遍历自己 Tile 的光源列表 → 而不是全局光源列表和前向渲染的对比:
- 传统 Forward:每个像素 × 全局光源列表(100 个光源全跑一遍)
- Forward+:每个像素 × 本 Tile 光源列表(通常 5-10 个光源)
和延迟渲染的对比:
- Deferred:GBuffer(大带宽)+ 一个全屏 Lighting Pass
- Forward+:无 GBuffer(低带宽)+ 每个像素只跑自己 Tile 的光
Forward+ 是移动端多光源场景的标配方案(因为避免了 GBuffer 的大带宽写入)。
2 UE 的 Forward Shading
UE 支持 Forward Rendering 模式(在 Project Settings 中切换 Forward Shading)。在这个模式下:
- 不存储完整 GBuffer
- 支持 MSAA(移动端 VR 的硬需求)
- 牺牲多光源支持,换取更低的显存带宽
五、Tile-Based GPU:硬件层面也分 Tile
移动端 GPU 本身就是 Tile-Based 架构:
桌面端 Immediate Mode GPU: → 整个帧存在 VRAM,逐 Primitive 渲染 → GBuffer 的 40-80MB 读写问题不大
移动端 Tile-Based GPU: → 屏幕被硬件分成 Tile(如 32×32 像素) → 每个 Tile 的数据在片上 SRAM 里处理,结束后才写回 VRAM → 片上 SRAM 非常快(TBDR 架构),但非常小(128-256KB) → 如果 GBuffer 是 5 张 × 32×32 × 4 bytes = 20KB per Tile,勉强能装下 → 如果 GBuffer 更复杂(7-8 张),一个 Tile 就溢出了 → 必须 spill 到 VRAM → 性能暴跌这就是为什么引擎要有两套管线:桌面端用 Deferred(接受 GBuffer 带宽),移动端用 Forward+(尊重 TBDR 架构)。不只是一套 Shader 代码换平台编译——是整套渲染管线的设计思路都不同。
六、三套管线的对比总结
| Forward | Deferred(UE 默认) | Forward+(TBDR) | |
|---|---|---|---|
| 光照复杂度 | O(N×M) | O(P×M)(P=像素数) | O(P×M_tile) |
| 半透明 | 天然支持 | 单独 Forward Pass | 天然支持 |
| MSAA | 支持 | 不支持(用 TAA) | 支持 |
| 显存带宽 | 低 | 高(GBuffer 读写) | 中等 |
| 材质多样性 | 任意 Shader | 受 GBuffer 格式限制 | 任意 Shader |
| 适用场景 | 简单场景、VR | 复杂场景多光源(桌面端) | 移动端、VR |
| UE 中怎么选 | Project Settings → Forward Shading | 默认 | Project Settings → Forward Shading(移动平台) |
七、总结
| 概念 | 一句话 |
|---|---|
| 前向渲染 | 每个物体 × 每个光源——直接但光照多了就炸 |
| GBuffer | 屏幕大小的多张纹理——存 BaseColor+Normal+Roughness+Metallic |
| 延迟渲染 | 先存属性、再算光——光照 O(像素×光源) 但牺牲了半透明和 MSAA |
| Forward+ | 用 Tile 预筛选光源——保留前向的优点,接近延迟的光照效率 |
| TBDR | 移动端硬件就是 Tile 架构——GBuffer 带宽是大忌 |
| UE 的选择 | 桌面端默认 Deferred,移动端/VR 推荐 Forward |
选哪套管线不是宗教问题——是数学问题。GBuffer 的带宽省了光照计算的重复、MSAA 的兼容性被 GBuffer 吃掉、移动端的 TBDR 又反过来限制 GBuffer 的规模。理解了这些 trade-off,你才知道什么时候该切 Forward Shading。