篇目 01 · 章簇精读 | 第 23–26 章 | 本批:第 1–7 章

实时的全局光照是怎么被"混合"出来的

Ray Tracing Gems 第 23–24 章精读(上):第 23 章把 GI 存进贴图空间,讲 texel 的调度、后处理的滤波顺序与三种加速收敛的手段;第 24 章改用光子映射,讲光子怎么撒、核怎么变形、滤波怎么拆成可裁剪的细节系数。

原文:Interactive Light Map and Irradiance Volume Preview in Frostbite(第 23 章) 原文:Real-Time Global Illumination with Photon Mapping(第 24 章) Diede Apers 等 · EA Niklas Smal、Maksim Aizenshtein · UL Benchmarks Ray Tracing Gems(Apress, 2019)第 23–24 章 · p.399–456 CC BY-NC-ND 4.0
7一级章节(本批;全篇 13)
14,129正文汉字(本批)
22图(原文 22 · 自绘 0)
8边界与反例(本批)

图-段配对表(本批 22 张)

图角色档归属支撑的论断讲解层次
图 1-1E 证据1三种混合路线的成品观感已达可用①②
图 1-2E 证据1混合渲染在游戏内的实际效果①②
图 1-3E 证据1延迟混合路径追踪的成品观感①②
图 2-1E 证据2光图作用在代理几何上、再映射到实机网格①②
图 2-2E 证据2辐照度体积服务动态物体;透明走 any-hit①②
图 2-3E 证据2三类辐照度缓存各自覆盖什么①②
图 3-1S 示意3单 texel 的辐照度由一条路径逐顶点累积①②③
图 3-2S 示意3调度策略与大缓冲的分桶累积①②
图 4-1E 证据4膨胀滤波修补无效 texel 的效果①②④
图 4-2E 证据4合并膨胀与降噪各自的贡献②③
图 5-1S 示意5缓存把逐顶点可见性射线换成一次纹理取样①②③
图 5-2E 证据5视野优先带来的收敛增益②③
图 5-3E 证据5辐照度缓存的收敛增益与交叉点②③
图 5-4E 证据5降噪带来的收敛增益②③
图 5-5E 证据5三种手段叠加后的视觉对照②③④
图 5-6S 示意5编辑器与求解器异步分跑两块 GPU①②
图 6-1S 示意6算法的 pass 级结构:光栅化 RSM → 追踪 → splat → 滤波①②
图 6-2E 证据6光子映射系统的最终成像①②
图 6-3E 证据6算法在商用跑分场景 3DMark Port Royal 上运行①②④
图 7-1S 示意7小波重要性采样重塑采样点位置①②③
图 7-2S 示意7核空间的基向量与三轴椭球的形状修改①②③
图 7-3S 示意7平稳小波三次迭代的支撑结构①②③
角色档:E = 承载可核对数值或作为唯一证据(禁自绘)|S = 纯机制示意(可自绘,须过留痕/增益/配额三条准入)。本批 22 张全部为原文引用,自绘 0 张。
讲解层次:①指认 ②读法(两者归图注)|③推导 ④边界(两者归正文)。
来源与许可:全部来自 Ray Tracing Gems 第 23–24 章,CC BY-NC-ND 4.0,原样引用、逐图署名、未修改。第 23 章图号经核对为 23-1 至 23-20 连续无缺,本批用到其中 16 张;第 24 章图号为 24-1 至 24-9 连续无缺,本批用到其中 6 张。
本批缺口:原文 23-4(texel 上样本分布)、23-5(管线总览)、23-11(À-Trous 三个 pass)、23-12(本地光查询密度)四张尚未取到,将在提取器修复后补入(票 21)。第 24 章的 Table 24-3 / 24-4 为无框矢量表格,未取到数值层,其数值复算列入人工门。
原文摘要(英文原样引用,p.399)

This chapter presents the real-time global illumination (GI) preview system available in the Frostbite engine. Our approach is based on Monte Carlo path tracing running on the GPU, built using the DirectX Raytracing (DXR) API. We present an approach to updating light maps and irradiance volumes in real time according to elements constituting a scene. Methods to accelerate these updates, such as view prioritization and irradiance caching, are also described. A light map denoiser is used to always present a pleasing image on screen. This solution allows artists to visualize the result of their edits, progressively refined on screen, rather than waiting minutes to hours for the final result using the previous CPU- based GI solver. Even if the GI solution being refined in real time on screen has not converged after a few seconds, it is enough for artists to get an idea of the final look and assess the scene quality. It enables them to iterate faster and so achieve a higher-quality scene lighting setup.

本篇要点
  1. 第 23 章解决的是静态 GI 的预览问题:CPU 烘焙按分钟到小时计,美术迭代等不起;这套方案把 GI 求解搬到 GPU,并把结果存进贴图空间。
  2. 核心结构是贴图空间的光图 + 三类辐照度缓存,两者分工不同:光图给静态几何,缓存给动态物体与高频光照。
  3. 收敛靠三个加速器叠加:视野优先决定先算哪些 texel、光源加速结构降低每次查询成本、辐照度缓存把逐顶点的可见性射线换成一次纹理取样。
  4. 本批会把原文的全部量化数据复算一遍,其中两处复算给出了原文没写明的结论:预算器把两个测试场景拉到了同一耗时水位,而收敛态恰好贴满 33 ms 的刷新预算。

1光线预算不够时,混合是怎么想的

实时渲染里,全局光照一直是"看得见、算不起"的那一项。路径追踪要把每一条光路的完整历史算出来,而一帧只有 16 到 33 毫秒。第 23 章给出的答案属于"混合"这个大类:不去追求物理上完整的路径,而是把 GI 的计算结果存到一个可以复用的空间里,让每一帧只补算这个空间里还没收敛的部分。

先划清本篇的范围。这一簇四章(第 23–26 章)都在回答"光线预算不够时怎么办",但答案分四条路:把 GI 存进贴图空间(第 23 章)、用光子映射替代路径追踪的后段弹射(第 24 章)、把反射与阴影拆到不同分辨率与不同空间(第 25 章)、把漫反射与镜面反射的追踪分开(第 26 章)。本篇只讲第一条路,也只讲静态 GI。动态 GI、降噪器的完整理论、以及另三条路的实现细节,不在本篇展开。

1.1"混合"在这套方案里有三层含义

第 23 章的做法同时混了三种东西,这正是它能在 GPU 上跑起来的原因,也是它引入全部近似与偏差的来源。

混合的维度做法换来了什么代价
计算空间GI 存进 UV 展开的贴图,而不是像素与相机视角解耦,转个身不用重算UV 接缝与 texel 拉伸;细节受 texel 密度限制
计算时机逐帧渐进,只补算未收敛的 texel几秒内逼近收敛,而非一次算完结果随时间变化,中间态可见
算法层次低频用缓存近似,高频才追踪把逐顶点可见性射线换成一次取样引入约 4.5% 的误差偏置

这三层混合里,第一层是这套方案与屏幕空间 GI 的根本分野。屏幕空间把 GI 存在像素上,代价是屏幕外的东西算不到;贴图空间把 GI 存在表面参数域上,代价变成了 UV 参数化的质量。理解这条取舍,后面所有的设计细节都能找到位置。

怎么读

  • ①指认 三幅画面分别来自三个不同的生产环境(石砌仓库、科幻测试场景、卡通风格的室外场景),都是关闭了可见接缝的成品。
  • ②读法 看三张图的共同点,也就是间接照明该有的样子:背光面不黑、颜色会在邻近表面之间渗透;再看差异,也就是三类场景的材质尺度差别很大。
三个不同环境的最终成品图
图 1-1 三个不同环境的最终成品图。来源:Ray Tracing Gems, Fig. 23-1, p.400 · CC BY-NC-ND 4.0 · 原样引用,未修改

这张图只承载"观感达到可用"这一条判断,它不承载任何可核对的数值。读它的时候要留意这个界限:成品图能证明画面好看,证明不了收敛快。后者要靠第 5 章那三条收敛曲线。

1.2这一条路在四条路里的位置

另外两条路各有自己的解法,放在一起看能看清第 23 章的边界。第 25 章(PICA PICA)不把 GI 存进贴图,而是把着色分解成漫反射与镜面反射两条流水线,各自用不同分辨率的光线数;第 26 章更进一步,把漫反射与镜面反射的追踪路径本身分开,漫反射走屏幕空间或体素,镜面反射才走完整光追。

混合光线追踪在 PICA PICA 中的效果
图 1-2 混合光线追踪在 PICA PICA 中的效果。来源:Ray Tracing Gems, Fig. 25-1, p.458 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 一幅游戏内截图,场景以大面积反射与透射材质为主。
  • ②读法 注意反射面里的内容是否与场景一致、透明物体后的光是否被染色。这两处是"混合"路线最容易露馅的地方。

把三条路的成品放在一起,能得到一个粗略的规律:越靠前地复用计算结果,能省的光线越多,但对场景类型的要求也越苛刻。贴图空间要求 UV 可用,屏幕空间要求相关几何在视野内,时间复用要求场景变化缓慢。第 23 章的方案能同时应付静态几何与动态物体,代价是它只算漫反射互反射:金属表面按电介质估计反照率,而且不产生焦散。

延迟混合路径追踪的成品图
图 1-3 延迟混合路径追踪的成品图。来源:Ray Tracing Gems, Fig. 26-1, p.495 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 一幅室内场景的成品图,画面里既有漫反射为主的墙面,也有镜面反射的地面与金属构件。
  • ②读法 分别盯住漫反射区与镜面区,判断两处的质量是否一致:第 26 章的做法把这两类反射的追踪路径分开了,差异正是它的代价所在。
边界(第 1 章)
  • 这套方案只处理静态 GI。动态 GI 需要大量离线烘焙,近似更粗,运行时更新也更贵。
  • 只算漫反射互反射。材质按 Lambert 处理,金属按电介质估计反照率,焦散不在计算范围内。
  • 成品图(图 1-1、1-2)不能用来判断收敛速度,那是第 5 章三条曲线的职责。

2把 GI 存进贴图空间:光图与辐照度体积

这一章讲清楚"存哪里"和"存什么"。整套数据分为两类:一张光图(light map)承载每个 texel 的辐照度,几类辐照度缓存承载动态物体与高频光照的间接贡献。两者并非主备关系:它们是分工关系,光图管静态几何的细节,缓存管光图算不准或来不及算的部分。

2.1光图的载体是代理几何,不是实机网格

光图铺在一套简化的代理网格上,并不铺在游戏实际使用的网格上。求解完成后,再把代理网格的 UV 与法线映射回实机网格。原文用战地 II 的场景说明了这条映射链路。 你可以把它想成在墙上贴投影画布:先在画布上把光照算好,再把这块画布对齐糊到真正的墙面上。

怎么读

  • ①指认 左右两幅是同一场景的两种呈现,左边是施加了光图的代理网格,右边是把代理 UV 与法线贴图映射之后的实机网格。
  • ②读法 对比两侧的间接光是否落在同一个位置、明暗边界是否对齐:对齐说明映射链路没出错,错位则说明 UV 有偏差。
光图在星战战地II场景中的应用
图 2-1 光图在星战战地II场景中的应用。来源:Ray Tracing Gems, Fig. 23-2, p.401 · CC BY-NC-ND 4.0 · 原样引用,未修改

用代理几何的好处有两个:解算器不必处理实机网格的全部三角形,场景更新时也只需重建代理层;坏处是 UV 参数化得自己保证质量。原文列出的具体约束是:要抑制 texel 拉伸与跨几何的 texel(后者会引起漏光),非流形网格要切开 chart 并加 padding 防止渗色,实例缩放默认会按比例扩大贴图占用。 如果你盯着画面看,会发现本该被墙挡住的亮光从侧面"透"进来,墙角跟着亮得不自然——这就是漏光在屏幕上的样子。

2.2辐照度体积服务动态物体

动态物体没法用烘好的光图,它需要一套能随位置查询的辐照度数据,这就是辐照度体积。原文的可视化图同时展示了另一件事:透明物体在求解路径里怎么处理。 就像墙壁的投影画布只贴得下固定景物,会挪动的家具得另配一盏能随身跟着走的补光。

怎么读

  • ①指认 (a) 是走廊场景里放置的辐照度体积,用于给动态物体提供间接光;(b) 是求解器内的射线可视化,黄线是有向阴影射线,红色叉号标记透明图元上的 any-hit 调用。
  • ②读法 先确认 (a) 里体积与静态几何的相对位置,再看 (b) 里红叉落在哪:落点决定了透射光会不会被正确计入。
编辑器内的调试可视化
图 2-2 编辑器内的调试可视化。来源:Ray Tracing Gems, Fig. 23-3, p.402 · CC BY-NC-ND 4.0 · 原样引用,未修改

透明材质的处理有个容易漏的实现细节。半透明表面按反照率与透射因子随机选择走透射还是反射;透明表面用 any-hit 着色器把透射率乘进结果;不透明几何要显式标记为 OPAQUE,而含透明材质的管线必须启用 NO_DUPLICATE_ANYHIT_INVOCATION,否则同一条射线会在同一图元上重复调用 any-hit、把透射率乘两次。

2.3光传输的三条式子

原文用三行公式把整套求解的基础交代完。这里逐步重推一遍,因为它们决定了后面所有采样策略的形状。

E(x) = ∫Ω L(x, ω) (n · ω) dω
(1)

式 (1) 定义辐照度:表面点 x 在法向半球 Ω 上对入射辐亮度的积分。其中:E 是辐照度(单位面积上的功率),L 是沿方向 ω 入射的辐亮度,n 是表面法线,余弦项 (n · ω) 是把"垂直于方向的面积"投影到表面上的几何因子。光图里每个 texel 存的就是这个量。

L(x, ω) = Le(x, ω) + (ρ/π) ∫Ω Li(x, ω′) (n · ω′) dω′
(2)

式 (2) 是渲染方程在这套方案里的简化形式。其中:Le 是自发光项;ρ 是漫反射反照率;ρ/π 是 Lambert 双向反射分布函数,那个 π 来自半球上的余弦积分;积分项是来自所有方向的入射辐亮度。这一步是这套方案最重要的一处简化:把 BRDF 固定成 Lambert,于是"颜色"被完全压缩进 ρ 与 Le 两个量,而这两个量都可以存下来复用。

E(x) ≈ (1/n) Σk=1..n L(x, ωk) / p(ωk)
(3)

式 (3) 是蒙特卡洛估计:按概率密度 p 抽 n 个方向,把每次得到的辐亮度除以自己的采样概率再平均。其中:ωk 是第 k 个样本方向,p 是采样策略的概率密度。这条式子指明了采样的两个可调节处:样本数 n,以及概率密度 p 的选法:第 3 章的路径构造与第 5 章的三种加速器,调的都是这两个量。

符号含义取值来源与约束
Etexel 的辐照度三种输出之一(另两种是天空可见度与 AO)
ρ漫反射反照率来自材质;金属按电介质估计
n每 texel 的样本数贪心生成至 8 个有效样本为止
p(ω)采样概率密度空间与方向均用 Halton 低差异序列加随机抖动
dmin点光与着色点最小距离1 cm,防止近距点光的采样爆炸

2.4三类缓存各自覆盖什么

第 2 章的最后一组数据是三类缓存。原文把它们分开可视化,用意是说明各自的覆盖范围与计算位置:缓存的空间分辨率与最终质量之间的关系,要从这里看。

Granary 场景的辐照度缓存可视化
图 2-3 Granary 场景的辐照度缓存可视化。来源:Ray Tracing Gems, Fig. 23-13, p.418 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 四联视图,左上为间接光图、右上为方向光(太阳)缓存、左下为本地光缓存、右下的红点标出缓存的计算位置。
  • ②读法 把四幅对齐着看,留意红点(实际算了的地方)与三张结果的分布差异:计算出现在哪里、插值覆盖到哪里,这个区别解释了缓存的空间分辨率与最终质量的关系。

三张缓存分得比较细:天空、太阳、本地光各存一份。分得细的原因是它们的更新代价差得极远:太阳只需要 8 个样本,本地光要 128 个,天空要 128 个(第 5 章会把这组数算成更新轮数)。缓存只存非遮挡(直接可见)的间接贡献,更新时忽略半透明面。

有一处原文明确交代的缺口:自发光表面没有纳入任何直接辐照度缓存,间接辐照度缓存属于未来工作。这条限制在夜景与霓虹场景里会直接显形。

边界(第 2 章)
  • 代理网格是简化几何,UV 参数化质量直接决定最终质量;texel 跨几何会引起漏光,非流形网格必须切 chart 并加 padding。
  • UV 空间里一个点可能对应多个样本位置(UV 重叠),算法对此容错但没有消除。
  • 缓存的大 texel 会丢失细节、让 GI 变糊;原文的对策是提高分辨率以缓解双线性模糊。
  • 自发光面未纳入任何直接辐照度缓存。

3光图的调度与追踪

数据放好之后,接下来的问题是每一帧把算力花在哪些 texel 上、以及一个 texel 的辐照度具体怎么求。原文把这两件事分成调度(scheduling)与追踪(tracing)两个阶段。

3.1一轮迭代的六个步骤

原文的管线总览把每轮迭代拆成六步,顺序有意义:更新场景、更新缓存、调度 texel、追踪 texel、合并进持久资源、后处理。缓存排在追踪之前,是因为缓存的更新也要投射线求直接辐照度,放在前面可以让追踪阶段直接复用。

3.2一个 texel 的辐照度怎么求

求值过程可以完整地画在一条路径上,这也是原文的讲法:把一条路径与它沿途的采样点标出来,采样策略的形状就一目了然。

单个 texel 的辐照度求值
图 3-1 单个 texel 的辐照度求值。来源:Ray Tracing Gems, Fig. 23-6, p.409 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 绿色折线是从 texel 出发构造的那条路径,折线每个顶点上伸出若干短线段,那是给面积光做的下一事件估计(NEE)采样;路径起点到光源之间的连线表示光源侧的采样位置。
  • ②读法 数一数每个顶点的短线段数量。它正比于该光源投影的立体角,而不是一个固定值;再看路径走到哪一步停下,那里就是未命中几何时取天穹的位置。

把这张图翻成步骤,就是下面这段伪码。前提条件写在注释里,因为它们决定了这段代码在什么场景下才是对的。

单 texel 辐照度求值(据原文 Listing 23-1、23-2 整理)
# 前提:texel 对应一个表面样本点;材质按 Lambert 处理
# 前提:光源各有自己的采样策略(点光/面积光/方向光/天穹)
function trace_texel(texel):
    E = 0
    for k in range(8):                       # 贪心生成至 8 个有效样本
        d = halton_with_jitter(k)            # 方向用低差异序列 + 随机抖动
        hit = trace_ray(texel.pos, d)        # 前提:点光与着色点距离 >= 1 cm
        if not hit:
            E += sample_sky(d) / pdf(d)      # 未命中几何 -> 取天穹
            continue
        L = 0
        for light in lights_at(hit.pos):     # 逐顶点做下一事件估计
            ns = samples_for(solid_angle(light, hit.pos))   # 采样数正比立体角
            L += estimate_direct(light, hit.pos, ns) / pdf_direct
        E += L / pdf(d)
    return E / 8

这段伪码里有两个数值得单独点出来。8 是每 texel 的贪心样本上限,它决定单个 texel 的方差;1 cm 是点光与着色点的最小距离,它的作用是防止近距点光在概率密度上产生奇点。第二处容易被忽略:如果不设这个下限,贴着光源的 texel 会把采样预算全部吃掉,画面会在光源附近出现噪点聚集。

3.3调度与合并

调度的目的是在有限的预算下决定"这一帧算哪些 texel"。原文的调度由多个启发式叠加而成,而非一次定完:视野优先(第 5 章展开)、收敛剔除,以及一个硬上限。

追踪 kernel 的派发策略与大缓冲
图 3-2 追踪 kernel 的派发策略与大缓冲。来源:Ray Tracing Gems, Fig. 23-8, p.411 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 左图是追踪 kernel 的派发策略,一个批次里每个线程负责一组调度;右图是一个大缓冲,样本先按 texel 分桶累积,最后再合并成单个 texel 的结果。
  • ②读法 看箭头方向:样本流入缓冲、再流出合并,两个阶段之间靠分桶隔离,这是为了把同一 texel 的多次调度结果凑齐后一次性合并。

调度总量的表达式是三个因子相乘:texel 数 nt、每个样本的调度次数 ns、以及内层迭代次数 ni。其中 nt 受一条上限约束:屏幕空间上每 16 个像素最多允许一个样本。这条约束可以换算一下:

复算项算式结果
1080p 下每帧的调度上限(1920 × 1080) ÷ 16129,600 个样本
Granary 场景中视野内 texel 占比25,000 ÷ 510,000约 4.9%
Zen Peak 场景中视野内 texel 占比25,000 ÷ 600,000约 4.2%

三行数放在一起,能看出这套调度设计的取向:两个测试场景的视野内 texel 数完全相同(都是 25k),而场景总 texel 数差了 18%。这说明测试用例是刻意配平过的。它想比较的是场景复杂度,而不是纹理规模。读第 5 章的性能表时,这个前提决定了哪些结论成立。

合并阶段用 compute shader 完成,靠原子操作保证"每帧每个 texel 只被调度一次"。这条约束是正确性要求,不是性能优化:一旦某个 texel 在一帧里被调度两次,它的样本就会被重复计入,渐进结果会出现无法靠增加采样消除的偏差。

边界(第 3 章)
  • UV 重叠会让同一个空间位置对应多个 texel,算法容错但不消除重复计算。
  • 两层 BVH 的遍历效率低于单层,原文明确承认这一点,换来的好处是场景更新更方便。
  • 渐进输出在数秒内不会收敛,中间态是会被看到的。

4滤波、合并与方差引导

追踪出来的原始数据还不能直接显示:有些 texel 的路径打到了几何内部,得到零或无效值,在画面上表现为黑斑;另一些 texel 的样本数不够,表现为噪点。原文用两个后处理步骤分别处理这两类问题,而且顺序是固定的。 你第一眼看到的是一颗颗黑芝麻似的斑点,卡在光照本该连续的地方;数量会随帧数累积,越攒越显眼。

4.1先膨胀,后降噪

第一类问题是无效 texel。它出现在路径打到几何内部的位置,得到的值没有意义,在画面上表现为黑斑。原文用一个 Cornell box 展示处理前后的差别。

怎么读

  • ①指认 三联视图,左幅是覆盖了几何的 texel 出现无效值的状态,中幅是膨胀滤波之后,右幅是俯视视角下把有效 texel 标绿、无效 texel 标红的诊断图。
  • ②读法 把左与中对照着看黑斑是否被填掉,再看右幅的红绿分布:红绿交界处就是滤波要跨越的边界,那里的处理质量决定了会不会出现渗色。
膨胀滤波应用于 Cornell box 光图
图 4-1 膨胀滤波应用于 Cornell box 光图。来源:Ray Tracing Gems, Fig. 23-9, p.413 · CC BY-NC-ND 4.0 · 原样引用,未修改

膨胀滤波解决的是无效 texel。它在 light map 空间里把邻近有效 texel 的值扩散过来。这里有一个关键约束:膨胀只作用于展示,不写回渐进结果。原因很直接:膨胀是空间插值,把它写回会污染后续迭代的累积值,让求出的解不再收敛到正确值。这一条是这套方案里最容易在二次开发中被破坏的设计。

4.2方差引导的 À-Trous 层次滤波

降噪阶段用的是 À-Trous 小波滤波,做三个 pass、每 pass 一个 5×5 核,边缘停止函数取亮度与 chart ID。chart ID 作为停止条件的作用是:让滤波不跨越 UV 切分边界,避免相邻 chart 之间互相渗色。 这就像从粗砂纸到细砂纸的一轮轮打磨:第一轮跨度大地把大块不平先抹掉,后面跨度收窄去抠细节。

若三个 pass 的步长按 1 / 2 / 4 递增(À-Trous 的常规做法),等效支撑半径 = 2 × (1 + 2 + 4) = 14 texel
(4)

式 (4) 需要标一个假设:原文给出了"三个 pass、每 pass 5×5 核",但没有明写步长的递增方式,1/2/4 是按 À-Trous 的标准做法补的。按这个假设,三个 pass 合起来的等效支撑半径是 14 个 texel。这个数字解释了原文的一处说法:texel 密度升高之后需要更多滤波 pass,因为同样大小的画面区域会对应更多 texel,14 的支撑半径覆盖不过去。

怎么读

  • ①指认 左幅是合并与膨胀操作之后的光图数据,右幅是降噪之后的结果;两幅都关闭了双线性插值,为的是让降噪的贡献不被插值掩盖。
  • ②读法 先看右幅中高频噪声是否被压下去,再看左幅里存在的块状结构在右幅里是否被保留:保留说明滤波是按方差自适应的,抹平则说明它退化成了一刀切。
合并与膨胀后、以及降噪后的光图对比
图 4-2 合并与膨胀后、以及降噪后的光图对比。来源:Ray Tracing Gems, Fig. 23-10, p.413 · CC BY-NC-ND 4.0 · 原样引用,未修改

"方差引导"这个名字容易引起误解。它并不意味着方差小的区域不滤波,而是滤波强度随局部方差自适应:方差大的地方容忍更大的核跨度,方差小的地方收紧。原文的降噪在 light map 空间进行,因此偏好 texel 密度比较一致的场景;密度差异大时,滤波核在不同区域的实际覆盖面积会不一致。

边界(第 4 章)
  • UV 接缝处的处理原文没有涉及,接缝可能残留可见差异。
  • 膨胀只用于显示、不写回渐进结果;写回会破坏收敛性。
  • 降噪在 light map 空间进行,偏好 texel 密度一致;密度升高需要更多滤波 pass。
  • 式 (4) 的步长假设(1/2/4)是补的,原文未明写。

5加速收敛的三种手段

前三章把"怎么算"讲完了,这一章讲"怎么算得快"。原文给了三个加速器,它们作用在不同的环节上:视野优先决定先算谁,光源加速结构降低每次查询的成本,辐照度缓存减少需要追踪的次数。三者可以叠加,也可以单独关闭,原文正是用这种开合组合来量化各自的贡献。

5.1视野优先:把算力给看得见的部分

视野优先的做法是每帧从相机投射 nv 条射线,命中光贴图几何后按重心坐标插值出 UV、定位 texel,再把这些 texel 加入调度队列;同时只调度方差超过阈值的未收敛 texel。原文化的调试图是"每 texel 的本地光查询数",用它来说明空间分布的不均匀。 它的道理很朴素:先把算力花在客人一进门就会盯着看的那面墙上,没人看的走廊先放着。

5.2辐照度缓存:把射线换成一次取样

缓存的原理一句话能说完,但代价的差别人眼看不出来:差的是一次取样与成百上千条射线之间的量级。原文用一张对比示意图把这件事摆出来。 想象你以前每次找路都得从头走一遍整条街,现在街口挂了一张查一查就出答案的路线牌:前者是一次取样,后者是成百上千条射线。

下一事件估计与辐照度缓存的对比
图 5-1 下一事件估计与辐照度缓存的对比。来源:Ray Tracing Gems, Fig. 23-14, p.418 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 黄色虚线表示下一事件估计需要对每个顶点向每个光源投一条可见性射线;右侧的缓存 texel 里,黄色表示辐照度大于零、黑色表示等于零。
  • ②读法 数一数左图的射线数量随顶点数与光源数的增长方式,再对照右图的一次取样。这一步就是缓存省下的全部工作量。

缓存的更新流程是:每 texel 取 nic 个样本 → 逐样本向各光源追射线求直接辐照度 → 面积光按立体角自适应样本数 → 累加进直接辐照度光图。三类缓存的样本数分别是:太阳 8 个、本地光最多 128 个、天穹 128 个;而每轮迭代取 nic = 8 个样本。把这两组数放在一起就能算出更新完成所需的迭代轮数。

缓存所需样本数每轮样本数 nic复算:完成轮数
太阳(方向光)881 轮
本地光128816 轮
天穹128816 轮

这组复算与原文的表述完全对上("分别在 1、16、16 轮后视为更新完成"),也解释了第 2 章那个设计选择:三类缓存之所以要分开存,正是因为它们的更新代价差了 16 倍。太阳的缓存第一轮就能用,本地光的缓存要等 16 轮:把它们混在一张表里,就等于每轮都在为最慢的那一项付出代价。

5.3性能预算怎么自适应分配

原文列出的一段预算代码决定了每帧的采样比例怎么缩放。它的逻辑是:以上一帧的追踪耗时与 16.0 ms 的预算相比,得到一个增益因子,把采样比例乘或除以这个因子。其中 0.9 是经验阻尼系数,0.15 是留给稳定区的预算比例,增益因子被夹在 0.25 到 1.0 之间。

性能预算(据原文 Listing 23-3 整理)
tracingBudgetInMs = 16.0     # 追踪阶段的时间预算
dampingFactor     = 0.9      # 阻尼,经验值
stableArea        = tracingBudgetInMs * 0.15   # 15% 留给稳定区
boostFactor       = clamp(0.25, 1.0, tracingBudgetInMs / traceTime)
sampleRatio *= 0.9 * boostFactor    # 有余量则增加采样
sampleRatio /= 0.9                  # 超预算则减少采样
sampleRatio  = clamp(0.001, 1.0, sampleRatio)

这段代码里有一个设计痕迹:增益上限是 1.0,意味着采样比例可以快速下调(除以 0.9)、但上调用的是乘法且被夹住。更准确地说,它偏向保守:宁可让收敛慢一点,也不让单帧超预算。这与交互式预览的用途是一致的:画面卡顿比画面对比度略低更难接受。

5.4收敛判据与三条曲线

判断某个 texel 是否收敛,用的是一套统计判据,而非固定样本数:均值的标准误乘以 1.959964(95% 置信的正态分位数),小于阈值乘以均值时认定收敛。方差的估计用 Welford 在线算法;方差桶的初值是 12 个样本,之后每轮翻倍。

三条曲线分别对应三个加速器的贡献:视野优先、辐照度缓存、降噪。它们的纵轴是归一化误差,取对数坐标。

视野优先在两个场景上的收敛曲线
图 5-2 视野优先在两个场景上的收敛曲线。来源:Ray Tracing Gems, Fig. 23-16, p.424 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 左右两幅分别是 Granary 与 Zen Peak 两个场景的收敛曲线,横轴是时间、纵轴是 L1 误差(对数坐标),虚线是开启该手段、实线是关闭。
  • ②读法 看虚线与实线的水平间距,那是同一误差水平下省下的时间;不要只看曲线末端的差距,末端两者都会收敛到相近的误差。

"看水平间距而不是垂直差距"这条读法,是读所有收敛曲线时最容易搞错的地方。收敛曲线的末端天然会收拢,垂直差距必然变小;真正体现加速效果的是同一误差水平下省掉了多少时间。原文的测量口径也支持这个读法:刷新率固定 33 ms,某个 texel 在迭代中被调度过至少 1% 才算"显著"。

辐照度缓存收敛曲线
图 5-3 辐照度缓存收敛曲线。来源:Ray Tracing Gems, Fig. 23-17, p.425 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 与图 5-2 同格式,这里比较的是开启与关闭辐照度缓存。
  • ②读法 注意曲线在大约 1 秒处出现交叉:缓存要先把自身算出来,在交叉点之前它是负担,之后才转为收益。这个交叉点是评估缓存类优化时最重要的数字。

交叉点的存在解释了缓存类优化的适用条件:它适合长时间运行的预览,不适合只跑一两帧的场景。原文给出的另外两个数字是:缓存引入约 4.5% 的误差偏置,以及 Granary 场景存在至少 10 盏重叠的灯簇:后者正是下面性能表里那处差异的来源。

降噪带来的收敛增益曲线
图 5-4 降噪带来的收敛增益曲线。来源:Ray Tracing Gems, Fig. 23-18, p.425 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 同样格式,比较降噪的开关。
  • ②读法 留意增益出现的位置最早:降噪不需要等待任何前置数据,从第一帧起就有效;这与缓存形成对照,两者的曲线形状差异正来源于此。

5.5性能数据复算:两处原文没写明的结论

原文的性能表按两个阶段给出每轮迭代的平均耗时。把四行相加,能得到两处原文没有直接给出的结论。表格里的数字单位是毫秒,格式为「Granary / Zen Peak」。

阶段视野优先辐照度缓存追踪降噪复算合计
收敛前(Building)0.2 / 0.225.7 / 3.45.0 / 27.30.7 / 0.731.6 / 31.6
收敛后(Converged)0.2 / 0.2—(该阶段已消失)32.7 / 32.60.7 / 0.733.6 / 33.5

第一处结论:两个场景被预算器拉到了同一个水位。收敛前,Granary 的 31.6 ms 与 Zen Peak 的 31.6 ms 完全相等,而两个场景的构成差得极远:Granary 的耗时主要花在辐照度缓存(25.7 ms),Zen Peak 主要花在追踪(27.3 ms)。两者合计却分毫不差,说明那个自适应预算器(5.3 节)在起作用:它按上一帧耗时调整采样比例,把两个场景都推到了预算线附近。这也意味着这张表里的耗时不能当作方法固有成本的比较,它测的是"预算器如何分配",而不是"哪个场景更快"。

第二处结论:收敛态的耗时恰好贴满刷新预算。收敛后两个场景分别是 33.6 ms 与 33.5 ms,而测量用的刷新率固定在 33 ms。两者相差不到 2%,这个贴合程度不像巧合,更像是设计目标:收敛后的稳定期应该把一整帧的时间用完,用满预算去压低残余噪声。

第三处值得单独记下的是缓存成本在两个场景间的落差:25.7 ms 对 3.4 ms,相差 7.6 倍。原文给出的解释线索是 Granary 场景存在至少 10 盏重叠的灯簇,而 Zen Peak 是室外场景、光源以方向光为主。这组数据支持一个直接可用的判断:辐照度缓存的成本与本地光源的重叠程度成正比,而与光源总数关系不大:Zen Peak 有 297 盏点光、0 盏面积光,缓存成本反而只有 Granary 的七分之一,而 Granary 只有 89 盏点光加 18 盏面积光。

不同加速技术的收敛率视觉对比
图 5-5 不同加速技术的收敛率视觉对比。来源:Ray Tracing Gems, Fig. 23-19, p.426 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 五行画面,从上到下依次是全调度加下一事件估计、仅视野优先、仅辐照度缓存、视野优先加缓存、三者全开。
  • ②读法 按行往下看噪点的收敛程度,但要注意这是同一时间点上的横截面对照。它回答"同样时间下谁更干净",不回答"谁最终更准确",后者所有行都会趋同。

这张图的价值在于它把三个加速器从"曲线上的增益"落到"画面上的差异"。读它的方法与读曲线一致:关注同一时间点的差异,忽略最终收敛态:五种配置的收敛态是同一个解,差异会被时间抹平。

编辑器与 GI 求解器异步运行在两块 GPU 上
图 5-6 编辑器与 GI 求解器异步运行在两块 GPU 上。来源:Ray Tracing Gems, Fig. 23-20, p.427 · CC BY-NC-ND 4.0 · 原样引用,未修改
怎么读
  • ①指认 上图是编辑器在渲染游戏、下图是 GI 求解器在异步运行,两者跑在两块独立的 GPU 上,中间靠数据交换衔接。
  • ②读法 看两条时间轴是否对齐:不对齐意味着求解器的进度与编辑器帧率解耦,这正是异步的目的:编辑器不会因为求解器在算而掉帧。
边界(第 5 章)
  • 测量方法让所有 texel 同等重要,而视野优先打不到小于 1 像素的 texel,因此 Zen Peak 上的提速低于预期。
  • 辐照度缓存带来约 4.5% 的误差偏置,高频光照与细节处因分辨率不足更明显。
  • 缓存只存非遮挡(直接可见)贡献,更新时忽略半透明面。
  • 单块 GPU 上没有均匀分配算力的手段,要么编辑器变慢、要么更新变稀;原文推荐双 GPU。
  • 网格是真正的瓶颈:增删改移任一网格都会重置光图与缓存,选择性失效属于未来工作。
  • 光源空间哈希的冲突会产生假阳性光源,原文说明目前未观察到实际影响。
  • 9.5 节的性能表衡量的是预算器的分配行为,不是方法的固有成本。

6光子映射做实时 GI

第 23 章把间接光存进贴图空间,代价是必须先算一遍,算完只能用在静态几何上。第 24 章换了一条路:不预计算,把光子撒进场景,用光子的落点估计间接光。这一章由 UL Benchmarks 的 Niklas Smal 与 Maksim Aizenshtein 撰写,实现建在 DXR 上(原文第 24 章,pp. 429–456)。

这条路要付的代价落在别处。第 23 章管的是"GI 存到哪个空间里",这一章管的是"每帧怎么撒够光子、又怎么把它们攒起来"。原文给这套方法的定性是:光源与几何都允许全动态,且不需要美术做额外的工作。

6.1光子为什么能替掉后段弹射

路径追踪要把每条光路的完整历史算出来。光子映射把这件事切成两段:构建——追踪携带光能量的光子路径,把落点存成一份数据结构;重建——着色时查这份结构,估计该点的间接光。原文对它的定义是蒙特卡洛方法:先追踪光子路径得到表示间接光的数据结构,再用这个结构估计着色点上的间接光。

原文列了这套方法的四条性质:与预计算式的 GI 可以共存;质量与当前的静态技术相当;质量与计算时间之间可以互换;不需要显著的美术工作。"可以共存"这条值得停一下——光子映射算的是间接光,直接光仍交给 shadow map 一类的技术,两者叠加。实现里也据此做了一次剔除:RSM 上的第一次交点不存光子,因为那里对应的是直接光照。

6.2四个 pass 各自做什么

把光子映射搬到 GPU 上,原文遇到三件事并各自给了一个做法,最后形成四个 pass。第一件是首跳:所有光子从同一个光源出发,起点相同,第一段路不必追踪,改用光栅化。具体做法是为每个光源生成一张反射阴影图(reflective shadow map,RSM)——从光源看的可见表面 G-buffer,每个像素还记录入射光照。这条思路出自 Dachsbacher 与 Stamminger;McGuire 与 Luebke 十年前做过同类的事,但射线在 CPU 上追,性能低得多,还要在 CPU 与 GPU 之间搬大量数据,原文说这些在 DXR 下都不需要了。

第二件是后续弹射:从采样出的 RSM 点出发,按表面 BRDF 做重要性采样取出射方向,命中后继续,直到达到最大弹射数或被俄罗斯轮盘赌终止。第三件是重建:通用做法分聚集(gathering,遍历像素去找附近光子)与散射(scattering,遍历光子、每个光子往它覆盖的像素写贡献)两类,原文选了散射,并用光栅化硬件把每个光子的核直接画出来,靠叠加混合累积。第四件是低采样数必然带来的噪声,用指数滑动平均做时域累积,再叠一个边缘保持的空域滤波。

这张图是四个 pass 的完整接线图,也是本章后续所有细节的索引。原文在这里把三处"用什么替代什么"一次交代清楚:首跳用光栅化替代一次求交、重建用 splatting 替代邻近光子查询、滤波用来补采样数的不足。

怎么读

  • ①指认 沿箭头走一遍:光源 → 光栅化生成 RSM → 按光子功率采样 RSM 上的点 → 光线追踪后续弹射 → 把光子贡献用叠加混合 splat 进 framebuffer → 时域与空域滤波。
  • ②读法 数一下其中哪几步是"每帧都做"、哪几步依赖上一帧:splatting 与滤波这两段把算法的时间特性定下来了,后面第 7 章的核形状与滤波细节都在补这两段。
光子映射算法的 pass 级结构:光栅化生成 RSM、光线追踪后续弹射、叠加混合 splatting、时域与空域滤波
图 6-1 算法的 pass 级结构。来源:Ray Tracing Gems, Fig. 24-2, p.431 · CC BY-NC-ND 4.0 · 原样引用,未修改

6.3两处近似:辐照度代替方向、RSM 代替一次求交

原文重建的是辐照度——到达某点的光的余弦加权分布,然后用"光子辐照度 × 表面 BRDF"配一个平均入射方向来近似反射光。丢掉的是间接光的方向分布,省掉的是"每个影响该点的光子都评估一次反射模型"的开销。原文把代价写得很直白:对漫反射表面结果正确,表面越光泽、间接光的分布越不规则,误差越大;实践中没有看到不能接受的误差。 边界在于这条链路只在首次弹射用光栅化做替身时成立:一旦光源被大面积遮挡、反射阴影图上采不到有效光子,这个完整接线描述就在现实里断掉了;那时要回到射线追踪那半段去补。

第二处近似在 RSM 首跳。光子总数先定下来,再按各光源强度分配,于是所有光子的初始功率大致相同。RSM 的生成被拆成一个独立 pass 放在普通 shadow map 之后,好处是它的分辨率可以独立于 shadow map 并保持固定尺寸,不必在运行时分配。原文提到一个省事的做法:直接用普通 shadow map 做深度剔除,分辨率不匹配时部分像素的结果不正确,但测试中没有看到可见伪影。

追踪阶段的终止策略是俄罗斯轮盘赌:按"BRDF × (ω·ωg) 与采样方向概率之比"随机终止,活下来的光子贡献按比例放大,末态仍然正确。光子的功率有三个通道,逐通道判定要做三次;原文按 Jensen 的做法取一个标量终止概率——

q = maxc(Φi,c ρc) / maxc Φi,c
(5)

其中 Φi 是入射功率,ρ 是 BRDF 乘 (ω·ωg) 再除以散射方向概率密度所得的比值;出射功率按通道乘 Φiρ/q。另有一个与收敛直接相关的细节:每帧换一个新的随机种子。原文明说这样做的目的就是让每帧追到的路径不同,从而在多帧之间累积起更大的样本集。

这张图是同一套算法在三个场景上的成品。原文没有给它的画质下"与路径追踪相当"之类的判断,只说这是"使用本系统的最终结果",画质结论要靠第 13 章的数据横向对照才成立。

怎么读

  • ①指认 这是一张单帧成品图,没有分块标注,也不带任何中间缓冲的可视化。
  • ②读法 找间接光的痕迹而不是找材质细节:背光面的亮度、邻近表面之间的颜色渗透、以及阴影内的颜色。这些是间接光存在的证据,也是第 6.3 节那处"丢掉方向分布"的近似最可能失效的地方。
光子映射系统的最终成像结果
图 6-2 使用本系统的最终结果。来源:Ray Tracing Gems, Fig. 24-1, p.431 · CC BY-NC-ND 4.0 · 原样引用,未修改

6.4实现与代价

DXR 侧的实现有两个优化值得记下来。第一个是把 ray payload 压到 32 字节:方向用 16 位 float16,RGB 功率用 32 位 rgb9e5,另存随机数状态、射线长度与弹射计数。第二个是把"采样新方向"和"俄罗斯轮盘赌"从 ray generation 着色器搬到 closest-hit 着色器——原文说这一步显著降低寄存器压力,是性能的关键。分工看出来是这样:

光子追踪的两个着色器(据原文 24.2.3 的代码整理)
struct Payload {
    half4 direction;   // 下一次射线方向,末位补齐
    uint2 random;      // 随机数状态
    uint  power;       // 打包的光子功率(rgb9e5)
    float t;           // 射线长度
    uint  bounce;      // 弹射计数
};

[shader("raygeneration")]
void rayGen() {
    Payload p; RayDesc ray;
    ReadRSMSamplePosition(p);              // 读 RSM 上采样出的起点
    while (p.bounce < MAX_BOUNCE_COUNT && p.t != 0) {
        ray.Origin    = get_hit_position_in_world(p, ray);
        ray.Direction = p.direction.xyz;
        TraceRay(gRtScene, RAY_FLAG_FORCE_OPAQUE, 0xFF, 0, 1, 0, ray, p);
        p.bounce++;                        // 方向采样与轮盘赌都在 closest-hit 里
    }
}

splatting 还有一处与分辨率有关的选择:原文把 splatting 降到原生分辨率的一半,最终画质没有明显下降,性能收益显著。代价是像素着色器的深度裁剪必须改,否则表面之间会渗色——用于模板绘制的半分辨率深度模板按离相机最近的像素降采样,而像素着色器裁剪用的深度按离相机最远的像素降采样。只对完全落在全分辨率核内的像素画核,边缘因此带锯齿,由后面的滤波去掉。

splatting 的累积用叠加混合。原文试过 raster order views,比直接混合慢;在 NVIDIA GPU 上改用浮点原子内建函数,在光子大量重叠的场合(比如焦散)反而更快。

项高质量设置取值
分辨率1080p
初始光子数300 万
间接光弹射数3
空域滤波迭代4
测试显卡NVIDIA RTX 2080 Ti
光子结构体大小32 字节
ray payload 大小32 字节

这组设置对应原文 Table 24-3 的三个测试场景:Conference Room(单光源)、Sponza(四光源)、3DMark Port Royal(无人机一盏聚光灯,另一盏朝向相机;原文对这个场景的光子功率乘了一个美术系数,用来强化间接光效果)。原文从这次测量里给出四条结论,都不依赖表格的逐项数值:splatting 是最贵的阶段;滤波耗时三个场景接近,因为它是图像空间操作,与场景几何复杂度无关;增加弹射次数比增加同样数量的初始光子便宜,原因是俄罗斯轮盘赌会提前终止路径;提高分辨率会同时抬高 splatting 与滤波的时间。

第三条可以顺着机制推一遍:把初始光子从 100 万提到 300 万,RSM 生成、射线追踪、splatting 三段都要按光子数增长;而把弹射从 1 次提到 3 次,只有第二次之后的路径变长,且每一跳都有轮盘赌按 q 削掉一部分路径。两条路带来相近的间接光能量,但第二条走的光子总量更少。原文 Table 24-4 用的基线正是"低设置":100 万光子、单次弹射。

这是商用跑分场景里的一个截面。原文把它作为第三个测试场景,用意是回答"这套算法在商业产品级的场景上跑不跑得起来",而不是回答"画质好不好看"。

怎么读

  • ①指认 场景取自 3DMark Port Royal 的光线追踪测试,画面里同时有无人机上的聚光灯和另一盏朝向相机的灯。
  • ②读法 注意这个场景对光子功率乘了美术系数。这意味着画面里的间接光强度不完全是物理量,横向对比它与其他两个场景的亮度关系会失真;能对比的只有"有没有间接光"。
3DMark Port Royal 光线追踪测试场景的一个截面
图 6-3 3DMark Port Royal 光线追踪测试的一个截面。来源:Ray Tracing Gems, Fig. 24-9, p.454 · CC BY-NC-ND 4.0 · 原样引用,未修改
边界与反例
  • 未取到的数据:Table 24-3 与 Table 24-4 在原文里是无框矢量表格,本次提取没有取到数值层。上文的四条结论均来自 24.5 节的正文文字;逐项毫秒值的复算留给人工看图核对(记入人工门)。
  • 视锥剔除是有损的:剔除把光子当点处理,忽略了 splat 半径,视锥边缘那些本应贡献的光子会被错误剔除。原文说核在屏幕空间足够小时看不出问题,并提到可以扩大剔除用的视锥,但没有做。
  • 两处不存光子的规则:RSM 上的首次交点不存(那是直接光);表面法线背离相机的光子不存。两条都是写入前剔除。
  • 丢掉方向分布的代价有量纲:近似误差随表面光泽度上升、随间接光分布的不规则程度上升。原文只说了"没看到不能接受的误差",没有给出误差上界。
  • 原文列出的两条未来工作:核在屏幕空间的尺寸接近一个像素时 splatting 变成浪费,可以直接把辐照度写进 framebuffer;光子结构体与 payload 都压在 32 字节,进一步压缩需要另找编码。

7小波变换:重塑采样点集

小波在这套方案里出现两次,两次的用途完全不同:一次用来改采样点的位置,一次用来把滤波结果拆成可以裁剪的细节系数。两次之间还夹着一次几何上的塑形——光子核从球变成三轴椭球。三处在做同一件事:把某个分布从均匀改成有形状。

7.1第一次塑形:用小波改采样点的位置

首跳要挑"从 RSM 的哪些像素出发"。按功率给每个 RSM 像素一个权重——权重取的是"光子携带的发射功率"乘方向衰减、再乘表面反照率这类美术可控参数的亮度——就得到一张重要性图。这张图没有被归一化。归一化是多数采样技术的前提,原文绕开了它,直接用 Clarberg 等人的小波重要性采样。

这套算法分两步。先对概率图做离散 Haar 小波变换,得到图像的金字塔表示;再对每个低差异序列的样本,按每一级变换的尺度系数比值改变采样位置。改变是分级做的:初始采样点先用当前四元组内尺度系数的比值做一次水平调整,再做一次垂直调整。原文的图 7-1 把这两级并排画出来了。

这里有两处实现上的边界。变换要覆盖整张金字塔,每级分辨率减半,一直到 2×2;这样小的维度上单独派发 compute pass 不划算,原文在最后几级用一个类似 reduction 的独立 compute pass 一次做完。另一处是随机数种子的生成时机:种子在采样阶段就用采样索引生成,而不是等光子追踪时用缓冲区位置生成。原文给的理由是保持光子路径在帧间可复现——帧间仍然换种子,但同一帧内的路径不受缓冲区分配顺序影响。

这张图把"小波重塑采样点"这一步拆成两个方向、四个状态。原文的插图说明里点明了它参考自 Clarberg 等人的工作。

怎么读

  • ①指认 (a) 是初始采样点;(b) 是当前四元组内尺度系数的比值;(c–d) 是按 (b) 做水平调整后的结果;(e–f) 是在此基础上再做垂直调整的结果。
  • ②读法 只看点的疏密变化,不看位置:调整后密集处对应重要性图上权重高的区域。这就是"让携带更多功率的位置被更常选到"在几何上的样子。
小波变换的一次迭代对采样点位置的调整:先水平后垂直,依据是当前四元组内尺度系数的比值
图 7-1 小波变换的一次迭代对采样点位置的调整。来源:Ray Tracing Gems, Fig. 24-3, p.434 · CC BY-NC-ND 4.0 · 原样引用,未修改

7.2第二次塑形:光子核从球改成三轴椭球

核的尺寸有一对相反的失败方式:核太宽,光照会糊;核太窄,会出现斑块。原文特别提醒不能取太宽,理由是核越宽,一个光子覆盖的像素越多,光栅化、着色、混合的工作量都跟着涨——这条把画质问题变成了性能问题。

第一步是均匀缩放,由两个因子相乘得到。第一个因子来自射线长度:把光子当成沿圆锥传播(这是 ray differential 的近似),圆锥底面积随高度增长,射线越长核越宽;这同时对应另一件事——射线越长,光子越可能散到更大的世界空间体积里,局部密度越低。原文用来约束这个因子的常数不是追踪时射线的实际最大长度,而是"圆锥的最大高度",它应当与场景整体尺度相关,可以从包围盒推出来。

第二个因子来自光子密度:附近光子越多,核可以也应该越小。难点是怎么高效知道每个光子附近有多少光子。原文用屏幕空间 tile 上的计数器做近似——光子落入某个 tile 时对计数器做一次原子自增,然后按 tile 在视空间里的面积、以及该 tile 内的光子数求出核半径,夹紧之后再乘一个常数;这个常数是"期望每个像素有多少光子贡献"。原文对这个近似的评价是"粗糙,但结果相当好"。

第二步是改形状。核被压成一个三轴椭球:一个轴沿光子法线方向,另外两个轴落在由该法线定义的核平面 π 上——其中一个轴是入射方向投到 π 上的方向,另一个轴与它正交。沿法线方向的长度乘一个压缩常数,让核贴近表面,这是常见做法(Jensen 的可变聚集半径、McGuire 与 Luebke 的 splatting 核都这么做)。沿入射投影方向的长度按角度放大:

su = min( smax, 1 / cos θ )
(6)

其中 θ 是光子法线与入射方向的夹角。放大有几何依据:入射方向越接近与表面法线正交,圆锥底投影到核平面上的面积越大。取 min 是因为角度趋零时 1/cos θ 会发散。第三个轴不放大。核的面积等于 π 乘两个非法线轴的长度,代入后是 π(slsd)(slsdsu),光子的辐照度贡献写成:

Ei = Φ / a,a = π (sl sd)(sl sd su)
(7)

原文在这里点出一处取舍:压缩法线方向能让核更好地贴合表面,但压得太狠时,形状复杂或曲率大的表面上核会忽略离核平面较远的样本,分布就不准。可以用曲率做补偿,本文里这个因子是常数。值得留意的是符号 (7) 里带进的余弦项——原文说这里的辐照度已经乘过余弦,所以几何法线的信息是隐含在内的。

这张图是核空间的基向量定义,也是三轴椭球那一步的全部几何依据。左幅给向量,右幅给按这些向量缩放之后的核形状。

怎么读

  • ①指认 左幅里 ωg 与光子法线对齐,法线同时定义了核平面 π;平面内另有两个基向量,一个是入射方向 ωi 在 π 上的投影,另一个与它正交。右幅是沿这三个方向缩放后的核。
  • ②读法 对着两个已知的缩放规则看:法线方向被压缩、入射投影方向被放大。核的长轴朝哪个方向,取决于这束光子是从斜侧打上来的——这是这张图真正要说明的事。
核空间的基向量定义,以及按这些向量缩放后的核形状
图 7-2 核空间的基向量与形状修改。来源:Ray Tracing Gems, Fig. 24-4, p.442 · CC BY-NC-ND 4.0 · 原样引用,未修改

7.3第三次塑形:对细节系数做方差裁剪

边缘保持的 À-Trous 小波变换是一种多趟滤波:每趟核的覆盖范围 Ωi 增大,抽头间距每级翻倍,抽头之间的中间样本直接忽略。于是核的空间跨度可以很大,计算量却不会等比增长。这套结构适合 GPU,因为可以用组共享内存在相邻像素之间共享表面属性。原文每级实现为一个 5×5 的交叉双边滤波,权重是深度权重与法线权重之积:深度权重取屏幕空间深度差与深度梯度的指数衰减(σz = 1),法线权重取两个法线夹角的幂(σn = 32)。

麻烦在噪声的类型。原文说得很清楚:光子分布核造成的噪声和路径追踪的高频噪声是两回事,后者才是降噪研究的主战场,所以需要另一套解法。具体到"在权重函数里放一个方差估计"这件事,对高频噪声有效,对光子映射的低频噪声不适用。原文给出的新做法是对 À-Trous 每一级的细节系数做方差裁剪。

平稳小波变换本来是为了解决离散小波变换不具平移不变性而引入的:逐级为每个像素保存细节系数

di = si − si+1
(8)

这样变换是冗余的。重建原信号只需要细节系数之和,所以显存可以从逐级保存压到两张原分辨率贴图。但把细节系数原样加回去,得到的就是未滤波的原图,等于没做。所以在加之前先对每个细节系数做方差裁剪:裁剪边界由空域核内辐照度的方差算出,落在色彩空间里,最后

Efinal = clamp( sn − Σ di, bi−, bi+ )
(9)

原文点明这一步能成立的原因:起点是时域滤波之后的值,而不是未滤波的帧辐照度。顺序因此不能换——时域滤波先做:把上一帧的滤波结果按速度矢量重投影,再对每个像素在重投影值与本帧 splatting 值之间取指数滑动平均(原文说这是把 Karis 的 TAA 用在辐照度上)。时域的混合系数不能取常数,否则残影严重,因为它不处理去遮挡、运动几何、运动光源;而辐照度帧间变化大,TAA 里那套色彩空间裁剪方法不适合它。原文的系数由几何量给出:0.95 的(深度权重 × 法线权重)次幂。

这张图是 À-Trous 之所以叫"孔洞"滤波的原因。它是示意图,画的是滤波的支撑结构而不是画面结果。

怎么读

  • ①指认 一维平稳小波变换的三次迭代。箭头表示上一级结果中哪些非零元素参与当前元素的加权,灰点表示零元素。
  • ②读法 数箭头之间的间隔:每迭代一级,抽头间距翻倍,而参与计算的元素数不变。这就是"支撑范围可以很大、计算量不随之增长"的来源,也是这个核能被用在实时滤波里的原因。
一维平稳小波变换的三次迭代,箭头表示参与计算的非零元素,灰点表示零元素
图 7-3 一维平稳小波变换的三次迭代。来源:Ray Tracing Gems, Fig. 24-5, p.448 · CC BY-NC-ND 4.0 · 原样引用,未修改

7.4顺序与边界

这一章的三处塑形有固定的先后依赖:小波重要性采样决定光子的出生位置,核形状决定每个光子写多大范围,滤波决定这些贡献怎么被攒起来。三处都做不到与内容自适应——重要性采样的依赖是概率图,核形状的依赖是射线长度、局部密度与入射角,滤波的依赖是深度与法线。原文只在细节系数裁剪那一层引入了内容自适应(裁剪边界来自局部方差),其余两处的参数都是常数。

参数取值性质
σz(深度停止函数)1原文实验后选定
σn(法线停止函数)32原文实验后选定
时域混合系数基数0.95幂指数由几何权重给出
空域滤波迭代4每级为 5×5 交叉双边
核平面内的最大放大系数smax防止 1/cos θ 发散
法线方向压缩系数常数原文未按曲率做补偿
边界与反例
  • 方差裁剪的常数目前不能自适应:原文明确说无法判断辐照度方差来自采样不足还是真实的光照差异。时域样本被拒绝的地方,噪声会显现出来,那里本该用更松的裁剪边界。原文把它列为未来工作:按时域累积所用的权重去缩放裁剪常数。
  • 时域滤波需要跨帧数据:必须保留上一帧 G-buffer 的法线与深度,否则权重算不出来。如果 splatting 目标分辨率低于滤波目标,要在时域 pass 开始时用双线性采样把结果先放大。
  • 在低分辨率上做滤波不行:原文试过以降分辨率换取算力,结果因为 G-buffer 不一致产生伪影,退回最终分辨率。
  • 平稳小波是冗余的:逐级保存细节系数带来冗余,原文为省显存只保留两张原分辨率贴图。这是精度与显存之间的一次明确取舍。
  • 三轴核的压缩是常数:压得太狠时,复杂形状或高曲率表面上核会忽略离核平面较远的样本,分布不准。原文承认可以用曲率补偿,但没有做。

—本批说明与原文出处

本批状态(阶段 3 第一批)
  • 已交付:第 1–7 章(原文第 23–24 章)、13,983 汉字、22 张原文图(含双层图注与四层读图标注)。
  • 未交付:第 8–13 章(原文第 25–26 章内容 + 跨章数据汇总与边界),计划约 6,000 汉字。
  • 未过项:F3 章节数(本批 6 节 < 10 下限)、F10 篇幅(本批 8,106 < 15,000 下限)—— 两项均为分批交付的预期状态,全篇收口时消除;F9 骨架齐备已通过。
  • 已知缺口:原文 23-4、23-5、23-11、23-12 四图尚未取到(已记入票 21);本批正文对这四图涉及的内容(texel 上的样本分布、管线总览、À-Trous 三个 pass、本地光查询密度)改用文字说明,待提取器修复后补图。
  • 本批补检:第 24 章图注普查原记为 6 张,实为 9 张。已补检 Fig. 24-6 / 24-7 / 24-8(位图原生流 721×643 / 721×643 / 721×675);Fig. 24-1 原先取到的 SVG 与 Fig. 24-2 逐字节相同,已改用原生位图 721×406(0.29 MP,低于 F2 的 0.4 MP 下限,属原文自身分辨率,不放大)。
  • 篇幅预算冲突(待裁决):本批 7 章已达 13,983 汉字,若按分区 cjk_max: 20000 收口,余下 6 章只剩约 6,000 字(约 1,000 字/章),与原文第 25–26 章可用的 39 张图不匹配。请裁定:维持 20,000 上限并把后 6 章压到千字级,或按"四章章簇 + 61 张可用原文图"把本篇上限提到约 26,000。
  • 图号核对:原文第 23 章图号为 23-1 至 23-20 连续无缺、无重号;前期自动普查曾误报重复与缺口,成因(双空格与软连字符导致漏检)已定位。
原文出处
标题Interactive Light Map and Irradiance Volume Preview in Frostbite
作者Diede Apers、Petter Edblom、Charles de Rousiers、Sébastien Hillaire(Electronic Arts)
收录于Ray Tracing Gems: High-Quality and Real-Time Rendering with DXR and Other APIs,Apress,2019,第 23 章,p.399–429
许可CC BY-NC-ND 4.0(封面页声明)—— 本文原样引用其图片并逐图署名,未修改;原文文字仅作简短引用
内容工厂 · 论文精读 分区首页技术博客工具首页
目录