GPU Gems 2 · Chapter 39. Global Illumination Using Progressive Refinement Radiosity
把 progressive refinement radiosity 改写为 receiver gather,在 GPU 上用纹理、可见性和 form factor 逐轮逼近全局光照。
学习目标
- 能解释 radiosity 如何用 accumulated/residual energy 和 form factor 逐轮逼近表面间的漫反射能量
- 能修改 Radiosity Lab 的 residual、reflectance、gradient 和 quadtree depth,预测传输量与细分候选数如何变化
- 能回答:为什么 GPU 实现选择 receiver gather,以及什么场景适合用低分辨率间接光搭配实时直达光
先把房间里的“余光”分成许多小块
想象把墙壁、地板和桌面切成很多小纸片。灯光照到其中一片后,这片纸又会把一部分能量反射给其他纸片;如果每次都把整间房间的所有互相影响同时解完,账本会大得难以携带。
本章解决的问题是:怎样只追踪当前最值得处理的那一小笔“还没发出去的能量”,并让 GPU 的每个纹理位置都能规则地完成自己的计算?如果把一片面直接“撒”到许多远处位置,就会遇到 GPU 不擅长的任意地址写入和巨大的 pairwise storage。
1. radiosity:把全局光照变成能量转账
↡把场景离散成许多小面元,并计算面元之间传递的漫反射能量;它适合表达软阴影和物体间接反射。Radiosity 把几何表面看成有限个 element。每一对 element 之间能传多少能量,由 form factor 描述;receiver 的 reflectance 决定收到的能量有多少会继续反射。这样,全局光照不再是“每个像素都独立发一堆 ray”,而是面元之间的能量交换。
经典做法把所有 pair 的 form factor 放进一个 N² 大系统,存储很快成为瓶颈。progressive refinement 则在需要时计算当前 shooter 到 receiver 的可见性和 form factor,避免把整张矩阵长期放在 GPU 上。
每个 element 维护两种能量:accumulated 是到目前为止收到的总量,residual 是还没有作为下一轮输入发出的量。初始时光源 element 有指定能量,其余为零;每轮 shooter 发完后 residual 清零,receiver 同时增加 accumulated 和 residual。
2. progressive refinement:优先处理 residual power 最大者
↡反复选择残余功率最高的 shooter,向所有可见 receiver 传能量,并在所有 residual 足够小后停止的迭代过程。选择最大 residual power(能量乘面元面积)是一个收敛策略:它优先消化对最终结果影响最大的未发射能量。一次 shot 会测试 shooter 到其他面元的 visibility,根据 form factor 计算传输,再乘上 receiver 的 reflectance。receiver 获得的结果既加入最终 accumulated,也加入下一轮可传播的 residual。
当所有面元 residual 都小于阈值时停止。阈值越低,画面更接近收敛结果,但要付出更多 shot 和纹理 pass;因此实验中应把“质量”与“剩余能量”同时记录,不能只看某一轮的局部亮度。
3. 用 receiver gather 代替 shooter scatter
↡由每个 receiver 主动读取 shooter 状态、计算自己收到的能量并写回自己位置的 GPU 数据流;它把不规则 scatter 反转成规则 gather。GPU 的纹理和 fragment pipeline 更擅长“每个位置执行相同 kernel”。因此本章不让 shooter 把能量撒到所有 receiver,而是遍历 receiver polygons:每个 receiver 检查自己是否能看见 shooter,随后在自己的 radiosity texel 中计算并写入结果。这样既保留 progressive refinement 的快速收敛,又把内存访问变成规则的 gather。
4. 一次 shot 的 visibility pass 与 reconstruction pass
↡从 shooter 的视角渲染并记录 polygon ID 的中间纹理;receiver 用它判断自己是否被 shooter 看见。一次 shot 分两遍。visibility pass 从 shooter 视角渲染场景,把 polygon ID 写入 item buffer;reconstruction pass 把每个潜在 receiver 以正交投影绘制到与 radiosity texture 同分辨率的 framebuffer,于是一个 fragment 对应一个 receiver texel。fragment shader 读取 item buffer 中投影位置的 ID,通过 visibility 判断和 analytic form factor 得到传输能量。
用 ID 而不是深度可以避免深度精度带来的比较问题,但也意味着不能直接使用硬件 shadow mapping 的 percentage-closer filtering。几何投影的边缘如果过于粗糙,还需要更高的 tessellation,让半球投影后的曲线更接近真实边界。
// reconstruction pass: receiver texel writes its own location
vec4 receiver = texture(receiverTexture, uv);
float visible = texture(itemBuffer, projectedShooterUv).r == receiver.id ? 1.0 : 0.0;
vec3 transfer = shooterResidual * formFactor(shooter, receiver) * visible;
receiverAccumulated += transfer * receiver.reflectance;
receiverResidual += transfer * receiver.reflectance;
outRadiosity = vec4(receiverAccumulated, receiverResidual.r);这里的关键不是某个具体纹理格式,而是写入方向:当前 fragment 只写它自己的 outRadiosity。下一轮重新选择 shooter,再执行 visibility 和 reconstruction,直到 residual power 低于停止条件。
5. texture quadtree:让细分跟着光照变化走
均匀把所有场景面切成相同分辨率,会在平坦墙面上浪费 texels,却可能在阴影边界不够细。Chapter 39 为每个 scene polygon 建一棵 texture quadtree:根节点是粗粒度区域,叶节点保存小的 16 × 16 radiosity texture。
每次 reconstruction 后,fragment program 评估 radiosity gradient;如果梯度超过阈值,说明线性插值无法稳定表达这片区域,就 split 当前节点;如果光照平滑,就保留当前粒度。用 gradient 而不是绝对亮度避免把一整面均匀明亮的墙过度细分。递归在光照平滑或达到最大深度时结束。
6. 性能取舍:把间接光降频,保留直达光细节
小模型可以通过 visibility pass 的 occlusion query 跳过不可见表面,也可以用 substructuring:以低于纹理最终分辨率的粒度进行 shooting,把 mipmap 或排序得到的层级用于下一 shooter 选择。选中同一 polygon 后,让其多个 element 连续 shooting,可以摊薄 mipmap 和排序成本;批量处理多个 shooter 还能减少 context switching。
如果目标是实时帧率,可以把间接光当作低频信号:用低分辨率 radiosity texture 计算 indirect,第一轮后清零 accumulated 中的 direct 部分;direct illumination 另用逐像素光照、shadow volume 等高频方法计算,最后将两者相加。这样牺牲部分间接光细节,换取更低的纹理与 fragment 成本。
先猜一猜:在下面的实验中把 reflectance 调高,transferred energy 会怎样?把 gradient 调高而保持 max depth 不变,split candidates 会不会立刻等于全部 leaf texels?打开实验后一次只改一个控件,比较 gather 和 scatter 两种访问方向。
receiver gather:每个 texel 写自己的位置,适合 GPU 数据并行。
三步验收:从残余能量到自适应光照纹理
第一步:记录能量并选 shooter
把每个面元的 accumulated 与 residual 分开保存;从光源初始化 residual,再按 residual power 选择下一次 shooter。只有当前 shooter 的残余能量被发射完,才进入下一轮选择。
本章小结
- radiosity 用 form factor 描述面元间能量转移。
- progressive refinement 优先发射 residual power 最大的 shooter。
- receiver gather 让每个 texel 写自己的位置,绕开 scatter。
- visibility/reconstruction 两遍完成一次 shot 的可见性与重建。
- texture quadtree 按 radiosity gradient 自适应细分,低频 indirect 可降分辨率。
练习
问题 1|排出一次 shot。 请按顺序写出 shooter 选择、visibility pass、receiver reconstruction、residual 更新和下一轮停止判断,并说明为什么 receiver 需要同时更新 accumulated 与 residual。
问题 2|修改 Demo 代码。 为 Radiosity Lab 增加一个 occlusion-query 开关:只对 visibility pass 中有可见 receiver 的节点运行 reconstruction。你要防止什么误判?
问题 3|场景选型。 一面平坦墙和一道锐利阴影边界使用同样的 quadtree split 规则。哪个区域应该得到更深的叶节点?为什么不用绝对 radiosity 值判断?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- radiosity
- shooter
- receiver
- progressive refinement
- receiver gather
- item buffer
- texture quadtree