GPU Gems 2 · Chapter 38. High-Quality Global Illumination Rendering Using Rasterization

把 final gathering 的同向射线批处理改写成 rasterization,用 depth peeling 在 GPU 上加速离线高质量全局光照。

学习目标

  • 能解释为什么高质量全局光照要把间接光加入可见点,并区分粗略第一遍与 final gathering 第二遍
  • 能修改 Quality Lab 的样本数、depth peeling 层数和停止阈值,预测 raster passes 与渐进画质如何变化
  • 能回答:什么时候选择 parallel clustering 的 rasterization 管线,什么时候不能把它当成交互式 ray tracer

先追一束“绕过墙壁”的光

想象房间里有一盏灯。直达光能照亮桌面,但墙壁反射过来的柔和亮度,才让桌下和角落不至于全黑。要把这种反射画出来,就要在每个可见位置向周围发出很多探针,看看各个方向上最后遇到的物体带来了多少光。

本章解决的问题是:这些探针数量巨大,CPU 逐条追踪会很慢;怎样把它们排列成图形硬件擅长的“同向批次”,让 rasterization 同时找到大量交点?如果只用很少探针或只算直达光,画面会出现噪声、漏光和缺少反射层次。

只看直达光,房间会失去反射带来的亮度direct illuminationlight光源 → 表面加入反射indirect illuminationlightwall bounce → 可见点

1. 先算一个便宜的全局光照近似

全局光照的难点不是画出一盏灯,而是估计“从半球各方向到达当前点的光”。路径追踪可以直接做这件事,但要让噪声下降,通常需要大量 rays。GPU Gems 2 Chapter 38 选择先做一遍便宜的近似,再把精力留给最终图像中真正需要的可见点。

第一遍可以使用 CPU 上的 grid photon mapping,或任何能快速产生 irradiance distribution 的方法;结果可以写成细分网格的 vertex colors,也可以放进 light map 或 volume texture。关键不是第一遍必须用哪一种算法,而是第二遍能以简单纹理/顶点读取拿到它。

两遍计算:先准备粗略光照,再高质量收集pass 1photon / irradiance 粗估便宜,但不直接交付irradiance datapass 2final gathering所有可见点一起处理finalimagedirect + indirect

2. final gathering:所有可见点都要问一遍半球

最终收集可以作用在屏幕上所有可见点,而不只是一小组稀疏点。这样不需要在稀疏点上做完计算后再插值,也更容易逐步显示结果:先用少量样本得到一张粗图,继续累积直到质量足够。

代价也很明确:如果有 P 个可见点、每点 N 个方向,就至少要处理 P × N 个 ray samples。原书给出的典型场景中,final gathering 的 rays 数量远超从相机出发的 rays,因此它是全流程瓶颈。

3. parallel clustering:把不同 origin 的同向 rays 放进一个 pass

半球聚类让每个可见点各自选择方向,采样更像传统 ray tracing;parallel clustering 则把方向离散成固定集合。对于其中一个 global ray direction,所有 origins 不同但方向相同,GPU 只需要沿这个方向做一次 parallel projection,再从投影结果中找到各点的最近交点。

把“每点不同的射线”改成“同向的一批投影”hemispherical clustering每个 origin 选不同方向不容易合成一次 raster passparallel clustering同一 global ray direction一次平行投影批量追踪

4. depth peeling:用多层 rasterization 找最近交点

一次普通投影只会留下一个深度层,但一条 ray 可能穿过多个物体。depth peeling 先用 reversed depth test 得到当前方向上的远层,把深度写入 B;下一轮把 B 复制到 C,拒绝比 C 更远的片元,于是逐步揭开下一层。对于每个可见点,只有投影命中位于它前方的交点才会写入采样结果 E;当更近的命中出现时覆盖旧结果。

// one depth-peeling iteration for a fixed global direction
if (projectedPointIsInFront(sample, visiblePoint, globalDirection)) {
  if (sample.depth < sampledDepth) {
    sampledDepth = sample.depth;
    bufferE = sample.irradiance;
  }
}
depthBufferB = renderFarthestLayer(globalDirection, depthBufferC);
depthBufferC = depthBufferB;

这段伪代码强调两个状态:sampledDepth/bufferE 保留当前最近命中,bufferB/bufferC 让下一轮不重复消费已经剥出的层。真实实现会把这些状态放在纹理和 framebuffer 中,循环直到没有足够多的新片元,或达到允许的 fragment 阈值。

depth peeling:把一条射线上的交点逐层揭开沿 global direction0layer 0 · near1layer 12layer 2 · farreversed depth testB / C buffersB:本轮最远深度C:上一轮边界copy B → CDsampledlayernearest kept

5. 把 visible buffer、投影与累积接成 GPU 管线

开始 final gathering 前,先从相机渲染场景,把每个可见点的位置、法线和颜色写入浮点缓冲 A。一个 global ray direction 接着驱动平行投影:从 A 的投影位置读取当前深度层 D,做 depth peeling 得到最近交点,写入 E,再将 E 的 irradiance 累加到 indirect illumination buffer。

每个 global ray direction 都走一轮可并行的管线buffer Apositionnormal + colorparallel projectionread D at projected Mdepth peeling B/Cwrite nearest into EEnearesthit+indirect没有为每个点维护一棵 GPU ray-tracing acceleration structure只改变 direction,就能处理下一批同向 rays
for (Direction direction : sampledDirections) {
  clear(depthB, maxDistance);
  clear(depthC, maxDistance);
  clear(sampledLayer, environmentColor);
  do {
    renderFarthestLayer(scene, direction, depthB, sampledLayer);
    copy(depthB, depthC);
    sampleVisiblePoints(bufferA, sampledLayer, bufferE, direction);
  } while (newFragments > stopThreshold);
  accumulate(bufferE, indirectBuffer);
}

这段结构把原书的十步实现压缩成“方向循环”和“深度层循环”两层。direct illumination 仍可独立用 shadow mapping 等快速技术求出,最后再与 indirect buffer 相加;因此提高 final gathering 样本数不会替代直达光的计算。

6. 质量、aliasing 与离线取舍

这套方法不是把 GPU 变成交互式 ray tracer。它的目标是离线高质量图像:让所有可见点同时处理,逐步累积样本,并用足够 coherent 的 rays 换取更高吞吐。样本方向可以使用 quasi-Monte Carlo 或其他稳定序列,减少移动场景中 photon shooting 随机性造成的 flickering 和 popping。

平行投影使用有限分辨率 buffer,所以会像 shadow mapping 一样产生 aliasing,甚至漏掉间接光。增加样本数不能自动修复这个问题;应从投影分辨率、深度层条件和阴影映射类的抗 aliasing 策略一起检查。允许少量未处理 fragments 作为停止阈值,可以减少多余迭代,但要明确这是质量与时间的选择。

final gather 的四个关键帧visible pointsP / N / colorprojectionglobal directiondepth peelingnearest hitaccumulateindirect light每次只处理一个预先采样的方向;换方向后重复同一套 raster passes这是离线高质量渲染管线,不承诺交互帧率

第 1 / 4 步 · 准备可见点

把最终收集拆成用户可观察的阶段:可见点、同向投影、深度层和累积。

先猜一猜:在下面的实验中把 samples 从 32 调到 64,再把 stop threshold 从 2% 调到 0%,哪个指标会增加,哪个指标只是让渐进画面更细?打开实验后一次只调一个参数,比较 parallel clustering 与 per-point directions。

同向 rays 一起走 raster pipelineprogressive imagesample count ↑ → noise ↓threshold 2% → 及早停止
visible points (teaching set)128
ray samples4096
direction batches32
raster/depth passes128
accepted fragments125

同向射线共享投影与 depth peeling,适合把所有 visible points 一起处理。

三步验收:从粗略 irradiance 到可停止的最终图像

分步1 / 3

第一步:准备两遍数据

先用便宜方法得到可读取的 irradiance 分布,再把相机看到的位置、法线和颜色写入 visible buffer A。不要把第一遍误当成最终图像;它的任务是提供第二遍每次命中时要读取的光照信息。

只看直达光,房间会失去反射带来的亮度direct illuminationlight光源 → 表面加入反射indirect illuminationlightwall bounce → 可见点
两遍计算:先准备粗略光照,再高质量收集pass 1photon / irradiance 粗估便宜,但不直接交付irradiance datapass 2final gathering所有可见点一起处理finalimagedirect + indirect

本章小结

  • 两遍方法让第一遍只负责快速提供 irradiance。
  • final gathering 在所有可见点上估计半球入射光。
  • parallel clustering 用固定 global ray direction 批量投影。
  • depth peeling 逐层排除交点,找到每个点的最近命中。
  • rasterization 加速离线高质量光照,但仍需处理 aliasing 与停止阈值。

练习

问题 1|排出一次 raster final gather。 给定 visible buffer A、深度缓冲 B/C、采样层 D 和结果缓冲 E,请按实际依赖顺序写出一次 global ray direction 的核心步骤,并说明 B 为什么要复制到 C。

问题 2|修改 Demo 代码。 为 Quality Lab 增加一个 jittered-directions 模式:每轮只改变方向序列,不改变 depth peeling 逻辑。你要额外观察什么现象?

问题 3|场景选型。 一个离线建筑镜头需要高质量间接光,另一个实时游戏镜头要求稳定 60 fps。哪个更适合本章方法?还需要检查哪两个限制?

名词解释

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

global illumination
final gathering
parallel clustering
global ray direction
depth peeling

讨论

评论区加载中…