GPU Gems 3 · Chapter 13. Volumetric Light Scattering as a Post-Process

从屏幕空间射线采样出发,解释体积光散射后处理如何用遮挡预处理、衰减和采样预算生成可控的光束,并处理边缘、前后景与带宽问题。

学习目标

  • 能解释体积光散射后处理如何把屏幕空间光源、像素射线和衰减样本串进片元着色阶段
  • 能修改 Volumetric Light Scattering Lab 的遮挡方法、采样数、density、decay 和 exposure,观察光束质量与带宽的取舍
  • 能回答:为什么黑色 occlusion pre-pass、stencil 标记和 guard band 分别解决哪一种屏幕空间错误

先把“空气里的光”想成一张图像上的柔软笔刷

现实中的光不会只在物体表面停住;空气里的微粒会让光在传播途中散开,背光物体就像在空中切出一束束可见的亮线。实时渲染若要逐体素追踪这种传播,交互场景很快会承受不起。

本章解决的问题是:能不能只拿已有的场景图像,在屏幕上沿着每个像素指向光源的方向取样,近似出带遮挡的光束?这样无需为每个动画物体预建体积,但也必须接受屏幕空间近似带来的漏光、条纹和边缘问题。

1. 空气不是透明背景:散射和遮挡一起决定光束

在 participating media 中,观察者看到的不只是物体反射的光,还包括光在视线附近被散射进来的那一部分。对太阳或明亮天空来说,遮挡物会减少从光源到当前像素的有效照明;我们的后处理只需要估算这种减少的比例。

当许多散射光沿着太阳方向形成扇形亮束时,它们常被称为 。本章的关键取舍是:把复杂的空气传播压缩成一张 source image,再把光束作为 additive post-process 叠回场景。

scene image → screen-space ray → additive scatteringsource frame任意动画场景lightpixel samplesdensity · decay · weightshaftsoutput只处理屏幕上的像素,不要求预先生成体积网格或为每个物体同步设置场景
把复杂场景压成一张图像:后处理只沿像素到屏幕空间光源的方向取样,再把衰减后的样本相加。

2. 屏幕空间射线:用一条向量替代体积积分

后处理先把世界空间的光源通过 world-view-project 变换投到屏幕,得到 ScreenLightPos。对每个像素,计算它和光源之间的差,再除以样本数并乘上 density,得到每一步的 deltaTexCoord。连续沿这个方向读 source image,就形成一条从像素“走向光源”的二维射线。

one pixel, one ray, many image sampleslightreceiver pixel12345678deltaTexCoord = (pixel − light) / samples × densityincrease density → smaller separation → brighter shafts over a shorter screen-space rangesample hit emissive region → add light;sample hit occluder → reduce the accumulated source
每个像素拥有自己的屏幕空间射线;样本越密,条带越少但纹理带宽与像素着色成本越高。

这里的样本不是完整的空间点:它只知道屏幕坐标,不知道被屏幕外的建筑遮挡,也不知道前后景的真实体积关系。因此它最适合明亮 emissive region 对比暗色 occluder 的场景;当 source image 的纹理比光照更强时,纹理变化也会被误读成光束。

每次累加可以同时乘上 weight 和 decay^i,最后再乘 exposure。weight 调单个样本的存在感,decay 让远离光源的样本逐步变弱,exposure 则是整个后处理的最终亮度旋钮。

additive samples with controlled falloffcontributionsample index i → farther from lightdecay 0.82decay 0.92weight onlyweight ↑ 或 exposure ↑ → 更亮;decay ↓ → 更快落下、shaft 更短
三个参数分工不同:weight 改每次加多少,decay 改沿射线走远后的衰减,exposure 最后统一缩放输出。

3. 后处理像素着色器:每个样本做同一件小事

核心循环不需要知道场景几何,只需要维护当前纹理坐标、采样颜色和一个逐步衰减的 illumination factor。现代 WebGL2/GLSL 可以把它写成下面的等价伪代码:

vec2 delta = (texCoord - screenLightPos) * density / float(sampleCount);
vec3 color = texture(frameSampler, texCoord).rgb;
float illuminationDecay = 1.0;
 
for (int i = 0; i < sampleCount; ++i) {
  texCoord -= delta;
  vec3 sampleColor = texture(frameSampler, texCoord).rgb;
  sampleColor *= illuminationDecay * weight;
  color += sampleColor;
  illuminationDecay *= decay;
}
 
return vec4(color * exposure, 1.0);

这段逻辑的优点是适用于任意动画图像、半透明物体和多个光源:每个光源都可以执行一次 screen-space pass,再做 additive blend。代价则是纹理读取数随 sampleCount 增长,低分辨率或低样本时还会出现规则条带。

quality / bandwidth / pass countfull resolutionsharp · bandwidth highdownsamplefilter · less BWextra light× passes先决定像素预算,再决定 samples、分辨率、filter 和 pass 数stochastic pattern 可打散低采样规则条带,但需要 temporal / spatial 过滤配合
降低分辨率可以减纹理带宽,但需要过滤与更好的采样图案来控制条带;多光源则按光源数量重复 screen-space pass。

4. 先处理遮挡,再让光束叠加

最直接的 occlusion pre-pass 是先用黑色、无纹理材质绘制遮挡物,再用这张 buffer 生成光束,最后把普通着色场景和光束结果合成。这样做不需要额外的几何体积,也能把建筑的颜色纹理从遮挡判断里剥离。

如果管线已有 stencil 或 alpha 位,可以在绘制 emissive region 时设位,在绘制遮挡物时不设位,后处理只让带位的样本参与 additive blend。无法改 source buffer 时,降低纹理对比度、使用雾和 aerial perspective,或让光束朝屏幕边缘和垂直方向 fade,也能压低错误条纹。

screen-space occlusion:让样本看见“遮挡”,而不是看见纹理pre-passoccluders = blackthen add shaftsstencilemissive pixels markedcontrastfog / aerial perspectivehides streaking选择依据:能改 source buffer → pre-pass;已有 bit budget → stencil;不能改内容 → contrast / fog / fade多光源可以逐个 screen-space pass 做 additive blend
屏幕空间采样把颜色当作可见性线索;pre-pass 与 stencil 先去掉纹理干扰,contrast 方法则降低错误条纹的可见度。

5. 屏幕空间近似的边界与工程预算

当遮挡物穿过屏幕边缘时,射线会突然读不到屏幕外的样本,于是光束闪烁;扩大可渲染区域形成 guard band 可以增加可寻址范围。近光源还可能出现背景光束穿到前景物体前面的错觉,这是因为后处理缺少完整的深度排序。光源接近屏幕的垂直方向时,投影位置趋向无穷,应该 clamp 到 guard-band 或逐渐 fade。

降低 source image 分辨率可以减少带宽;配合 bilinear filtering 能缓解块状采样,stochastic sampling 则能打散低样本时的规则条纹。多光源意味着多个 additive pass,因此应先用 GPU profile 观察纹理读取、片元数量和合成次数,再决定样本数与分辨率。

post-process caveats:屏幕空间不是完整体积edge flicker → guard bandshaft in front → depth caveatperpendicular → clamp / fade近似有效的前提:emissive region 比 occluder 明亮,且采样射线仍在可见屏幕范围内
这是屏幕空间近似的边界:guard band 减少边缘闪烁,深度/遮挡处理缓和前后景错误,clamp 或 fade 避免光源投影趋于无穷。

6. 三步把效果变成可调的后处理路径

分步1 / 3

先确定屏幕空间射线

把光源投影到屏幕;对每个像素计算指向光源的 delta;用 samples 和 density 决定射线覆盖范围。先在无遮挡图像上观察光束的几何方向,再加入可见性处理。

one pixel, one ray, many image sampleslightreceiver pixel12345678deltaTexCoord = (pixel − light) / samples × densityincrease density → smaller separation → brighter shafts over a shorter screen-space rangesample hit emissive region → add light;sample hit occluder → reduce the accumulated source
每个像素拥有自己的屏幕空间射线;样本越密,条带越少但纹理带宽与像素着色成本越高。
scene image → screen-space ray → additive scatteringsource frame任意动画场景lightpixel samplesdensity · decay · weightshaftsoutput只处理屏幕上的像素,不要求预先生成体积网格或为每个物体同步设置场景
把复杂场景压成一张图像:后处理只沿像素到屏幕空间光源的方向取样,再把衰减后的样本相加。

Volumetric Light Scattering Lab

猜一猜:把 samples 或 exposure 调大,光束会更稳定还是只会更亮?把 occlusion mode 切到 none,又会暴露哪一种 screen-space 错觉?

prepass · 24 samples · density 0.75lightoccluderpost-process light shafts · longer bars mean wider screen-space reachstreaking15%occlusion leakage18%shaft brightness81%sample bandwidth: 37 units · conceptual trend, verify with GPU profile

黑色无纹理 pre-pass 把遮挡物与材质颜色分开,适合能控制 source buffer 的管线。

本章小结

  • 后处理用屏幕空间射线近似空气中的体积光散射
  • samples 与 density 决定射线覆盖和采样成本
  • decay 控制沿光路的衰减,weight/exposure 控制亮度层级
  • pre-pass 或 stencil 把遮挡和材质纹理分开
  • guard band、filter 与 profile 处理屏幕边界和带宽取舍

练习

问题 1|修改 Demo 代码。 把下面的循环改为“越远离光源,样本贡献越小”,并指出 decay 的初始值为什么应该不大于 1。

vec3 color = texture(frameSampler, texCoord).rgb;
for (int i = 0; i < sampleCount; ++i) {
  texCoord -= delta;
  color += texture(frameSampler, texCoord).rgb * weight;
}

问题 2|诊断遮挡错误。 你看到墙面纹理沿光源方向形成规则条纹,但光束本身方向正确。你会先切换哪一种 occlusion method?为什么?

问题 3|场景选型。 一个室外太阳光场景在低分辨率后处理下出现屏幕边缘闪烁,另一个近距离灯光场景出现背景光束穿到前景物体前面。分别怎样处理?

名词解释

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

participating media
crepuscular rays
screen-space sampling
attenuation
occlusion pre-pass

讨论

评论区加载中…