Deferred Shading 延迟着色法
Forward Rendering 的流程是:
Geometry
↓
Fragment
↓
Lighting
↓
最终颜色也就是说,每个物体的 Fragment Shader 直接把光照算完。光源很多时,大量 Fragment 会反复执行同一套光照循环。
Deferred Shading 把过程拆开:
Geometry Pass
↓
先保存“之后算光需要的数据”
Lighting Pass
↓
再统一读取这些数据计算光照“Deferred”指的就是:把光照计算延后到第二个 Pass。
核心变化
Forward:
Position / Normal / Material
↓
立刻遍历 Lights
↓
ColorDeferred:
Position / Normal / Material
↓
G-Buffer
↓
之后统一遍历 Lights
↓
Color光照公式没有换,改变的是执行时机和数据组织方式。
G-Buffer
G-Buffer 是一组屏幕大小的纹理,记录最终可见表面的几何和材质信息。
LearnOpenGL 官方使用:
gPosition
→ World-space Fragment Position
gNormal
→ World-space Normal
gAlbedoSpec.rgb
→ Diffuse / Albedo
gAlbedoSpec.a
→ Specular intensity一个屏幕像素在 Geometry Pass 后已经记录了:
它在哪里
它朝哪里
它是什么颜色
它有多少镜面反射Geometry Pass
官方 Fragment Shader 一次输出三个颜色附件:
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;写入:
gPosition = FragPos;
gNormal = normalize(Normal);
gAlbedoSpec.rgb =
texture(texture_diffuse1, TexCoords).rgb;
gAlbedoSpec.a =
texture(texture_specular1, TexCoords).r;这里不计算光照。
因此:
Geometry Pass
= 把后续光照需要的数据先收集起来MRT 和 FBO
G-Buffer FBO:
G-Buffer
├─ Color Attachment 0 → Position
├─ Color Attachment 1 → Normal
├─ Color Attachment 2 → Albedo + Specular
└─ Depth Attachment然后通过多个 Draw Buffers 一次写入。
这与 Bloom 一节一次输出 Scene / Bright Texture 是同一个 Multiple Render Targets 机制。
官方 Position 和 Normal 使用浮点纹理,因为它们可能有负值或超过 1:
Position = (-3.4, 1.2, 8.7)
Normal = (-0.6, 0.4, 0.7)Lighting Pass
第一遍完成后,第二遍只画一个 Fullscreen Quad。
当前屏幕像素直接从 G-Buffer 取回:
vec3 FragPos =
texture(gPosition, TexCoords).rgb;
vec3 Normal =
texture(gNormal, TexCoords).rgb;
vec3 Diffuse =
texture(gAlbedoSpec, TexCoords).rgb;
float Specular =
texture(gAlbedoSpec, TexCoords).a;然后继续使用熟悉的 Blinn-Phong:
vec3 lightDir =
normalize(lights[i].Position - FragPos);
vec3 diffuse =
max(dot(Normal, lightDir), 0.0)
* Diffuse
* lights[i].Color;
vec3 halfwayDir =
normalize(lightDir + viewDir);
float spec =
pow(
max(dot(Normal, halfwayDir), 0.0),
16.0
);所以 Deferred Shading 并没有发明新的光照模型。
它只是从:
“当前物体的 Fragment 现场算光”变成:
“先把最终可见表面的数据记录下来,
再按屏幕像素统一算光”大量光源时为什么有意义
Deferred Lighting 面向最终屏幕像素,而不是让每个物体自己带着全部光源列表去完成着色。
这特别适合:
大量动态点光源
城市夜景
霓虹场景
室内很多局部灯不过基础版本仍然会让每个屏幕像素遍历所有光源,所以光源数量极大时依然昂贵。
Light Volume
点光源经过衰减后,只在有限范围内有明显贡献。
官方 8.2 增加 Radius:
float distance =
length(
lights[i].Position - FragPos
);
if(distance < lights[i].Radius)
{
// calculate lighting
}思想是:
Point Light
↓
只有有限影响半径
↓
只在这个范围内考虑它这是进一步优化 Deferred Lighting 的基础。
G-Buffer 的代价
Deferred 不是免费性能。
每帧都需要写和读:
Position
Normal
Albedo + Specular
Depth所以它会增加:
显存占用
Framebuffer 带宽
纹理读取带宽分辨率越高,这个代价越明显。
透明物体
传统 G-Buffer 通常只保存每个像素最前面的一个表面。
透明物体却需要同时考虑前后多个表面和混合顺序,因此常见做法是:
Opaque
→ Deferred
Transparent
→ Forward实际引擎常是 Hybrid,而不是所有东西都 Deferred。
Depth 为什么还要复制
Lighting Pass 用 Fullscreen Quad 输出到 Default Framebuffer 后,默认 Depth Buffer 并没有 Geometry Pass 的深度。
官方还会继续画 3D Light Boxes,所以先把:
G-Buffer Depth
↓ copy
Default Framebuffer Depth这样后画的 Light Cubes 才能和原场景正确做 Depth Test。
Forward 与 Deferred
Forward:
Geometry
↓
Material
↓
Lighting
↓
ColorDeferred:
Geometry
↓
G-Buffer
├─ Position
├─ Normal
└─ Albedo / Specular
↓
Lighting Pass
↓
Color可以把差别概括为:
Forward
→ 物体在第一次绘制时就完成光照
Deferred
→ 第一次只记录表面信息
→ 第二次才完成光照与前面章节的连接
Framebuffer:
提供离屏附件Bloom:
演示 MRT 一次输出多个附件Normal Mapping:
决定最终写进 G-Buffer 的 NormalHDR / Bloom:
可以继续接在 Deferred Lighting 后面完整链路:
Geometry
↓
G-Buffer
↓
Deferred Lighting
↓
HDR
↓
Bloom
↓
Tone Mapping
↓
Gamma
↓
Screen