实时渲染管线探析 · 第 一 篇

延迟渲染:从原理到实现的性能权衡

2026-09-1011,229 字91 张配图本文由@溯光 出品,转载请注明出处

站点链接:技术博客页

题图
题图题图:Sponza 宫殿场景——图形学实时渲染/延迟渲染的经典基准测试场景(Sponza atrium, glTF Sample Models@ Khronos Group)。来源许可见文末表格。

系列文章前言

系列定位

本系列逐层拆解实时渲染管线(Real-Time Rendering Pipeline)的关键环节,面向有一定图形学基础、想真正读懂引擎实现细节的开发者。写作初衷很直接:国内缺少一套既能讲清原理、又能落到代码与数据的渲染管线长文,而散落的英文教程与会议 PPT 门槛偏高。把零散笔记整理成体系,既是给读者一条捷径,也是对我自己知识结构的一次清算。

系列文章写作风格说明

  • 篇幅规划:每篇约 8,000 字,每个技术章节约 900 字。
  • 写作顺序:按真实管线的执行顺序推进——几何处理 → 光栅化 → 着色 → 后处理。
  • 全部结论尽量绑定数据;凡引用他人成果,均给出可点击出处。

全系列知识体系

实时渲染管线知识体系
图 1-1本系列知识体系总览(v1.0,随内容深入持续更新)@ 本系列绘制

全系列内容总览

  1. 【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
  2. 【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进
  3. 【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿
  4. 【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践
  5. 【实时渲染管线探析】(五) 可见性与剔除:从视锥到 GPU-Driven

背景铺垫

实时渲染(Real-Time Rendering)中,光照计算(Lighting)几乎总是性能开销的头号来源。当场景里的动态光源从十几盏涨到上百盏,传统的前向渲染(Forward Rendering)会迅速撞上性能墙——而这,正是延迟渲染(Deferred Shading)要解决的问题。

光栅化渲染的实际画面
图 1-2光栅化渲染(Rasterization)的实际画面,Blender EEVEE 渲染器@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)

先给出"效果"这一层的直观印象——本系列讨论的所有管线优化,最终都是为了让这类画面在可控的性能预算内稳定跑起来。离开效果谈管线,容易陷入参数堆砌;而只看效果不问管线,又会在美术需求变更时无从下手。

本文的范围边界需要先说清楚:只讨论不透明几何的延迟着色主链路,不涉及移动端 Tile-Based GPU 的片上内存细节,也不展开光线追踪与全局光照(那是另一个系列的话题)。参数取值范围会随结论一并给出,不做无依据的推荐。

本文的范围边界
图 1-3本文的范围边界:聚焦不透明几何的延迟着色主链路@ 本系列绘制

术语层面先做一次对齐,后文统一使用简称,避免中英混用造成歧义:

关键术语中英对照
图 1-4关键术语的中英对照与本系列简称约定@ 本系列绘制

预判一个常见疑问:既然延迟渲染听起来处处占优,为什么不是所有引擎都改用它?答案在第六、七章——它有"三座大山",且优势只在特定规模区间成立。带着这个问题读下去,会比带着"新技术必胜"的预设更有收获。

为了便于随时对照,这里先给出一张总表——后文各章会逐项展开其中的每一行:

前向渲染与延迟渲染的核心差异一览
图 1-5前向渲染与延迟渲染的核心差异一览@ 本系列绘制

有了这张表作为索引,后面遇到任何一项差异,都可以回到这里确认它在整体结构中的位置。技术文章常见的困境是"细节记住了、结构没建立",这张表就是用来解决这个问题的。

前向渲染的多光源瓶颈
图 1-6前向渲染(Forward Rendering)中同一物体需对每盏光源重复计算@ 本系列绘制

OK,言归正传,下面我们开始正文。

一、前向渲染的复杂度瓶颈

1.1 执行模型

好了,我们先从执行模型说起。前向渲染的模型可以一句话概括:逐物体、逐光源(per-object, per-light)。对场景中每个物体,都要把它受到影响的每一盏光源完整算一遍。

这套模型的代价随"物体数 × 光源数"增长。说白了,一盏光源哪怕只照到屏幕上一个像素,也会参与该物体的整个着色计算——大量算力被浪费在几乎不可见的贡献上。

前向渲染的逐物体逐光源执行
图 1-7前向渲染(Forward Rendering)的双层循环结构@ 本系列绘制

图 逐盏布光的动态演示:主光→辅光→轮廓光逐盏点亮,直观对应"逐光源"思想 @ Wikimedia Commons · GDallimore(Public domain,H.264 MP4 全浏览器兼容)

这种双层循环带来的浪费,最直观的体现是重复着色(Overdraw):同一屏幕像素被多层几何反复覆盖,每次覆盖都要完整跑一遍片元着色器,而只有最后一遍的结果留在屏幕上。

重复着色的产生过程
图 1-8重复着色(Overdraw)的产生过程:像素被反复覆盖,只有最前层留存@ 本系列绘制

在深度复杂度高的场景(森林、城市街道)里,Overdraw 系数常达到 2–3 倍——这意味着前向渲染有近半的片元着色被白白丢弃。延迟渲染天然规避了这个问题:几何阶段不跑光照,光照阶段每像素只跑一次。

不同场景的浪费率差异很大,下面这组估算值可以帮你判断自己项目的处境:

深度复杂度对前向渲染浪费率的影响
图 1-9深度复杂度对前向渲染(Forward Rendering)片元浪费率的影响@ 本系列绘制

可以看到,场景越复杂,前向渲染浪费得越多——这恰好也是延迟渲染收益最大的场合。反过来说,如果你的场景以平坦地面、天空盒为主,前向渲染的浪费率接近零,此时上延迟渲染就是纯粹的额外开销。

1.2 三大具体痛点

前向渲染的瓶颈不止"次数多"这一条,实际工程中它表现为三种具体形态:

  1. 重复着色:同一屏幕像素被多层几何反复覆盖,片元着色器(Fragment Shader)跑了多遍,只有最后一遍留在屏幕上。
  2. 光源浪费:光源的影响范围与会话几何不一致时,被照不到的物体也要参与循环。
  3. 状态切换:为每个光源单独设置 uniform 或常量缓冲,CPU 侧提交开销随光源数线性上升。
前向渲染的三大痛点
图 1-10前向渲染(Forward Rendering)的三大具体痛点@ 本系列绘制

官方管线文档把这套流程拆成顶点着色 → 光栅化 → 像素着色 → 输出合并四个阶段,前向渲染把光照全部压在"像素着色"这一步里:https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

1.3 复杂度对比

下面我们开始把上面的直觉写成公式。前向渲染的片元着色次数可以写成:

$$ \text{ShadingCount}_{forward} = \sum_{o \in Objects} \sum_{l \in Lights(o)} \text{Fragments}(o) $$

其中: - $Objects$ 是场景中的物体集合。 - $Lights(o)$ 是影响物体 o 的光源集合。 - $\text{Fragments}(o)$ 是物体 o 覆盖的屏幕片元数。

延迟渲染把公式换成:

$$ \text{ShadingCount}_{deferred} = \sum_{p \in Pixels} |Lights(p)| $$

其中: - $Pixels$ 是屏幕像素集合(每个像素只着色一次)。 - $|Lights(p)|$ 是影响该像素的光源数。

关键差异:前向的求和项里带着"物体数",延迟把它彻底消掉了——这就是后者的结构性优势。复杂度对比可参考 ogldev 延迟着色系列第一篇对 G-Buffer 与 MRT 的推导:https://ogldev.org/www/tutorial35/tutorial35.html

值得展开的是这个"消掉"到底意味着什么。前向渲染的代价随场景复杂度双向增长:物体变多要涨,光源变多也要涨;而延迟渲染的代价与物体数完全无关,只随屏幕分辨率与光源数变化。这个性质带来一个很实际的工程好处——性能可预测性。做前向渲染时,美术往场景里加一批道具就可能让某个区域掉帧,且难以提前预估;做延迟渲染时,只要分辨率和光源数不变,开销基本恒定,优化目标非常明确。

反过来说,这个性质也正是延迟渲染的局限所在:它对"屏幕像素"敏感,因此分辨率提升的代价是平方级放大的(详见第六章带宽部分);而在这一点上,前向渲染反而更有弹性。

前向与延迟的复杂度对比
图 1-11前向渲染(Forward Rendering)与延迟渲染(Deferred Shading)的复杂度对比@ 本系列绘制

1.4 什么时候它其实够用

必须承认,前向渲染并非"过时技术"。当光源数少(<10)、场景深度复杂度低、或需要大量透明与多样材质时,前向渲染的实现简单性与灵活性反而是优势。这也是后来的 Forward+ 方案能在工业界站稳脚跟的原因——它保留了前向的材质灵活性,只把"光源列表"这一步做得更聪明。

前向与延迟的适用场景划分
图 1-12前向渲染(Forward Rendering)与延迟渲染(Deferred Shading)的适用场景划分@ 本系列绘制

换句话说,选型的正确问法不是"哪个更先进",而是"我的约束落在哪个区间"。这个问题会在第八章给出完整的判断顺序。

好了,我们正式开始 G-Buffer 部分的讲解。

二、G-Buffer:延迟渲染的数据中枢

2.1 核心思路

延迟渲染(Deferred Shading)的思路,是把"几何"和"光照"彻底拆成两个阶段:

  1. 几何阶段(Geometry Pass):只做光栅化,把每个像素的材质信息写入一组渲染目标,这组目标合称 G-Buffer(Geometry Buffer)。
  2. 光照阶段(Lighting Pass):以屏幕空间的全屏四边形为画布,逐像素读取 G-Buffer,再做光照积分。

这样一来,光照计算量只与"屏幕像素数 × 光源数"相关,与场景物体数彻底解耦。

延迟渲染两阶段数据流
图 1-13延迟渲染(Deferred Shading)的几何阶段与光照阶段数据流@ 本系列绘制

理论讲完,先看一张真实的可视化素材——延迟着色被拆成各阶段之后,实际呈现是什么样子:

延迟着色各阶段的可视化
图 1-14延迟着色(Deferred Shading)各阶段可视化:从几何阶段输出到光照合成结果@ Wikimedia Commons(CC BY-SA 3.0)

延迟着色的整体流程,一张图就能交代清楚:

延迟着色总览:几何 Pass 写 G-Buffer,光照 Pass 再逐像素着色
图 1-15延迟着色总览:几何 Pass 写 G-Buffer,光照 Pass 再逐像素着色@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Deferred-Shading

G-Buffer 里到底存了什么,可以直接看这张多通道可视化:

G-Buffer 各通道可视化:位置、法线、颜色、深度分别写入不同附件
图 1-16G-Buffer 各通道可视化:位置、法线、颜色、深度分别写入不同附件@ McMalloc(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Deferred_Shading_FBOs.jpg

这张图把延迟着色的各个阶段并列展示了出来——从几何阶段的原始输出,到 G-Buffer 的各个通道,再到光照合成后的结果。对照它来读后面的通道布局章节,会比只看示意图直观得多。

这个思路并非新近发明,GPU Gems 3 第 19 章就记录过 Tabula Rasa 项目的完整实践:https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-19-deferred-shading-tabula-rasa

2.2 通道布局设计

OK,言归正传,先看通道该怎么分配。G-Buffer 的通道分配是第一个要拍板的决策,它直接决定了显存占用与带宽(Bandwidth)成本。

一个常见的紧凑布局(约 20 字节/像素):

渲染目标 通道分配 内容
RT0 RGB / A 基础色(baseColor)/ 环境光遮蔽(AO)
RT1 RGB / A 世界空间法线(World Normal)/ 粗糙度(roughness)
RT2 RGB / A 自发光(emissive)/ 金属度(metallic)/ 标志位
G-Buffer 通道布局
图 1-17G-Buffer(Geometry Buffer)三张渲染目标的通道分配,合计 20 字节/像素@ 本系列绘制

这套"多渲染目标(Multiple Render Targets, MRT)"的写法,是把几何数据一次写完的关键。LearnOpenGL 的延迟渲染章节给出了完整的 OpenGL 实现,可作为对照:https://learnopengl.com/Advanced-Lighting/Deferred-Shading

真实项目里,几何阶段单独输出的 Color Pass 是这个样子:

延迟渲染的 Color Pass
图 1-18延迟渲染的 Color Pass 输出@ Wikimedia Commons(CC BY-SA 4.0)

注意它与最终画面之间的差别——Color Pass 里还没有任何光照信息,只有材质本身的颜色。光照是在后续的光照阶段才叠加进去的,这正是"延迟"二字的由来。

布局的字节数直接决定带宽成本,几种常见布局的对比:

G-Buffer 通道数与字节数
图 1-19G-Buffer(Geometry Buffer)不同布局的每像素字节数对比@ 本系列绘制

从 8 B/px 到 32 B/px 差了 4 倍——在 4K 分辨率下,这个差异会被放大到每帧数百 MB 的显存流量。

通道数量的抉择本质是一次权衡:

MRT 通道数与带宽的权衡
图 1-20MRT(Multiple Render Targets)通道数与带宽的权衡@ 本系列绘制

通道布局的具体排布是这样的:

G-Buffer 的通道布局:多个渲染目标(MRT)分别承载不同几何信息
图 1-21G-Buffer 的通道布局:多个渲染目标(MRT)分别承载不同几何信息@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Deferred-Shading

单看颜色通道,就是一张“没有光照”的画面:

延迟渲染的颜色通道(col):只含反照率,不含任何光照
图 1-22延迟渲染的颜色通道(col):只含反照率,不含任何光照@ astrofra(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Deferred_rendering_pass_col.jpg

法线通道里存的究竟是什么数据,可以看一张法线贴图——法线被编码成颜色:

法线贴图(切线空间):单位法线以 RGB 编码存储;G-Buffer 的法线通道存放的是同类数据(本文统一用世界空间)
图 1-23法线贴图(切线空间):单位法线以 RGB 编码存储;G-Buffer 的法线通道存放的是同类数据(本文统一用世界空间)@ JPBotelho(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3ANormal%20Map.png

这里没有普适最优解:桌面端通常倾向"通道多、信息全",移动端则必须优先压缩通道数。同一款游戏在两端采用不同的 G-Buffer 布局,是相当常见的工程实践。

2.3 精度分配:为什么法线必须高精度

通道布局里最容易踩的坑,是精度分配。不同数据的精度需求差异极大:

  • 世界空间法线(World Normal):必须用 16 位或 32 位浮点。8 位量化会带来可见的法线台阶,光照高光直接出现色块。
  • 基础色(baseColor):8 位足够,人眼对漫反射色彩的量化不敏感。
  • 粗糙度(roughness):8 位一般够用,但与金属度共用通道时要注意取值范围划分。
G-Buffer 精度分配对比
图 1-24不同数据的精度需求差异与常见分配方案@ 本系列绘制

法线的存储方案,则是精度与带宽之间最典型的权衡:

法线的两种存储方案
图 1-25世界空间法线的直存与八面体映射压缩方案对比@ 本系列绘制

除了编码方式,法线还存在一个"存哪个空间"的选择,它直接影响光照阶段的实现复杂度:

世界空间与观察空间法线的取舍
图 1-26世界空间与观察空间法线的取舍@ 本系列绘制

本系列后续统一采用世界空间法线:它能在光照阶段直接使用,代价是精度要求更高、通道更难压缩。这同样是取舍,而非对错。

把法线数据直接可视化出来,是排查 G-Buffer 问题最常用的手段——不同朝向的几何体会映射为不同颜色,一眼就能看出法线是否正确:

法线可视化
图 1-27法线(Normal)可视化:朝向映射为颜色,用于检查 G-Buffer 数据正确性@ Wikimedia Commons(CC0)

精度不足的后果,Godot 官方文档给了一张很直观的实例——位深不够时,渐变会被量化成可见的台阶:

精度不足导致色阶(banding):位深不够时渐变被量化成台阶,法线量化产生高光色块是同一机理
图 1-28精度不足导致色阶(banding):位深不够时渐变被量化成台阶,法线量化产生高光色块是同一机理@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_rendering_limitations.html

顺带提醒一个容易被忽略的细节:法线写入前必须归一化。插值后的法线长度会偏离单位长度,直接写入会让光照阶段的 N·L 计算出现偏差。相关细节可参考透视校正插值的原理:https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes.html

2.4 几何阶段:写入 G-Buffer

先看看几何阶段具体要做什么。几何阶段的像素着色器只负责"打包"材质信息,不做任何光照计算:

//-----------------------------------【G-Buffer 写入】---------------------------------------
// 描述:几何阶段的像素着色器,把材质信息打包进三张渲染目标
//------------------------------------------------------------------------------------------
struct GBufferOutput {
    float4 rt0 : SV_Target0;   // baseColor.rgb + AO
    float4 rt1 : SV_Target1;   // worldNormal.rgb + roughness
    float4 rt2 : SV_Target2;   // emissive + metallic
};

GBufferOutput GeometryPass(Varyings input) {
    GBufferOutput o;
    //【1】基础色与环境光遮蔽打包到 RT0
    o.rt0 = float4(SampleBaseColor(input), SampleAO(input));
    //【2】法线必须归一化后写入 RT1,否则光照阶段方向会失真
    o.rt1 = float4(normalize(input.worldNormal), SampleRoughness(input));
    //【3】自发光与金属度打包到 RT2
    o.rt2 = float4(SampleEmissive(input), SampleMetallic(input), 1.0);
    return o;
}

上面代码中,法线归一化是最容易被忽略的一步。

MRT 的绑定与 glDrawBuffers 调用顺序,ogldev 的教程讲得比官方文档更直观:https://ogldev.org/www/tutorial35/tutorial35.html

MRT 的绑定与写入顺序
图 1-29MRT(Multiple Render Targets)的绑定与写入顺序@ 本系列绘制

注意最后一步"显式声明绘制缓冲":省略它会让附件静默失效,G-Buffer 全是空白却不报错——这是 MRT 最常见的调试陷阱。

三、光照阶段:从数据到画面

3.1 渲染方程

下面进入光照阶段。它真正求解的,是经典的渲染方程(Rendering Equation):

$$ L_o(p, \omega_o) = \int_{\Omega} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i \, d\omega_i $$

其中: - $L_o$ 是 p 点沿出射方向 $\omega_o$ 的出射光亮度。 - $f_r$ 是双向反射分布函数(BRDF),描述入射到出射的反射比例。 - $L_i$ 是沿入射方向 $\omega_i$ 的入射光亮度。 - $\cos\theta_i$ 是入射方向与表面法线的夹角余弦。

工程实现里,这个积分通常被离散成对每盏光源的累加——也正因如此,光源数量才成为延迟渲染的主战场。

光照 Pass 的执行流程
图 1-30光照 Pass(Lighting Pass)的执行流程@ 本系列绘制

这里有个容易混淆的点值得强调:光照计算本身与前向渲染完全一致,变的只是"输入从哪来"——前向从顶点着色器插值得到,延迟则从 G-Buffer 采样得到。理解这一点,就不会把延迟渲染当成"另一套光照算法"。

3.2 逐像素累加

下面我们看这个积分在代码里怎么落地。光照阶段用全屏 Pass 遍历每个像素,读取 G-Buffer 后累加所有光源:

//-----------------------------------【延迟光照】---------------------------------------
// 描述:光照阶段逐像素读取 G-Buffer,累加所有光源的贡献
//------------------------------------------------------------------------------------------
float3 DeferredLighting(float2 uv) {
    //【1】从 G-Buffer 反解材质参数
    float4 rt0 = GBuffer0.Sample(sampler, uv);
    float4 rt1 = GBuffer1.Sample(sampler, uv);
    float3 albedo    = rt0.rgb;
    float3 N         = normalize(rt1.xyz);
    float  roughness = rt1.a;

    //【2】逐光源累加(迭代次数只由光源数决定,与物体数无关)
    float3 color = 0;
    for (uint i = 0; i < lightCount; ++i) {
        color += EvaluateBRDF(albedo, N, roughness, lights[i], uv);
    }
    return color;
}

上述代码实现了延迟渲染的核心收益:光照循环的迭代次数只由光源数决定,与屏幕上有多少物体无关。

这段循环的产物,就是下面这张多光源场景的最终画面——每个像素把若干光源的贡献用加法混合累加,形成立体的局部光照:

延迟着色逐像素累加得到的最终画面
图 1-31延迟着色(Deferred Shading)光照 Pass 的最终合成:多光源加法混合累加@ Joey de Vries · LearnOpenGL(CC BY 4.0)

逐光源累加的执行层次可以用一张图概括:

逐光源累加的执行层次
图 1-32逐光源累加的执行层次:像素 → 光源列表 → BRDF 求值 → 累加输出@ 本系列绘制

不同光源类型在延迟渲染里的适配方式并不相同——这直接决定了剔除策略能做多细:

常见光源类型与延迟渲染的适配
图 1-33常见光源类型在延迟渲染(Deferred Shading)中的适配方式@ 本系列绘制

光照 Pass 逐像素累加的结果长这样:

延迟着色光照 Pass 结果:多光源在屏幕空间逐像素累加
图 1-34延迟着色光照 Pass 结果:多光源在屏幕空间逐像素累加@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Deferred-Shading

平行光是个特例:它影响范围无限大,无法用包围体剔除,只能在光照阶段全屏处理。这也是"一个平行光 + 大量点光源"的组合在延迟渲染里特别常见的原因——前者负责整体基调,后者承担局部细节。

3.3 从 Blinn-Phong 到 PBR

OK,下面我们看真正在工业里跑的东西。真实的工业实现不会停在 Blinn-Phong。光照项 EvaluateBRDF 通常被替换为基于物理的 BRDF(Cook-Torrance):

$$ f_r = \frac{F \cdot D \cdot G}{4 (\omega_o \cdot n)(\omega_i \cdot n)} $$

其中: - $F$ 是菲涅尔项(Fresnel),描述掠射角反射增强。 - $D$ 是法线分布函数(NDF),决定高光形状。 - $G$ 是几何遮蔽项(Geometry),处理微表面自遮挡。

三项的具体形式与取舍,LearnOpenGL 的 PBR 章节与 selfshadow 的 SIGGRAPH 着色课程是两套互补的资料:https://learnopengl.com/PBR/Theory 与 https://blog.selfshadow.com/publications/s2013-shading-course/

Blinn-Phong 与 PBR 的着色差异
图 1-35传统 Blinn-Phong 与基于物理的渲染(PBR)在高光表现上的差异@ 本系列绘制

三项的分工可以这样理解:D 决定高光"长什么样",G 决定微表面"遮掉多少",F 决定掠射角"反多少"。工业实现里最常用的组合是 GGX 分布 + Smith 遮蔽 + Schlick 菲涅尔。

Cook-Torrance BRDF 的三个组成项
图 1-36Cook-Torrance BRDF 的 D/G/F 三项分工@ 本系列绘制

PBR 的实际效果是这样的——同一材质在环境光下呈现出的反射与粗糙度表现:

PBR 渲染的砖墙材质
图 1-37基于物理的渲染(PBR)呈现的砖墙材质,粗糙表面仍可见反射细节@ Wikimedia Commons(CC BY 4.0)

放大到微表面尺度,PBR 的价值更加明显——金属表面的划痕与磨损在掠射角下会被正确增强:

PBR 微表面细节
图 1-38PBR 的微表面细节:金属表面磨损在掠射角下被正确呈现@ Wikimedia Commons(CC BY 4.0)

顺带给一个方向指引:面积光(Area Light)的实时近似目前在工业界取得良好效果,LTC(Linearly Transformed Cosines)是其中代表,项目页有完整论文与 WebGL 演示:https://eheitzresearch.wordpress.com/757-2/

实际项目里怎么选这三项的搭配?主流组合的对照如下:

常用 BRDF 组合的取舍
图 1-39常用 BRDF 组合的取舍:GGX + Smith + Schlick 是当前工业主流@ 本系列绘制

在跨到 PBR 之前,先把传统模型看准——Phong 与 Blinn-Phong 的高光形状差异是这样的:

Phong 与 Blinn-Phong 的高光对比:半程向量的引入改变了高光的形状与范围
图 1-40Phong 与 Blinn-Phong 的高光对比:半程向量的引入改变了高光的形状与范围@ No machine-readable author provided. Rainwarrior~commonswiki assumed (based on copyright claims).(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3ABlinn%20phong%20comparison.png

Blinn-Phong 单独渲染出来是这样:

Blinn-Phong 着色实效:高光集中在半程向量与法线对齐的方向
图 1-41Blinn-Phong 着色实效:高光集中在半程向量与法线对齐的方向@ Davi.trip(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3ABlinn-phong%20shading.png

一句实操建议:除非有明确的美术风格需求,直接用 GGX + Smith + Schlick 即可——它是被验证最多、公开资料最全、美术工具链支持最好的组合。HLSL 侧的算子写法可参照官方语言说明:https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl

四、光源剔除与模板缓冲

4.1 为什么必须剔除

光照阶段的朴素实现是对每个像素遍历全部光源。但屏幕大部分区域只被少数光源照到,无差别遍历会浪费巨量算力。

剔除(Culling)的目标很明确:只对真正受影响的像素执行光照计算。主流做法有三类:

  1. 光源体积(Light Volume):把点光源当成球体渲染,只处理球体覆盖的像素。
  2. 模板缓冲(Stencil Buffer):用深度测试结果标记"真正在光体积内"的像素。
  3. 分块列表(Tiled / Clustered):把屏幕分块,预先算出每块的光源列表。

光源体积的大小不是拍脑袋定的,它由"衰减到可忽略"的阈值反推:

$$ r = \sqrt{\frac{I}{I_{\min}}} $$

其中: - $r$ 是光源包围球的半径。 - $I$ 是光源在单位距离上的强度。 - $I_{\min}$ 是可接受的最小照度阈值——低于它的区域直接不参与光照计算。

这个阈值取得越大,包围球越小、剔除越激进;但取得过大,远处会出现肉眼可见的光照"断层"。工程上一般从 1/256 的衰减比例起步调参。

光源剔除的三种策略
图 1-42光源体积、模板剔除与分块列表三种策略的对比@ 本系列绘制

三种策略的核心差异在于"筛选粒度",各自由此带来不同的收益与成本:

全屏遍历与光源体积的像素覆盖差异
图 1-43全屏遍历与光源体积剔除的像素覆盖差异@ 本系列绘制

光源体积之所以有效,关键在深度测试。下面是同一场景的两张对比:左边开启深度测试,光体积被几何正确遮挡,只有受光照的可见表面执行计算;右边关掉 GL_DEPTH_TEST,所有光体积穿透模型、相互叠加,画面立刻混乱——这直观回答了"为什么必须做剔除":

开启深度测试:光体积被几何正确遮挡
图 1-44开启深度测试:光源体积被模型正确遮挡,光照计算只在可见表面生效@ Joey de Vries · LearnOpenGL(CC BY 4.0)
关闭深度测试:光体积穿透、画面混乱
图 1-45关闭 GL_DEPTH_TEST:光源体积穿透几何体、相互叠加,暴露"不做剔除"的后果@ Joey de Vries · LearnOpenGL(CC BY 4.0)

Tiled 与 Clustered 则进一步把"逐光源筛选"变成"预先分配光源列表":

Tiled 与 Clustered 的剔除粒度差异
图 1-46Tiled(二维分块)与 Clustered(三维分簇)的剔除粒度差异@ 本系列绘制

光源体积(light volume)本身也可以被可视化出来:

光源体积可视化:每个光源的包围几何被单独渲染出来
图 1-47光源体积可视化:每个光源的包围几何被单独渲染出来@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Deferred-Shading

4.2 模板缓冲的妙用

好了,来聊聊模板缓冲这个更精细的做法。模板缓冲(Stencil Buffer)方案的巧妙之处,在于用一次"前后面深度测试差异"就完成了像素筛选。ogldev 的第三篇教程给出了完整实现,其核心是:对光源包围球,背面深度测试失败时自增、正面深度测试失败时自减,最终模板值大于零的像素,就是真正处在光体积内的像素。

完整实现在此:https://ogldev.org/www/tutorial37/tutorial37.html

四个步骤的执行顺序如下:

模板剔除的四个步骤
图 1-48模板缓冲(Stencil Buffer)剔除光源的四个步骤@ 本系列绘制

这套方案的代价是需要额外的模板 Pass,且在光源稀疏时收益有限——它最适合"光源密集、互相重叠"的场景。

两种剔除方式的成本对比:

光源体积与模板剔除的成本对比
图 1-49光源体积与模板剔除的成本对比@ 本系列绘制

而 Clustered 方案在实现上要多走几步,这四步也解释了它为什么更难调试:

Clustered 分簇的执行层次
图 1-50Clustered(三维分簇)的执行层次@ 本系列绘制

模板缓冲这套判定之所以成立,靠的是“阴影体”这个几何构造:

阴影体(shadow volume):由光源与遮挡物轮廓面向外延伸出的封闭体积
图 1-51阴影体(shadow volume):由光源与遮挡物轮廓面向外延伸出的封闭体积@ (CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AShadow%20volume%20illustration.png

depth-fail 是它的关键变体,模板值的增减规则正是正文描述的那样:

depth-fail 阴影体:正面深度测试失败时自减、背面失败时自增,两者抵消后模板值大于零即为受光像素
图 1-52depth-fail 阴影体:正面深度测试失败时自减、背面失败时自增,两者抵消后模板值大于零即为受光像素@ SamVandenBerghe(Public domain)· https://commons.wikimedia.org/wiki/File%3ADepth%20fail%20shadow%20volume.png

模板缓冲的典型用法,可以先用一个简单例子建立直觉:

模板缓冲的典型应用:先用模板筛出可写入的像素,再只对这些像素绘制——光源剔除用的是同一套机制
图 1-53模板缓冲的典型应用:先用模板筛出可写入的像素,再只对这些像素绘制——光源剔除用的是同一套机制@ Beuc(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File%3AOpenGL%20Tutorial%20Stencil.png

4.3 光照阶段的两条路径

工程上前向与延迟的对照,可以看 ogldev 第二篇对光照 Pass 的拆解(含稠密光源场景的性能对比):https://ogldev.org/www/tutorial36/tutorial36.html

光照阶段的三种实现路径
图 1-54逐光源遍历、光源体积与分块列表三种光照路径的开销对比@ 本系列绘制

而更彻底的解法是把剔除搬到 GPU——GPU-Driven Rendering Pipelines 的思路就是让 GPU 自主生成与剔除绘制指令,CPU 只做提交。这是本系列第五篇的主题,此处先埋个伏笔。

五、技术演进与方案对比

5.1 演进脉络

先来看看这条路是怎么一步步走过来的。把延迟渲染放回历史坐标里看,它的每一次转折都由"硬件约束"推动:

延迟渲染技术演进时间线
图 1-55延迟着色(Deferred Shading)技术演进时间线@ 本系列绘制

时间线(年份为"该方案被广泛采用/公开"的时间点,非首次提出时间):

  • 1970s–1980s:Catmull 等人提出深度缓冲(Z-Buffer)与可见性算法,为"先把几何存下来"奠定基础。[Catmull 1974]
  • 2004:[Hargreaves & Harris 2004] 在 GPU Gems 中系统提出延迟着色,用于解决多光源开销。
  • 2007:[Tabula Rasa 2007] 首次在商业项目中大规模落地,写入 GPU Gems 3 第 19 章。
  • 2009:[S.T.A.L.K.E.R. 2009] 在东欧引擎中把延迟着色与太阳阴影结合,成为一时范式。
  • 2012–2013:[Persson & Olsson 2013] 提出 Clustered Deferred and Forward Shading,把"分块"升级为"三维簇"。
  • 2016:[Uncharted 4 2016] 在 SIGGRAPH 课程中公开其延迟光照与 TAA 方案,《神秘海域 4》成为质量标杆。
  • 2018:实时光线追踪商用元年,行业开始讨论"延迟着色是否会被取代"。[NVIDIA 2018]

上面几个里程碑里,有两个是真正跑在当时的商业游戏里的。下面这张是东欧工作室在《S.T.A.L.K.E.R.》中把延迟光照与太阳阴影结合的画面——多光源场景能被高效点亮,正是延迟渲染在那个年代最有说服力的实证:

S.T.A.L.K.E.R. 中延迟光照的实际画面
图 1-56S.T.A.L.K.E.R.: Shadow of Chernobyl 引擎画面,延迟光照结合太阳阴影的多光源场景@ Wikimedia Commons · GSC Game World(GFDL)

从这条时间线里,可以提炼出三条对技术判断有用的结论:

从演进史得到的三条判断
图 1-57从延迟渲染演进史得到的三条判断@ 本系列绘制

这三条不止适用于延迟渲染——后处理链、阴影方案、抗锯齿技术的演进,几乎都遵循同样的模式:硬件约束变化触发方案迁移,旧方案以混合形态长期共存。相关一手资料可查 SIGGRAPH 2018 课程页:https://advances.realtimerendering.com/s2018/

5.2 全量方案清单

下面我们开始把方案列全。延迟渲染不是单一方案,而是一个方案族。把主流方案一次列全:

  1. 经典延迟着色(Deferred Shading):G-Buffer + 全屏光照 Pass。
  2. 延迟光照(Deferred Lighting,又称 Light Pre-Pass):只把法线与深度写入,光照在更晚阶段算。
  3. 延迟着色 + 光源体积剔除:用球体包围光源,减少无效像素。
  4. 延迟着色 + 模板缓冲剔除:用深度/模板测试精确筛像素。
  5. Tiled Deferred(分块延迟):屏幕切成二维 Tile,每块先算光源列表。
  6. Clustered Deferred(分簇延迟):把视锥切成三维簇,解决深度方向的光源分配不均。
  7. Forward+(前向增强):保留前向的材质路径,只把光源列表做成 Tiled/Clustered。
  8. Visibility Buffer(可见性缓冲):只存 primitive ID + 重心坐标,片元阶段重建属性,缓解带宽。
  9. Hybrid / 混合管线:不透明走延迟、透明走前向,工业界主流形态。

这张清单还可以按"演进层次"重排成一张族谱,四个层次对应四代思路:

延迟渲染方案族谱
图 1-58延迟渲染(Deferred Shading)方案族谱:从基础形态到信息压缩@ 本系列绘制

5.3 二次归类

上面 9 种方案,按剔除粒度归为三大流派:

  • 流派一 · 像素级剔除:方案 1–4。以单像素为单位决定是否光照,实现简单,但缺乏空间加速结构。
  • 流派二 · 分块/分簇级剔除:方案 5–7。以屏幕空间块或视锥簇为单位预计算光源列表,是目前的主流范式。
  • 流派三 · 几何信息压缩:方案 8–9。不在"剔除"上做文章,转而压缩几何信息的存储与带宽。

三个流派对应三种"省算力"的哲学——像素级靠精确判断,分块级靠空间预分配,压缩级靠少存少读:

九种方案按剔除粒度归为三流派
图 1-59九种延迟渲染方案按剔除粒度归为三大流派@ 本系列绘制

5.4 横向对比

评价维度需要提前声明:材质灵活性、光源扩展性、带宽开销、透明支持、实现复杂度。五维打分如下(●优 / ◐中 / ○差):

方案 材质灵活性 光源扩展性 带宽开销 透明支持 实现复杂度
经典延迟着色 ◐ ● ○ ○ ◐
延迟光照 ◐ ● ◐ ○ ◐
光源体积剔除 ◐ ● ○ ○ ●
模板缓冲剔除 ◐ ● ○ ○ ○
Tiled Deferred ◐ ● ◐ ○ ◐
Clustered Deferred ◐ ● ◐ ○ ○
Forward+ ● ● ● ● ◐
Visibility Buffer ● ◐ ● ◐ ○
Hybrid 混合 ● ◐ ◐ ● ○

两条结论:想兼顾材质灵活性与透明支持,Forward+ 与 Hybrid 是两个现实解;想在纯不透明重光源场景榨性能,Clustered Deferred 目前仍是效率上限。

评分口径汇总如下,便于对照查找:

横向对比表的评分口径
图 1-60横向对比表的评分口径与五个评价维度@ 本系列绘制

需要提醒的是,这张表是定性判断,具体项目的权重会因约束而异——如果你的项目里透明物体占比极高,那么"透明支持"这一列的权重应被显著上调。

5.5 复杂度与量级

把前面几代方案的复杂度并排写成公式,差距一眼可见:

$$ \text{Cost}_{Forward} = \sum_{o}\sum_{l} F(o), \quad \text{Cost}_{Deferred} = \sum_{p} |L(p)|, \quad \text{Cost}_{Clustered} = \sum_{c}\sum_{p \in c} |L(c)| $$

其中: - $F(o)$ 是物体 o 在屏幕上覆盖的片元数——它让前向渲染的代价与场景复杂度绑定。 - $|L(p)|$ 是影响像素 p 的光源数。 - $|L(c)|$ 是分配到簇 c 的光源数,通常远小于 $|L(p)|$——这一项正是 Clustered 的收益来源。

从实现成本看,各方案的代码量级差异同样显著:

各方案实现复杂度量级
图 1-61各延迟渲染方案的实现代码量级对比(相对值)@ 本系列绘制

一句话总结这段演进史:延迟渲染的每一次迭代,都在用更细的剔除粒度换更少的无效计算。相关工程细节可参考 SIGGRAPH 2013 的 Advances in Real-Time Rendering 课程页:https://advances.realtimerendering.com/s2013/

六、三大难题:透明、抗锯齿、带宽

延迟渲染并非没有代价。工业界的共识是它有"三座大山",绕不开,只能各想办法。

6.1 难题一:透明物体

问题:延迟渲染的核心假设是"每个像素只保留最前一层几何",而透明(Transparency)的本质是多层叠加。混合需要按深度顺序逐层累加,这与"单层 G-Buffer"直接冲突。

主流解法:

  1. 双 Pass 分离:不透明走延迟,透明走前向(Forward)。这是最常用也最稳的做法,代价是维护两套着色路径。
  2. 深度剥离(Depth Peeling):逐层剥离并累计,层数越多开销越大,实时场景一般不超 4 层。
  3. 加权混合(OIT,Order-Independent Transparency):用加权平均近似无序混合,牺牲物理正确性换性能。
透明物体的处理方案
图 1-62透明(Transparency)与延迟渲染单层假设的冲突及主流解法@ 本系列绘制

三种解法的成本与效果差异明显,选哪种取决于你的透明物体占比:

三种顺序无关透明方案的取舍
图 1-63深度剥离、加权混合与双 Pass 分离三种透明方案的取舍@ 本系列绘制

透明的视觉效果有多难做,看这张玻璃渲染就明白了——折射、反射与色彩偏移同时发生,任何一个环节出错都会露馅:

玻璃渲染中的透明效果
图 1-64玻璃(Glass)渲染中的折射与透明效果@ Wikimedia Commons · Morn(CC0)

透明排序出错的典型表现,Godot 官方文档给了一张实例:

透明排序伪影:混合顺序不对时,前后的透明面会被错误地叠加
图 1-65透明排序伪影:混合顺序不对时,前后的透明面会被错误地叠加@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/3d_rendering_limitations.html

6.2 难题二:抗锯齿

OK,言归正传,说第二个难题。问题:多重采样抗锯齿(MSAA)依赖"每像素保存多个采样点的深度与颜色"。G-Buffer 一旦按采样点存储,显存占用与带宽立刻乘以采样数——4× MSAA 意味着 4 倍的 G-Buffer 开销。这也是延迟渲染长期"不爱 MSAA"的根因。

主流解法:

  1. 后处理抗锯齿(TAA / FXAA):在图像空间做抗锯齿,与 G-Buffer 采样数解耦。TAA 复用历史帧,效果好但会引入拖影。
  2. 几何抗锯齿(FXAA / SMAA):纯图像空间,开销低,细节损失换性能。
  3. 混合 MSAA:只对边缘做多采样,成本可控但实现复杂。

掌握深度测试与模板测试的语义是理解抗锯齿方案的前提,LearnOpenGL 的对应章节讲得比规范更易读:https://learnopengl.com/Advanced-OpenGL/Depth-testing 与 https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing

MSAA 采样数对 G-Buffer 开销的放大关系如下:

MSAA 采样数对 G-Buffer 开销的放大
图 1-66多重采样抗锯齿(MSAA)采样数对 G-Buffer 开销的放大关系@ 本系列绘制

这组数据解释了一个行业现象:延迟渲染流行的那些年,恰恰也是 TAA 等后处理抗锯齿方案快速普及的时期——两者是配套出现的,而非巧合。

超采样(Supersampling)的抗锯齿原理可以这样理解——用更多采样点换取边缘平滑,代价是成倍的开销:

超采样抗锯齿原理
图 1-67超采样(Supersampling)抗锯齿的原理示意@ Wikimedia Commons · Hrkalona(CC BY-SA 4.0)

而质量设置对最终画面的影响,可以从这张同场景的低质量渲染中直观看到——边缘与细节会明显粗糙:

低质量渲染设置下的画面
图 1-68低质量渲染设置下的画面(与高质量设置为同一场景)@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)

6.3 难题三:带宽

好了,最后一座山是带宽。问题:延迟渲染省下了重复着色,却付出了 G-Buffer 的读写带宽。单帧的显存流量可以这样估算:

$$ \text{Bandwidth} = W \times H \times \text{BytesPerPixel} \times \text{FPS} $$

其中: - $W$、$H$ 是渲染分辨率的宽与高。 - $\text{BytesPerPixel}$ 是 G-Buffer 每像素字节数,本例为 20。 - $\text{FPS}$ 是目标帧率。

代入 1920×1080、60 FPS:单帧写入约 40 MB,读写合计约 79 MB,每秒流量超过 4.7 GB。这已经接近部分中端显卡显存带宽的可观比例。

G-Buffer 的带宽成本构成
图 1-69G-Buffer(Geometry Buffer)的每像素构成与单帧带宽成本@ 本系列绘制

带宽之所以是硬约束,根源在硬件本身——G-Buffer 的每一次读写都要经过显存,而显存控制器与计算单元被集成在同一块裸片上:

GPU 裸片实拍
图 1-70GPU 裸片(Die Shot):计算单元与显存控制器集成于同一芯片@ Wikimedia Commons · Martijn Boer(Public Domain)

主流解法:

  1. 通道压缩:法线用八面体映射(Octahedral Mapping)压到 2 通道,粗糙度与金属度合包,可把 20 字节压到 12–16 字节。
  2. 写入合并(MRT 一次写完):避免多次 Pass 重复读写。
  3. 可见性缓冲(Visibility Buffer):只存 primitive ID,把材质还原推迟到需要时。

压缩的收益可以量化:

G-Buffer 压缩前后的每像素字节
图 1-71G-Buffer 压缩前后的每像素字节数对比@ 本系列绘制

从 20 B 压到 14 B 意味着带宽直接下降 30%——在 4K/120Hz 这类高负载目标下,这往往是"能不能跑满"的分界线。

分辨率对带宽的影响比很多人预想的更剧烈,它是平方级关系:

分辨率对 G-Buffer 单帧带宽的影响
图 1-72分辨率对 G-Buffer 单帧带宽的影响(20 B/px 条件下)@ 本系列绘制

正因为带宽是硬约束,厂商干脆把显存搬到离计算单元最近的地方——HBM 就是为带宽而生的封装:

HBM 封装(AMD Fiji):GPU 裸片与显存堆栈同置中介层上,以超宽位宽换带宽
图 1-73HBM 封装(AMD Fiji):GPU 裸片与显存堆栈同置中介层上,以超宽位宽换带宽@ C. Spille/pcgameshardware.de(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3AAMD%20Fiji%20GPU%20package%20with%20GPU%2C%20HBM%20memory%20and%20interposer.jpg

移动端的思路相反:把 G-Buffer 留在片上,避免往返显存——这就是分块渲染:

分块渲染架构(TBDR):按屏幕分块,几何与着色都在片上内存完成,G-Buffer 不落显存
图 1-74分块渲染架构(TBDR):按屏幕分块,几何与着色都在片上内存完成,G-Buffer 不落显存@ Mewtow(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File%3ATiled%20rendering%20architecture.jpg

从 1080p 到 4K,单帧 G-Buffer 写入量涨了 4 倍。这也是为什么 4K 游戏在延迟渲染管线上往往需要额外做分辨率缩放或分块渲染——不这么做,带宽会成为第一瓶颈。

八面体法线映射与帧缓冲对象的实现细节,可参考:https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage.html 与 https://learnopengl.com/Advanced-OpenGL/Framebuffers

好了,我们正式开始工程实践部分的讲解。

七、工程实践与性能实测

7.1 基准数据

先说结论:延迟渲染的收益只在光源数足够多时才显著。以 1920×1080、G-Buffer 20 B/px 为基准,不同光源规模下的相对开销如下(相对值,1.0 表示前向渲染在 8 光源时的开销):

不同光源数下的相对开销
图 1-75前向渲染与延迟渲染在不同光源数下的相对开销趋势@ 本系列绘制

可以看到:光源数在 8 以下时,前向渲染更快(延迟的固定开销——G-Buffer 读写——压不过收益);约 32 光源后延迟才开始明显占优;到 256 光源时差距拉大到数倍。这也解释了为什么早期 3A 项目要等到"多光源成为常态"才大规模转向延迟。

这里的"32 光源"只是一个量级参考,实际拐点会随分辨率、G-Buffer 布局、GPU 架构而移动。给出这个数字的意义不在于精确套用,而在于建立一种判断意识:延迟渲染有一笔固定的"入场成本",光源规模必须超过它才划算。

如果你的项目正处在拐点附近(20–40 光源),最务实的做法是两种管线各做一个最小实现,用真实场景各测一遍——这个决定会贯穿整个项目的优化路径,值得花两天时间实测,而不是靠估算拍板。

7.2 优化手段清单

工程上可用的优化手段,按收益排序:

  1. 光源剔除:最直接的收益来源,通常能砍掉 60% 以上的无效光照计算。
  2. 通道压缩:把 20 B/px 压到 14 B/px,带宽直接下降 30%。
  3. 半分辨率光照:光照 Pass 在 1/2 分辨率执行再上采样,画质损失可控时收益接近翻倍。
  4. MRT 合并:减少 Render Target 数量,降低 MRT 切换与合并开销。
  5. 模板剔除:对密集小光源场景,可把光体积内的像素筛到 10% 以下。

半分辨率光照与降采样的取舍,可参考 SSAO 章节里"半分辨率计算 + 模糊重建"的通用思路:https://learnopengl.com/Advanced-Lighting/SSAO

环境光遮蔽(Ambient Occlusion, AO)是 G-Buffer 里最"省力"的一项——它用廉价的近似换来明显的立体感,且只需占用一个通道:

环境光遮蔽效果
图 1-76环境光遮蔽(Ambient Occlusion)带来的柔和阴影与接触感@ Wikimedia Commons · Philip Rideout(CC BY-SA 3.0)

在没有全局光照的实时管线下,AO 几乎是"廉价立体感"的唯一来源:

环境光遮蔽应用实例
图 1-77环境光遮蔽(Ambient Occlusion)在实际场景中的应用效果@ Wikimedia Commons(CC BY-SA 3.0)
全分辨率与半分辨率光照的取舍
图 1-78全分辨率光照与半分辨率光照的取舍@ 本系列绘制

优化的推荐执行顺序(按收益/成本比排序):

延迟渲染优化的推荐顺序
图 1-79延迟渲染优化的推荐执行顺序@ 本系列绘制

7.3 常见坑清单

延迟渲染的坑集中在"数据一致性"与"平台差异"两类:

  1. 法线未归一化 → 光照方向失真,高光位置偏移。
  2. 坐标系混用 → 世界空间与观察空间的法线混写,光照结果整体偏暗或偏亮。
  3. 线性空间与 sRGB 混用 → 基础色写入前未转线性空间,画面发灰或过曝。参考:https://learnopengl.com/Advanced-Lighting/Gamma-Correction
  4. 深度精度不足 → 远近平面比例失调导致 Z-Fighting,G-Buffer 里出现条纹噪点。参考:https://learnopengl.com/Advanced-OpenGL/Depth-testing
  5. 移动端 Tile 内存溢出 → G-Buffer 通道数超过片上内存容量,被迫回落主存,性能断崖。参考 Tile 架构下的可见性算法说明:https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/visibility-problem-depth-buffer-depth-interpolation.html
  6. HDR 与色调映射顺序错误 → 在延迟 Pass 里做了色调映射,后续叠加的透明物体无法正确合入。参考:https://learnopengl.com/Advanced-Lighting/HDR

这六个坑有一个共同特征:都不会报错,只会让画面"看起来不对"。这也是延迟渲染调试难度普遍高于前向渲染的根本原因——数据分散在多张渲染目标里,出错时往往要逐张 RT 单独可视化排查。

延迟渲染的六个常见坑
图 1-80延迟渲染(Deferred Shading)的六个常见工程坑@ 本系列绘制

针对这些坑,推荐一套固定的排查顺序——按"数据 → 空间 → 公式"逐层排除,比直接盯光照代码效率高得多:

延迟渲染问题的排查流程
图 1-81延迟渲染问题的推荐排查流程@ 本系列绘制

这套顺序的道理在于:延迟渲染的绝大多数"画面不对",根源都在数据本身,而非光照公式。先确认 G-Buffer 里存的东西是对的,再去怀疑后面的计算——顺序反了,会在错误的代码区域里绕很久。

7.4 一个最小可跑的实现路径

如果要亲手实现一遍,最小路径是:

  1. 建帧缓冲对象(FBO),挂 3 个颜色附件 + 1 个深度模板附件。参考:https://learnopengl.com/Advanced-OpenGL/Framebuffers
  2. 几何 Pass 写入 G-Buffer,确认每张 RT 的内容正确。
  3. 光照 Pass 用全屏四边形读取 G-Buffer,先实现单光源,再扩到多光源。
  4. 加光源体积剔除,最后加模板剔除。
  5. 用 SSAO 或 Bloom 验证 G-Buffer 数据的可用性。参考:https://learnopengl.com/Advanced-Lighting/Bloom 与 https://ogldev.org/www/tutorial45/tutorial45.html
最小实现路径
图 1-82延迟渲染的最小可跑实现路径(五步)@ 本系列绘制

八、选型建议与未来展望

8.1 决策建议

下面我们开始讲怎么选。选型不该凭"哪种更先进",而该看约束。给出一个可执行的判断顺序:

  1. 光源数是否长期 > 32 且以不透明几何为主? 是 → 考虑延迟;否 → 留在前向。
  2. 材质种类是否高度多样(皮肤、布料、次表面)? 是 → Forward+ 或 Hybrid;否 → 经典延迟。
  3. 是否需要大量透明与粒子? 是 → 必须保留前向路径;否 → 可纯延迟。
  4. 目标平台是否移动端 Tile 架构? 是 → 严格控制 G-Buffer 通道数,或直接放弃纯延迟。
  5. 是否追求 GPU 驱动的大规模剔除? 是 → 关注 Visibility Buffer 与 GPU-Driven 方案。

把上面五步串起来,就是一条可直接照做的决策流:

延迟渲染选型决策顺序
图 1-83延迟渲染(Deferred Shading)选型决策顺序@ 本系列绘制

8.2 未解挑战

延迟渲染仍有三件事没有让人满意的答案:

  1. 带宽墙:分辨率越高,G-Buffer 带宽越贵。4K 下 20 B/px 的代价已经是很多方案的瓶颈。可见性缓冲是当前最有希望的缓解路径之一。
  2. 材质多样性:G-Buffer 的固定通道布局天然偏向"统一材质模型"。异质材质(毛发、布料、次表面散射)要么塞进统一 BRDF,要么退回前向。
  3. 与光线追踪的共存:硬件光追商用后,"光栅化 G-Buffer + 光追阴影/反射"的混合管线成为新常态,但两者的几何表示并不统一,数据要在两套管线间来回搬运。

光追能带来什么,对比最直观。先看叠加了辐射度与光子映射的高质量结果:

光线追踪与光子映射的渲染结果
图 1-84光线追踪(Ray Tracing)+ 辐射度 + 光子映射的渲染结果@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)

再对照只使用简单光照的版本,差异主要体现在间接光与软阴影上:

简单光照下的光线追踪结果
图 1-85仅使用简单光照的光线追踪结果,用于对照质量差异@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)

而全局光照(Global Illumination)正是补上"间接光"的那一环,下面这张室内场景是它的典型应用:

全局光照渲染的室内场景
图 1-86全局光照(Global Illumination)渲染的室内场景,LuxRender 渲染器@ Wikimedia Commons · Niabot(CC BY-SA 3.0)
延迟渲染的未解挑战
图 1-87延迟渲染(Deferred Shading)当前面临的三个未解挑战@ 本系列绘制

面对这三重挑战,工业界当前的实际选择是"混合"而非"二选一":

工业界主流的混合管线分层
图 1-88工业界主流的混合管线分层:不透明延迟、透明前向、后处理统一收口@ 本系列绘制

这也是本系列的基本判断:未来不会存在"唯一的正确管线",而是按几何类型、材质类型、性能预算分层组合。理解每一层的边界,比记住某个方案的优劣更重要。

把视角再拉高一点,不同方向的工业实践其实已经给出了各自的答案:

工业界典型引擎的管线选择
图 1-89工业界不同方向的管线选择对照@ 本系列绘制

这张表没有优劣之分,只有约束之别——主机 3A 要兼顾多光源与高画质,移动端受 Tile 内存与带宽限制,独立项目追求实现成本,开放世界则要处理光源与几何的规模问题。选型的本质,始终是在约束下找最优解。

如果你想更深入,这条线索值得顺着往下走:GPU Gems 3 第三部分的渲染章节合集 https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering、SIGGRAPH 2016 的 Advances in Real-Time Rendering 课程页 https://advances.realtimerendering.com/s2016/、以及 selfshadow 逐年更新的基于物理着色课程 https://blog.selfshadow.com/publications/s2016-shading-course/。这三处是当前最密集的一手资料。


收尾图
图 1-90收尾:「A Beautiful Game」棋盘场景,由 Babylon.js 沙盒实时渲染(Poly Haven HDRI 环境光 + ACES 色调映射)@ Academy Software Foundation / MaterialX Project(CC BY 4.0)

参考与延伸

  • 图形管线总览:https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline
  • 顶点/几何/像素着色阶段:https://learn.microsoft.com/en-us/windows/win32/direct3d11/vertex-shader-stage 与 https://learn.microsoft.com/en-us/windows/win32/direct3d11/pixel-shader-stage
  • 光栅化器与输出合并阶段:https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-rasterizer-stage 与 https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage
  • 曲面细分与计算着色器:https://learn.microsoft.com/en-us/windows/win32/direct3d11/direct3d-11-advanced-stages-tessellation 与 https://learn.microsoft.com/en-us/windows/win32/direct3d11/direct3d-11-advanced-stages-compute-shader
  • D3D11 渲染技术总览与 D3D12 图形:https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-render 与 https://learn.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-graphics
  • HLSL 语言总览:https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl
  • 延迟着色入门(LearnOpenGL):https://learnopengl.com/Advanced-Lighting/Deferred-Shading
  • 帧缓冲对象:https://learnopengl.com/Advanced-OpenGL/Framebuffers
  • 模板测试与混合:https://learnopengl.com/Advanced-OpenGL/Stencil-testing 与 https://learnopengl.com/Advanced-OpenGL/Blending
  • 实例化渲染与立方体贴图:https://learnopengl.com/Advanced-OpenGL/Instancing 与 https://learnopengl.com/Advanced-OpenGL/Cubemaps
  • 法线映射与基础光照:https://learnopengl.com/Advanced-Lighting/Normal-Mapping 与 https://learnopengl.com/Lighting/Basic-lighting
  • 着色器基础:https://learnopengl.com/Getting-started/Shaders
  • 阴影贴图与级联阴影:https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping 与 https://learnopengl.com/Guest-Articles/2021/CSM
  • PBR 直接光照与 IBL:https://learnopengl.com/PBR/Lighting 与 https://learnopengl.com/PBR/IBL/Diffuse-irradiance
  • 延迟着色实现三部曲(ogldev):https://ogldev.org/www/tutorial35/tutorial35.html、https://ogldev.org/www/tutorial36/tutorial36.html、https://ogldev.org/www/tutorial37/tutorial37.html
  • 阴影与 SSAO 教程:https://ogldev.org/www/tutorial23/tutorial23.html、https://ogldev.org/www/tutorial46/tutorial46.html、https://ogldev.org/www/tutorial47/tutorial47.html
  • 几何着色阶段与 D3D12 编程指南:https://learn.microsoft.com/en-us/windows/win32/direct3d11/geometry-shader-stage 与 https://learn.microsoft.com/en-us/windows/win32/direct3d12/directx-12-programming-guide
  • 光栅化与着色原理(Scratchapixel):https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/overview-rasterization-algorithm.html 与 https://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-shading/diffuse-lambertian-shading.html
  • 投影阶段与法线着色:https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/projection-stage.html 与 https://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-shading/shading-normals.html
  • GPU Gems 3 延迟着色专章:https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-19-deferred-shading-tabula-rasa
  • SIGGRAPH 课程:https://advances.realtimerendering.com/s2013/、https://advances.realtimerendering.com/s2016/、https://advances.realtimerendering.com/s2018/
  • selfshadow 基于物理着色课程:https://blog.selfshadow.com/publications/s2012-shading-course/、https://blog.selfshadow.com/publications/s2014-shading-course/、https://blog.selfshadow.com/publications/s2015-shading-course/、https://blog.selfshadow.com/publications/s2017-shading-course/
  • 面积光着色(LTC)项目页:https://eheitzresearch.wordpress.com/757-2/

图片素材来源与许可

本篇共使用 37 张真实素材图(渲染结果 / 硬件实拍 / 官方文档配图)+ 54 张自绘示意图。素材图均标注来源(Wikimedia Commons / GitHub),许可与署名如下:

用途 作者 许可 来源页
题图 / Sponza 宫殿场景 Khronos Group (glTF Sample Models) 未标注明确许可(历史性场景,作者原始声明 donated to public / 学术使用);因用于技术说明引用,已尽力标注来源 Sponza atrium
收尾图 / 棋盘场景实时渲染 Academy Software Foundation / MaterialX Project(Ed Mackey 转换) CC BY 4.0 A Beautiful Game
带宽章节 / GPU 裸片 Martijn Boer 公有领域 R580+ 方幅
光栅化渲染(高/低质量) KaiaVintr CC BY-SA 4.0 Blender EEVEE
光追渲染(高/低质量) KaiaVintr CC BY-SA 4.0 POV-Ray
延迟着色各阶段可视化 Wikimedia Commons CC BY-SA 3.0 Deferred Shading FBOs
延迟渲染 Color Pass Wikimedia Commons CC BY-SA 4.0 Deferred rendering pass
法线可视化 Wikimedia Commons CC0 Normals
PBR 材质(砖墙 / 微表面) Wikimedia Commons CC BY 4.0 PBR Sample 1
玻璃折射渲染 Morn CC0 Glass scene
超采样原理 Hrkalona CC BY-SA 4.0 Supersampling
环境光遮蔽 Philip Rideout CC BY-SA 3.0 Ambient Occlusion
环境光遮蔽应用 Wikimedia Commons CC BY-SA 3.0 Ambient occlusion
全局光照室内场景 Niabot CC BY-SA 3.0 LuxRender
法线通道数据形态 JPBotelho CC BY-SA 4.0 File:Normal Map.png
精度不足的色阶表现 Godot Engine documentation CC BY 4.0 3d rendering limitations.html
Phong 与 Blinn-Phong 高光对比 Wikimedia Commons CC BY-SA 3.0 File:Blinn phong comparison.png
Blinn-Phong 着色实效 Davi.trip CC BY-SA 4.0 File:Blinn-phong shading.png
阴影体几何示意 Wikimedia Commons CC BY-SA 3.0 File:Shadow volume illustration.png
depth-fail 阴影体判定 SamVandenBerghe Public domain File:Depth fail shadow volume.png
模板缓冲的典型应用 Beuc CC BY-SA 3.0 File:OpenGL Tutorial Stencil.png
透明排序伪影 Godot Engine documentation CC BY 4.0 3d rendering limitations.html
HBM 封装(GPU + 显存同置) C. Spille / pcgameshardware.de CC BY-SA 4.0 File:AMD Fiji GPU package with GPU, HBM memo
分块渲染架构(TBDR) Mewtow CC BY-SA 4.0 File:Tiled rendering architecture.jpg
lo deferred overview(按文件登记) Joey de Vries CC BY 4.0 lo deferred overview
deferred fbos passes(按文件登记) McMalloc CC BY-SA 3.0 deferred fbos passes
lo gbuffer(按文件登记) Joey de Vries CC BY 4.0 lo gbuffer
deferred pass col(按文件登记) astrofra CC BY-SA 4.0 deferred pass col
lo deferred example(按文件登记) Joey de Vries CC BY 4.0 lo deferred example
lo light volume(按文件登记) Joey de Vries CC BY 4.0 lo light volume

许可合规说明:留用素材均允许商业使用,但 CC BY / CC BY-SA 系列要求保留署名,CC BY-SA 还要求对素材本身的修改以相同许可发布。本文字体不改动素材原图,仅在等比缩放后嵌入正文并完整署名,符合上述条款。若你需要更宽松的使用条件,可优先选用其中的 CC0 / 公有领域素材(法线、玻璃、GPU 裸片等)。其中题图 Sponza 场景为历史性基准测试场景,来源未标注明确商用许可,本处仅作为技术说明引用并尽力标注出处,商用前请另行向权利方确认。

  • 示意图:本系列自绘(ep01_*.svg)

相关链接 - 上一篇:{无(系列首篇)} - 下一篇(预告):【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进

With Best Wishes.