GPU Gems 3 · Chapter 19. Deferred Shading in Tabula Rasa

从 Tabula Rasa 的双管线实践出发,解释如何用可读的 MRT 属性、局部光体积与带宽预算把延迟着色落到真实游戏引擎。

学习目标

  • 能解释 deferred shading 如何把材质写入与光照计算拆成离散阶段,并指出它对 MMO 场景扩展性的价值
  • 能修改 Tabula Rasa Deferred Shading Lab 的管线、光数量、分辨率、光体积和透明路径,比较绘制次数与带宽
  • 能回答:为什么透明几何需要 forward fallback,以及为什么 G-buffer 的通道必须通过可读的属性契约隐藏

先问:灯越来越多,为什么画面不该越来越慢

想象一座夜晚的城市:建筑数量、玩家数量和特效灯都不可预知。如果每栋建筑每遇到一盏灯就重新画一遍,新增一盏灯既增加 CPU 决策,又增加几何重复工作。

本章要处理的难题是:先把每个可见像素的表面资料存下来,再让每盏灯只处理自己覆盖的像素。这样做会多写几张屏幕大小的纹理,也会让透明物体和低端硬件变得棘手;工程目标不是“永远延迟”,而是用清楚的边界换取可扩展的光照成本。

1. 先把材质和光照拆开

传统 forward shading 在画一个物体时同时完成形状、材质、入射光和表面响应。它直观,却会让“哪些灯影响哪些物体”的 CPU 判断和多次几何 pass 随场景规模一起增长。Tabula Rasa 面对的是 MMO:玩家、可见特效和动态灯的数量都没有紧上限,因此选择把前两步和后两步拆开。

材质阶段只负责把像素需要的资料写入屏幕大小的缓冲;光照阶段以屏幕像素为单位读取这些资料。几何复杂度不再直接乘上灯数量,但代价是额外的 render-target 写入、读取和视频内存管理。

deferred shading: write once, light latermaterial passno light loopposition · normal · materiallight passsphere / cone / box volumeonly covered pixelsfinal passdiffuse + specularfog · edge · outputeach pixel lit once per lightgeometry complexity decoupledmaterial shaders and light shaders share only an explicit pixel-attribute contract

第 1 / 3 步 · 几何阶段只写入位置、法线和材质属性

播放三个阶段:几何负责写资料,光负责读资料,最终 pass 负责组合结果。

2. G-buffer 要成为稳定的属性接口

延迟管线依赖 multiple render targets,把一个 fragment 的多个属性同时写出。典型布局会为深度保留高精度目标,再用若干 RGBA 目标存漫反射、法线、镜面输入和标志位。Shader Model 3.0 级硬件的同时目标数量和位深有限,所以每个通道都应被当作珍贵资源。

不要让材质 shader 和 light shader 直接记住“漫反射在 MRT 0 的 rgb”这种物理布局。把每一项资料定义为 ,再通过 DL_Set*DL_Get* 访问器读写。以后压缩、换通道或增加默认值,只需要重建共享头文件和 shader,而不必逐个修改所有效果文件。

a readable G-buffer is an API, not a pile of channelsmaterial shaderDL_SetDiffuseDL_SetNormalDL_SetDepthMRT targetsMRT 0diffuse + flagsMRT 1normal + materialMRT 2specular inputsdeptheye-space depthchannel budget is finitelight shaderDL_Get*stable contractstorage may changeshader need notknow whereview-space normals can reconstruct z and save one channel when the sign is known

视空间法线还能进一步节省通道:可见表面的法线 z 分量符号一致,加上单位向量约束,就能从 x、y 重建 z。重要的是先把“需要什么属性”固定下来,再决定怎样压缩,不要把某一种灯专用的数据写进所有材质的公共接口。

3. 让光体积裁掉无关像素

光照 shader 理论上可以对每盏灯画全屏四边形,但那会让远离灯的像素也执行纹理读取和光照。light volume 把灯的影响范围投影到屏幕:点光使用球体,聚光灯使用锥体,局部方向光使用盒体,方向光这类全局灯才使用全屏路径。

Stencil masking、early z-rejection 和 dynamic branching 也可以进一步丢弃像素,但它们都依赖 locality:相邻像素最好做出相似的拒绝或分支决定。额外的“便宜 pass”如果带来更多 draw call,可能抵消最终光照 pass 的收益;要用目标硬件测量,而不是把每个优化都叠上去。

shade the pixels a light can actually touchpoint lightsphere volumespot lightcone volumebox lightbox volumedirectionalfull-screen quadtighter screen-space bounds mean fewer light-shader pixels and less bandwidth

4. 深度和法线缓冲不只服务光照

延迟缓冲提供了眼空间深度、法线和其他像素资料,水面、雾、体积粒子和边缘处理都可以复用。水面在同一个 pass 中比较当前水深和被折射像素的深度:目标确实在水面之后才执行折射,否则拒绝这次采样。这样可以自动形成岸线,也能让颜色和透明度随深度非线性变化。

边缘处理不需要依赖固定分辨率的魔法数字。对中心像素和八个邻居同时采样,比较深度梯度的最大变化与最小变化,再比较相邻法线夹角的 cosine 变化,得到 0 到 1 的 edge weight。权重为 0 时保留中心样本,权重为 1 时混合四个角方向的双线性样本,最终在一次后处理 pass 中完成柔化。

the buffer is useful beyond lightingwater / refractioncurrent water depth vs target depthbehind? → refract; otherwise rejectresolution-independent edgesNNNNCNNNNmax/min depth gradientnormal cosine change → edge weightdepth and normal data become a shared substrate for water, fog, particles, and edge smoothing

5. 把带宽预算拆成可观察的阶段

延迟 shading 的主要税不是抽象的“更复杂”,而是每帧的字节数:材质阶段从写一个目标变成写多个 MRT,光照阶段再读取这些目标。Tabula Rasa 还把 diffuse light 和 specular light 分开写入 light accumulation buffer,最后一次全屏合成时再与未着色颜色组合。这样 light shader 不必反复读取完整材质,也保留了 HDR、对比度和其他光照后处理的独立入口。

分辨率是最直接的控制杆:像素数上涨会同时放大 MRT 写入、光照读取和最终合成。降低不需要高精度的目标位深、减少不必要属性、复用 shadow-map pool,并限制同一帧需要重建的 shadow map 数量,往往比在光照函数里再塞一层分支更有效。

bandwidth is the deferred-shading taxmaterial writesscreen-size MRTslight accumulationdiffusespecularadditive light passesfinal composeunlit × diffuse+ specular + fogone final fetch pathresolution, render-target formats, and pooled shadow maps decide whether the trade is worth it

6. 透明几何必须有清楚的边界

alpha blending 的根本问题是:一个屏幕像素可能叠着多个材质片元,而普通 G-buffer 只保存最近的那个。要支持真正的透明,需要深 framebuffer 或 order-independent transparency 机制;Tabula Rasa 的务实做法是让水和其他半透明几何在不透明 deferred pass 完成后回到 forward fallback。这样能复用 deferred 的深度输入,却无法保证两条管线的所有灯型和材质特性完全一致。

头发和地表植被尤其容易暴露差异:它们视觉上偏好 alpha blending,但如果回到 forward,阴影和灯型可能与不透明世界不匹配。因此工程上选择 alpha testing 或 clip 让它们作为“完全不透明”的像素进入 deferred,并用缩放模拟淡入淡出,而不是让每种透明都污染 G-buffer 契约。

静态和动态 shadow map 也应该分开管理:静态几何的地图可以复用,动态几何只更新自己的地图再合成;shadow-map pool 保留纹理并按灯的优先级分配,避免每帧创建和释放造成显存碎片。对日夜循环里的全局光,还可以量化光位置和方向,减少相邻帧深度值微小抖动带来的阴影闪烁。

7. 把引擎方案组装成可扩展的三步

DL_PixelOutput MaterialPS(MaterialInput input) {
  DL_PixelOutput output;
  DL_Reset(output);
  DL_SetDiffuse(output, input.diffuse);
  DL_SetNormal(output, input.normal);
  DL_SetDepth(output, input.eyeDepth);
  return output;
}
 
float3 LightPS(float2 uv, Light light) {
  float3 diffuse = DL_GetDiffuse(uv);
  float3 normal = DL_GetNormal(uv);
  float depth = DL_GetDepth(uv);
  return EvaluateLight(light, diffuse, normal, depth);
}

这段伪 HLSL 的重点不是 API 名字,而是依赖方向:材质 pass 初始化并写入通用属性,light pass 通过访问器读取属性并只处理光体积覆盖的像素。代码共享库让 forward、deferred、shadow-map 和 skinned/static technique 可以共用同一套数据契约。

分步1 / 3

先建立材质属性和双管线边界

用 MRT 保存深度、法线、漫反射和通用材质属性;材质 shader 不做光照,同时保留低端硬件和透明几何所需的 forward 路径。

deferred shading: write once, light latermaterial passno light loopposition · normal · materiallight passsphere / cone / box volumeonly covered pixelsfinal passdiffuse + specularfog · edge · outputeach pixel lit once per lightgeometry complexity decoupledmaterial shaders and light shaders share only an explicit pixel-attribute contract

第 1 / 3 步 · 几何阶段只写入位置、法线和材质属性

播放三个阶段:几何负责写资料,光负责读资料,最终 pass 负责组合结果。

a readable G-buffer is an API, not a pile of channelsmaterial shaderDL_SetDiffuseDL_SetNormalDL_SetDepthMRT targetsMRT 0diffuse + flagsMRT 1normal + materialMRT 2specular inputsdeptheye-space depthchannel budget is finitelight shaderDL_Get*stable contractstorage may changeshader need notknow whereview-space normals can reconstruct z and save one channel when the sign is known
GPU Gems 3 · Chapter 19

Tabula Rasa Deferred Shading Lab

可交互

改变光数量、光体积、分辨率和透明几何,观察“几何复杂度解耦”如何换来 MRT 写入与带宽成本。

material → MRT → light volumes → composegeometryMRT + depth24 lightspoint volumespoint light volumes keep lighting local; opaque geometry stays in deferred path.
光照覆盖像素0.43 M
估算绘制次数26 calls
render-target 流量66 MB/frame
透明几何路径deferred

本章小结

  • deferred shading 把材质写入与光照读取拆成可扩展阶段
  • MRT 属性必须通过稳定访问器隐藏物理通道布局
  • light volume 让局部光只覆盖真正需要的屏幕像素
  • 深度、法线和累积缓冲可以复用到水面、边缘与 HDR 后处理
  • 透明几何和带宽预算决定 deferred 与 forward 的边界

练习

问题 1|修改 Demo 代码。 如果 material shader 新增一个 roughness 属性,你会怎样修改 MRT 契约,才能避免所有 light shader 立刻重写?

问题 2|诊断带宽瓶颈。 Lab 中 deferred 路径的绘制次数比 forward 少,但把分辨率提高后更慢。你会先检查哪三个数据?

问题 3|场景选型。 不透明建筑、动态水面和需要稳定阴影的草地,分别选择哪条管线?

名词解释

本章出现的专业名词,用大白话再讲一遍。

deferred shading
multiple render targets
material attribute
light volume
light accumulation buffer
forward fallback

资料与写作方式声明

本章以GPU Gems 3 · Chapter 19. Deferred Shading in Tabula Rasa权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…