Skip to main content

Chapter 105: Deferred Rendering

Deferred Rendering 要解决的主问题,是在一帧里同时存在大量不透明物体、法线贴图材质和动态光源时,怎样把光照计算从“每个物体的材质 pass”移动到“屏幕空间的 lighting pass”,并让 G-buffer、深度、材质参数和后续屏幕空间效果形成一条可复查的数据路径。

本章使用一个贯穿帧作为材料:夜间城市街道,画面里有湿润沥青、霓虹灯牌、车辆金属、墙面贴花、八十个点光和聚光灯、少量玻璃橱窗以及雨水反射。Forward 管线会让每个受光物体在材质着色时处理光源列表;Deferred 管线先把每个可见像素的几何和材质属性写入 G-buffer,再在屏幕空间读取这些属性计算光照。读完本章后,读者应能定位 Deferred 管线中的 G-buffer 写入、lighting pass、decal、SSAO、SSR、透明 fallback 和带宽瓶颈,并能根据 attachment 布局判断一个延迟管线能否承载目标画质。

这里的 G-buffer 指 Geometry Buffer。它是一组 render target 和 depth/stencil 资源,用来保存后续 shading 所需的可见表面属性。它通常保存 albedo、normal、roughness、metallic、emissive、motion vector、material id 或压缩后的材质参数。它的核心价值是把“可见性解析”和“光照计算”拆开:几何 pass 先确定每个屏幕像素对应哪个表面,lighting pass 再根据这些表面属性计算直接光、间接光近似和局部屏幕空间效果。

Deferred Rendering 的工程判断集中在一个取舍上:它降低多光源逐物体 shading 的重复计算,却增加 G-buffer 写入、读取、格式转换和 render target 带宽。Unity 6.4 的 Built-In Deferred 文档明确列出了 RT0 到 RT4 的 G-buffer 布局和位数,Microsoft Direct3D 11 Output-Merger 文档也说明一个 pixel shader 可写入多个 render target,并要求这些 render target 的尺寸、维度和采样数匹配;这些资料支撑本章的工程边界:Unity 6.4 Deferred rendering path展示了商业引擎如何把材质属性、深度、光照累加和 forward fallback 组合成可运行管线。

105.1 Deferred Rendering 的基本设计与优势

Deferred Rendering 的基本设计,是把一帧拆成至少两个核心阶段:G-buffer pass 和 lighting pass。G-buffer pass 渲染不透明物体,写出表面属性;lighting pass 读取 G-buffer 和 depth,根据屏幕像素重建 shading 输入,计算每个像素受到的光照。这个设计让光照计算的单位从“物体 × 光源”变成“像素 × 影响该像素的光源”。

在贯穿帧里,湿润街道由多个 mesh、多个材质和大量小光源组成。Forward 管线中,如果一个物体受到十个局部灯影响,它的 fragment shader 往往需要循环处理十个灯,物体数量和材质变体会把光照逻辑复制到多个 pass 或多个 shader variant 中。Deferred 管线先让街道、墙面、车辆写入 G-buffer;之后每个小灯只在自己的屏幕影响区域内读取这些属性并累加光照。光源数量增加时,主要增长的是 lighting pass 的屏幕像素覆盖和 light culling 成本。

这条路径的第一层优势是光照和几何复杂度解耦。大量小灯照在同一批可见像素上时,G-buffer 已经保存了这些像素的 normal、albedo 和 material 参数,lighting pass 无需再次执行所有物体的材质前半段逻辑。对于夜间城市、科幻走廊、室内灯带、爆炸火花、局部动态光密集场景,Deferred 管线能把光照扩展性压到屏幕空间范围内评估。

第二层优势是屏幕空间数据复用。SSAO 需要 depth 和 normal,SSR 需要 depth、normal、roughness 和上一阶段颜色,decal 需要在屏幕空间修改局部材质属性,debug view 需要独立显示 normal、roughness、metallic 或 velocity。G-buffer 把这些数据变成一组共享资源,使后续效果围绕同一份可见表面数据组织。

第三层优势是可观察性。Forward 管线里,最终颜色通常由材质、光照、阴影、反射和雾效混在一个 fragment shader 输出中。Deferred 管线保留了中间属性,调试时可以单独查看 albedo 是否处在线性空间、normal 是否归一化、roughness 是否被错误压缩、motion vector 是否反向、depth 是否精度不足。对工程排查而言,G-buffer debug view 是把视觉错误拆回资源字段的入口。

Deferred 管线的代价也出现在同一条数据路径里。G-buffer pass 会向多个 color attachment 写入数据,lighting pass 会再把这些 attachment 读回。高分辨率、HDR、MSAA、移动 GPU、tile memory、带宽受限平台会放大这部分成本。透明物体、折射、粒子、毛发、玻璃通常还需要 forward fallback,因为一个屏幕像素只保存最前面的不透明表面属性,单层 G-buffer 无法表达多层透明排序和多层材质混合。

可以把 Deferred Rendering 看作一个工程合约:几何阶段承诺把后续光照需要的属性写成稳定格式,lighting 阶段承诺只依赖这些属性、depth、shadow 和光源数据完成大部分不透明表面 shading。这个合约成立时,多光源场景获得可扩展性;合约膨胀时,G-buffer 字段数量、格式位数和带宽会吞掉收益。

105.2 G-Buffer Attachment Layout and Readback Contract

G-buffer 布局决定 Deferred 管线能表达哪些材质、支持哪些 debug view、消耗多少 bandwidth。Attachment layout 指每个 render target 的格式、通道、色彩空间、写入阶段和读取阶段。Readback contract 在这里指后续 pass 读取 G-buffer 时依赖的字段约定:normal 存在哪个空间、roughness 的范围是否线性、albedo 是否已经转成 linear、material id 如何映射 BRDF 分支、motion vector 使用当前帧到上一帧还是上一帧到当前帧。

贯穿帧中至少需要六类数据。湿润沥青需要 albedo、normal、roughness 和 metallic;车辆金属需要更高精度的 normal 和 specular 参数;墙面贴花需要 decal 修改 albedo、normal 或 material;SSR 需要 depth、normal、roughness 和 scene color;TAA 需要 motion vector;debug view 需要把每个字段直接显示出来。布局设计要先列出这些消费方,再反推每个字段的格式。

一个常见的 G-buffer 草案可以写成下面的资源表。这个表不是固定模板,它展示的是字段分组的判断方式。

Attachment建议格式字段主要读取方关键边界
GBuffer0RGBA8_UNORM 或 sRGB 输入后线性存储albedo.rgb、occlusion.alighting、SSAO 合成、debug albedoalbedo 进入 lighting 前应处在线性空间
GBuffer1RGBA8_UNORMRG16_UNORM 加压缩normal.xy 或 normal.xyz、roughnesslighting、SSR、SSAO、debug normalnormal 空间必须固定为 world 或 view
GBuffer2RGBA8_UNORMmetallic、specular、material id、shading flagsBRDF 分支、材质选择、debug materialmaterial id 数量受通道位数限制
GBuffer3RGBA16_FLOAT 或目标 HDR 格式emissive、prelight、lighting accumulationemission、lightmap、reflection、后处理输入HDR 需要明确曝光前后的存储边界
VelocityRG16_FLOATRG16_SNORMmotion vector.xyTAA、motion blur、temporal SSR方向约定必须和 temporal pass 一致
Depth/Stencildepth24/stencil8、depth32 或平台格式depth、stencil maskposition reconstruction、light volume、SSAO、SSRdepth range 和 reversed-Z 约定要统一

布局的第一条判断是空间约定。Normal 可以存 world space,也可以存 view space。World space normal 便于跨 pass 保持稳定,SSR、reflection probe 和 debug 观察更直观;view space normal 便于屏幕空间运算,某些 SSAO 或光照计算少一次矩阵变换。两者都成立,工程中要让写入 shader、lighting shader、debug shader 和离线分析工具使用同一约定。混用 normal 空间会表现为光照方向随相机旋转漂移,或者 SSR 反射方向错误。

布局的第二条判断是格式精度。Albedo 通常不需要 float attachment,但 normal、roughness、motion vector 和 HDR lighting 对精度更敏感。Normal 用 RGBA8_UNORM 存 xyz 时会有量化误差;用 octahedral encoding 存到两个通道可以节省带宽,但 lighting pass 必须解码并重新归一化。Motion vector 若用 8 位通道,快速移动边缘会产生 temporal ghosting;若用 RG16_FLOAT,带宽上升但 TAA 更稳定。

布局的第三条判断是字段复用。一个通道同时承载 material id、shading flags 和 specular 参数时,后续 BRDF 分支会被这个编码约束。比如 material id 只有 8 位时,debug shader 可以显示 256 种分类;如果项目需要 clear coat、subsurface、cloth、anisotropy、toon 等多种 shading model,就要决定新增 attachment、压缩参数、使用材质表索引,或者让部分材质走 forward/special pass。

布局的第四条判断是 readback contract。G-buffer 的“读回”主要发生在 GPU pass 之间,也可以发生在工具 debug 或 CPU 截帧分析中。每个字段都要有明确解释:取值范围、单位、空间、编码、清屏值、无效值、是否允许 decal 修改。没有这个 contract,debug view 看到一张灰色 roughness 图时,读者无法判断它表示材质统一、压缩错误、通道读取错误,还是 sRGB 转换错误。

下面这段简化的 pass 声明展示了 attachment contract 的组织方式。它不是某个 API 的完整代码,只保留资源字段、格式和消费方关系。

struct AttachmentDesc {
const char* name;
const char* format;
const char* semantic;
const char* producer;
const char* consumer;
};

AttachmentDesc gbufferLayout[] = {
{"GBufferAlbedoAO", "RGBA8_UNORM", "linear albedo + ambient occlusion", "GBufferPass", "LightingPass, DebugView"},
{"GBufferNormalRoughness", "RGBA8_UNORM", "oct normal.xy + roughness + unused", "GBufferPass, DecalPass", "LightingPass, SSAO, SSR"},
{"GBufferMaterial", "RGBA8_UNORM", "metallic + specular + material id + flags", "GBufferPass, DecalPass", "LightingPass, DebugView"},
{"SceneVelocity", "RG16_FLOAT", "current-to-previous clip motion", "VelocityPass", "TAA, MotionBlur"},
{"SceneDepthStencil", "D24S8", "device depth + stencil masks", "DepthPrepass, GBufferPass", "LightingPass, SSAO, SSR"},
};

代码里的关键点是 producer 和 consumer 先写出来。Attachment 不是孤立纹理,它是 pass 之间的接口。GBufferNormalRoughness 被 G-buffer pass 写入,被 decal pass 局部修改,又被 lighting、SSAO、SSR 读取;因此它的 normal encoding、roughness 范围和 decal 写入规则要在管线层固定。SceneVelocity 的方向约定写成 current-to-previous clip motion 后,TAA 和 motion blur 才能使用同一套重投影逻辑。

Debug view 应覆盖每个 contract 字段。Albedo view 检查色彩空间和纹理通道;normal view 检查空间、归一化和接缝;roughness view 检查材质压缩;material id view 检查 BRDF 分支;velocity view 检查方向和相机抖动;depth linear view 检查 near/far、reversed-Z 和远处精度。Debug view 的价值在于它让“最终画面异常”回到“哪个 attachment 字段异常”。

105.3 实现可扩展的 Deferred 管线

可扩展 Deferred 管线的核心,是把 pass 顺序、资源生命周期、debug hook 和 fallback path 固定下来。G-buffer 只是中间资源;完整管线还要处理 depth prepass、decal、SSAO、SSR、shadow、lighting、transparent、post-processing 和 profiling。扩展性来自 pass 接口清晰;过度增加 G-buffer 通道只会扩大带宽和维护成本。

下面的流程图描述贯穿帧的一条典型路径。它包含不透明物体、decal、屏幕空间效果、光照、透明 fallback 和后处理。

这张图的边界是单帧不透明主路径。Depth prepass 先写可见深度,可以减少后续 G-buffer pass 的 overdraw,也能给 SSAO、SSR 和 light volume 提供稳定 depth。G-buffer pass 写入表面属性。Decal pass 在屏幕空间或 mesh decal 路径上修改 albedo、normal、roughness 等字段。SSAO 和 SSR prepare 读取 depth/normal/roughness 生成中间结果。Lighting pass 读取 G-buffer、shadow、AO、reflection 输入,输出 HDR scene color。Transparent forward 在最终不透明光照之后渲染玻璃、粒子、半透明水雾和特殊材质。

Depth prepass 的取舍要结合场景。夜间城市有大量遮挡、墙面、车辆和室内物件,G-buffer 写入成本较高,depth prepass 可以让后续 color attachment 写入集中在最终可见像素上。草地、粒子、alpha test 植被密集时,depth prepass 也可能增加顶点和 alpha test 成本。判断顺序是先看 G-buffer pass 是否存在严重 overdraw,再看 depth prepass 增加的 draw、vertex 和 depth bandwidth 是否低于节省的 color bandwidth。

G-buffer pass 应尽量保持材质前半段统一。它负责解包纹理、计算 normal、写入材质参数和 emissive,不负责遍历大量光源。材质系统要把“写入哪些 G-buffer 字段”做成稳定接口,使 PBR、decal、lightmap、emissive 和 material id 都能落到同一组 attachment。若材质每增加一个功能就新增 shader variant 和 attachment 字段,Deferred 管线会很快被 variant 和带宽压住。

Decal 在 Deferred 管线里常有两种位置。第一种在 G-buffer pass 后、lighting pass 前,把贴花投影到表面并修改 G-buffer 字段,例如墙面弹孔改变 albedo 和 normal,路面水迹降低 roughness。第二种在 lighting pass 后作为 color decal 合成,只改变最终颜色。前者能参与光照,适合材质属性变化;后者成本更低,适合纯颜色标记。贯穿帧里的湿地水迹应放在 lighting 前写 roughness,霓虹招牌上的污渍可以只做 color decal。

SSAO 和 SSR 依赖 G-buffer 的字段质量。SSAO 使用 depth 和 normal 估计局部遮蔽,normal 空间错误会让阴影沿错误方向扩散。SSR 使用 depth、normal、roughness 和 scene color 做屏幕空间反射,roughness 过低会让湿润地面出现不稳定高频反射,motion vector 错误会破坏 temporal resolve。把这些效果放进 Deferred 管线时,要把它们视为 G-buffer contract 的消费方。

Transparent forward 是延迟管线的常规补充。玻璃橱窗、雨滴、粒子和半透明水雾需要排序、混合、折射或多层深度。单层 G-buffer 保存的是最前不透明表面,无法描述透明层后面的多个表面。工程做法是先完成不透明 Deferred 光照,再用 forward pass 渲染透明对象;透明 pass 读取 scene color、depth、reflection、light list 或 probe 数据,输出到 HDR scene color。

可扩展性还依赖 render graph 或 pass graph。每个 pass 声明自己读取和写入哪些资源,图系统负责生命周期、barrier、aliasing、debug marker 和 profiling hook。即使项目没有完整 render graph,也应把 pass 的资源表写清楚:G-buffer pass 写 RT0/RT1/RT2/depth,decal pass 读 depth 写部分 G-buffer,lighting pass 读所有 G-buffer 写 scene color,transparent pass 读 scene color/depth 写 scene color。

下面这段伪代码展示 pass 关系。它强调资源声明和执行顺序,不绑定 Vulkan、Direct3D、Metal 或 WebGPU 的具体对象名。

RenderPass& depth = graph.addPass("DepthPrepass");
depth.write(depthStencil);

auto& gbuffer = graph.addPass("GBufferPass");
gbuffer.read(depthStencil);
gbuffer.write(gbuffer0);
gbuffer.write(gbuffer1);
gbuffer.write(gbuffer2);
gbuffer.write(velocity);

auto& decals = graph.addPass("DeferredDecals");
decals.read(depthStencil);
decals.write(gbuffer0);
decals.write(gbuffer1);

auto& lighting = graph.addPass("DeferredLighting");
lighting.read(depthStencil);
lighting.read(gbuffer0);
lighting.read(gbuffer1);
lighting.read(gbuffer2);
lighting.write(hdrSceneColor);

auto& transparent = graph.addPass("TransparentForward");
transparent.read(depthStencil);
transparent.read(hdrSceneColor);
transparent.write(hdrSceneColor);

这段代码后面的判断是:Deferred 管线可维护的前提,是每个 pass 的资源关系可枚举。图系统看到 DeferredLighting 读取 G-buffer 后,就能插入正确的 resource transition 或 memory barrier;debug UI 也能在该 pass 前后截取 attachment。若 pass 内部私自读写未声明资源,后续平台适配、aliasing 和性能分析都会失去依据。

105.4 光照与材质在延迟管线中的处理

Deferred lighting pass 的输入来自三类数据:G-buffer 表面属性、光源与阴影数据、屏幕空间或环境光中间结果。它的输出通常是 HDR scene color 或 lighting accumulation buffer。材质系统要把 BRDF 所需参数压缩到 G-buffer 字段里,光照系统要把大量光源限制到对应屏幕区域,二者共同决定 Deferred 管线的画质和成本。

以 PBR 金属粗糙度模型为例,lighting pass 至少需要 normal、view direction、albedo、roughness、metallic、occlusion、emissive 和 depth 重建的位置。Depth 本身通常保存设备深度,lighting shader 会用逆投影矩阵重建 view-space 或 world-space position。这个重建步骤必须和相机 jitter、reversed-Z、near/far、clip space convention 对齐。位置重建错误会让点光衰减、SSR ray marching 和 shadow lookup 全部偏移。

下面的片段展示 lighting shader 的主干。它省略具体 BRDF 公式,只保留 G-buffer 解码、位置重建、光源循环和累加关系。

vec3 albedo = texture(gbufferAlbedoAO, uv).rgb;
float ao = texture(gbufferAlbedoAO, uv).a;
vec4 normalRoughness = texture(gbufferNormalRoughness, uv);
vec3 normalWS = decodeOctNormal(normalRoughness.xy);
float roughness = normalRoughness.z;

vec4 material = texture(gbufferMaterial, uv);
float metallic = material.x;
float materialId = material.z;

float deviceDepth = texture(sceneDepth, uv).r;
vec3 positionWS = reconstructWorldPosition(uv, deviceDepth, inverseViewProjection);
vec3 viewDirWS = normalize(cameraPositionWS - positionWS);

vec3 radiance = vec3(0.0);
for (uint i = 0u; i < visibleLightCount; ++i) {
Light light = visibleLights[i];
float shadow = sampleShadow(light, positionWS, normalWS);
radiance += evaluateBRDF(albedo, normalWS, viewDirWS, roughness, metallic, light) * shadow;
}

vec3 emissive = texture(gbufferEmission, uv).rgb;
outColor = vec4(emissive + radiance * ao, 1.0);

这段 shader 的关键约束是输入全来自 contract。decodeOctNormal 对应 G-buffer normal 编码;reconstructWorldPosition 对应 depth range 和相机矩阵;visibleLights 对应 light culling 结果;evaluateBRDF 对应 material 参数布局。任何一个字段的空间或范围发生变化,lighting pass 都要同步更新,否则最终画面会出现方向错、亮度错或材质分支错。

光源处理有三种常见路径。第一种是全屏 lighting,每个像素遍历所有光源或一个全局光源列表,适合少量方向光和环境光。第二种是 light volume,对点光渲染球体、对聚光渲染锥体,只在光体积覆盖的像素执行 lighting shader,并利用 depth/stencil 减少无效区域。第三种是 tiled 或 clustered deferred,先把屏幕划分为 tile 或 cluster,计算每个 tile/cluster 影响的光源列表,lighting shader 只遍历局部列表。

贯穿帧里的八十个小灯适合 light volume 或 tiled/clustered。霓虹灯牌附近的点光只覆盖少量屏幕像素,用光体积渲染时,远离灯的像素完全不执行该灯计算。街道上大范围方向光或天空光适合全屏 pass,因为它们影响整个画面。大量小灯同时存在时,clustered deferred 可以把光源筛选前移到 compute pass,降低 lighting shader 的循环长度。

Stencil 在 Deferred lighting 中常用于限制光体积。渲染点光球体时,可以先用 depth/stencil 标记球体与场景深度的交集,再在 lighting pass 中只对标记区域执行累加。这个做法减少全屏像素访问,但增加 light volume draw、stencil state 和深度测试复杂度。判断它是否值得使用,要看光源屏幕面积、遮挡比例、draw 提交成本和 GPU depth/stencil 性能。

材质分支是 Deferred 管线的常见压力点。G-buffer 通常保存一个 material id 或 shading model id,lighting pass 根据它选择 BRDF。分支数量少时,可以在一个 lighting shader 中处理 standard、clear coat、emissive 等模型;分支数量多时,wave 内不同像素走不同路径,SIMD/SIMT 执行效率下降。工程上常把大多数不透明物体收敛到 standard PBR,把皮肤、头发、布料、各向异性金属等特殊材质放到专用 deferred variant 或 forward/special pass。

Shadow 的成本仍然存在。Deferred 管线减少材质与光源的组合复杂度,但 shadow-casting objects 仍需为 shadow map 渲染,lighting pass 仍需 shadow lookup。小灯没有阴影时成本接近屏幕覆盖和 BRDF;小灯带阴影时,成本还包含 shadow map 生成、shadow atlas、filtering 和采样。贯穿帧里,霓虹小灯可以多数无阴影,少量关键车灯或路灯启用阴影,这样才能保留视觉层次并控制帧时间。

Light accumulation 的输出也需要设计。简单管线可直接把 lighting 结果写到 HDR scene color。更复杂的管线会分离 diffuse/specular、direct/indirect、emissive/reflection,服务调试、denoising 或后续 temporal pass。每增加一个 accumulation target,都会增加写带宽和后续读取成本。只有当后续 pass 真正消费这些分离字段时,拆分 accumulation 才有工程收益。

105.5 Deferred Rendering Bandwidth Lighting and Memory Budget

Deferred 管线的预算要同时看 attachment 字节数、分辨率、刷新率、pass 次数、读写次数、MSAA、tile memory 行为和 light coverage。一个常见误判是只数灯光数量。真实瓶颈可能出现在 G-buffer 写入、lighting pass 读取、HDR scene color 写入、SSR 多次采样、透明 fallback overdraw 或 resource transition 上。

先建立一个基础估算。假设 1920×1080,G-buffer 包含 albedo AO 4 字节、normal roughness 4 字节、material 4 字节、velocity 4 字节、depth 4 字节,总计 20 字节每像素。单次完整写入的理想数据量约为 1920 × 1080 × 20 = 41.5 MB。如果 60 FPS,每秒仅这组理想写入就是约 2.49 GB/s。Lighting pass 读取这些字段并写 HDR scene color 后,还会追加读带宽和写带宽。实际 GPU 上还会受压缩、tile store/load、cache、MSAA、overdraw 和格式对齐影响。

这个估算不用于预测绝对性能,它用于比较设计方案。若把 normal 从 4 字节扩成 RGBA16_FLOAT 的 8 字节,1080p 单帧多出约 8.3 MB 写入和相应读取;在 4K 下变成约 33.2 MB。若开启 4x MSAA 的多采样 G-buffer,样本级存储会显著放大 attachment 成本。许多 Deferred 管线把 MSAA 留给 forward path 或使用 TAA/FXAA/SMAA 等后处理抗锯齿,就是因为多 render target 的 MSAA 成本过高。

带宽预算的第一步是列出每个 pass 的读写矩阵。下面的表把贯穿帧中最容易消耗带宽的阶段放到同一视角下。

Pass主要读取主要写入主要风险观察证据
Depth Prepassvertex/index/material alphadepth增加顶点和深度写入depth pass GPU time、early-z 效果
GBuffer Passtextures、mesh attributes、depth多个 G-buffer RT、velocityMRT 写入和 overdrawRT bandwidth、overdraw heatmap、draw timing
Decal Passdepth、decal textures部分 G-buffer RT大面积 decal 重写decal pass timing、G-buffer debug view
SSAO/SSRdepth、normal、roughness、scene colorAO、reflection intermediate多次采样和 temporal historysample count、cache miss、temporal artifacts
Lighting PassG-buffer、depth、shadow、light listHDR scene colorG-buffer 读取、BRDF、shadow lookuplight coverage、shader cost、shadow timing
Transparent Forwardscene color、depth、light/probeHDR scene coloroverdraw、排序、混合transparent overdraw、blend bandwidth

预算的第二步是判断光照覆盖。八十个点光听起来很多,但每个小灯只覆盖几十个像素时,lighting cost 可能低于一个全屏 SSR pass。相反,一个覆盖全屏的高成本 area light 近似或大范围 shadowed spot light 会消耗更多 shader 时间。Deferred 管线应记录每个 light 的屏幕面积、是否投影阴影、是否使用 cookie、是否进入 clustered list、平均每 tile 光源数量和最大 tile 光源数量。

预算的第三步是控制 G-buffer 字段增长。每个新增字段都要回答四个问题:哪个 pass 写它,哪个 pass 读它,debug view 如何验证,删除它会损失哪类画面能力。若一个字段只服务少量材质,可以考虑 material table 索引、压缩编码、低分辨率 buffer、专用 pass 或 forward fallback。若一个字段被 lighting、SSR、SSAO、TAA 多方依赖,就应给它稳定精度和明确 contract。

预算的第四步是区分桌面 immediate-mode GPU 和移动 tile-based GPU 的边界。桌面 GPU 常有较高显存带宽,Deferred 的多 RT 写入和读取更容易被承载;移动 GPU 常依赖 tile memory 降低外部内存访问,G-buffer 的 store/load、MSAA resolve、后续 pass 读取可能触发额外外部带宽。移动项目若使用 Deferred,通常要压缩 G-buffer、减少 RT 数量、限制 HDR buffer、使用 tile-friendly pass 顺序,并用实际 GPU profiler 观察外部 memory bandwidth。

预算的第五步是建立降级策略。分辨率提升、HDR、motion vector、SSR、SSAO、shadow、decal、透明 overdraw 会共同推高成本。工程上可以按优先级降级:先降低 SSR 分辨率或步数,再限制小灯阴影,再合并或压缩 G-buffer 字段,再减少大面积 decal,再降低透明物体复杂度,最后才改变核心材质模型。这个顺序保留 Deferred 管线的主要收益,同时让视觉变化可控。

一个可复用的排查顺序是:先打开 G-buffer debug view,确认字段正确;再看 GPU capture 的 pass timing,定位是 G-buffer 写入、lighting、SSR/SSAO 还是 transparent;接着查看 attachment 格式和分辨率,计算基础读写量;再看 light coverage 和 per-tile light count;最后检查 resource transition、store/load、MSAA 和 temporal history。这个顺序把视觉错误、资源错误和性能错误放到同一条数据路径上。

Deferred Rendering 的最终判断可以收束为一句话:当场景以大量不透明表面和大量动态光为主,并且平台带宽能承载 G-buffer 读写时,Deferred 管线能把光照扩展性、屏幕空间效果和调试可见性组织到一个稳定系统里;当场景以透明、多层材质、极低带宽或强 MSAA 需求为主时,Forward、Forward+、hybrid deferred 或专用 pass 会占据更合适的位置。

最小自检任务

给定一个 1920×1080、60 FPS 的夜间街道场景:不透明物体较多,80 个小型点光和聚光灯,湿润路面需要 SSR,墙面需要 decal,玻璃橱窗和雨滴需要半透明,目标平台包含桌面独立 GPU 和一类带宽受限移动 GPU。请设计一个 Deferred 管线草案,写出 G-buffer attachment 布局、pass 顺序、透明 fallback、debug view 和性能排查顺序,并说明哪些字段或效果在移动平台上优先降级。

答案要点

合理答案应先给出 G-buffer contract。至少包含 albedo/AO、normal/roughness、material 参数、velocity 和 depth/stencil,并说明 normal 空间、albedo 色彩空间、roughness 范围、motion vector 方向和 material id 用途。字段设计要绑定消费方:lighting 读取 albedo/normal/roughness/material/depth,SSAO 读取 depth/normal,SSR 读取 depth/normal/roughness/scene color,TAA 读取 velocity。

Pass 顺序应从 depth prepass 或 G-buffer pass 开始,接着是 G-buffer、decal、SSAO/SSR prepare、deferred lighting、transparent forward、post-processing 和 present。Decal 若改变湿润路面的 roughness,应放在 lighting 前写 G-buffer;玻璃橱窗和雨滴应走 transparent forward,读取 scene color 和 depth 后混合到 HDR scene color。

光照策略应区分全屏光和局部光。方向光、环境光适合全屏 lighting pass;小型点光和聚光灯适合 light volume 或 tiled/clustered light list。带阴影的小灯要受数量控制,因为 shadow map 生成和 shadow lookup 仍然消耗 GPU 时间。

性能排查顺序应先看 G-buffer debug view,再看 GPU capture 的 pass timing,然后计算 attachment 基础读写量,接着观察 light coverage、per-tile light count、SSR/SSAO 采样成本、transparent overdraw 和 resource transition。移动平台优先降低 SSR 分辨率或步数、限制小灯阴影、压缩 normal 和 material 字段、减少 G-buffer RT 数量、控制大面积 decal 和透明 overdraw。

本章知识点总结

  • 设计目标:Deferred Rendering 把不透明表面的属性写入 G-buffer,再在屏幕空间完成大部分光照计算。
  • 核心收益:多光源场景中,光照成本主要随光源覆盖像素和局部光源列表增长。
  • G-buffer 合约:每个 attachment 都要定义格式、通道、空间、范围、生产 pass、消费 pass 和 debug view。
  • 字段边界:normal、roughness、material id、velocity 和 depth 的编码会直接限制 lighting、SSR、SSAO 和 TAA 的稳定性。
  • 管线顺序:典型 Deferred 帧由 depth、G-buffer、decal、SSAO/SSR、lighting、transparent forward 和 post-processing 组成。
  • Decal 位置:改变材质属性的 decal 应在 lighting 前修改 G-buffer,纯颜色 decal 可在 lighting 后合成。
  • 透明处理:单层 G-buffer 保存最前不透明表面,玻璃、粒子、雨滴和多层混合通常走 forward fallback。
  • 光源组织:方向光适合全屏 pass,小型点光和聚光灯适合 light volume、stencil 限制或 tiled/clustered 列表。
  • 材质分支:Deferred lighting 依赖 material id 选择 BRDF,过多 shading model 会增加分支和 variant 压力。
  • 阴影成本:Deferred 管线降低材质与光源组合成本,但 shadow map 生成和 shadow lookup 仍需单独预算。
  • 带宽预算:G-buffer 的字节数、分辨率、刷新率、读写次数、MSAA 和 store/load 共同决定内存压力。
  • 平台边界:桌面 GPU 更容易承载多 RT 带宽,移动 tile-based GPU 需要压缩布局、减少外部内存访问并依赖 profiler 验证。
  • 排查顺序:先验证 G-buffer 字段,再定位 pass timing,随后计算读写量,最后检查 light coverage、采样成本和资源状态。
  • 适用结论:Deferred 管线适合大量不透明表面与大量动态光源共存的场景,透明密集、强 MSAA 或极低带宽场景需要 hybrid 设计。