单篇论文精读

Multi-Layer Reservoir Splatting:追一个被遮挡的样本,凭什么还能带着历史重新露脸

多层喷管 + 深度范围 + 激活域:给「时域复用断在 disocclusion」缝上一个不靠猜的补丁 · 万字拆解

Pengpei Hong · Song Zhang · Daqi Lin · Markus Kettunen · Chris Wyman · Cem Yuksel ACM SIGGRAPH 2026 ReSTIR 家族 · 2026-09-23 草稿 · 图文骨架
原文摘要 Abstract, p.1(英文原样引用)

“ReSTIR [Bitterli et al. 2020] efficiently reduces path tracing noise by reusing samples spatiotemporally. Recent ReSTIR methods [Liu et al. 2025] improve temporal reuse from prior frame primary hits. But disocclusions still invalidate some pixel histories, degrading quality with spatially varying noise. We address disocclusions in ReSTIR by introducing multiple screen-space layers, using reservoir splatting to shift samples between layers. This reuses previously occluded samples that become visible again, reducing noise in disoccluded regions. While shifting samples across layers typically requires tracing multiple rays, we introduce depth ranges and redefine the integration domain to eliminate many ray queries, improving performance. To minimize costs for maintaining many layers, we selectively track only active domains propagated from previous frames. We validate across multiple scenes, showing greatly reduced disocclusion noise while incurring only a small incremental cost.”

这一篇要讲清的主线
13 章一级章节(分解点)
4 式核心公式逐步重推
11 张原文图(E 8 / S 3,自绘 0)
6 条边界与反例

图-段配对表 · 本稿 11 张

图角色档归属支撑的论断层次状态
图 1Ech1运动物体身后的噪点拖尾 = 时域复用被 disocclusion 打断①②③④原文引用 (p1)
图 2Sch4域(pixel, 层) 的移位定层:层序 = 逐交点计数①②③原文引用 (p3)
图 3Sch6深度范围把逐段追光换成两次可见性判定①②③原文引用 (p4)
图 4Sch7激活域只维护很小的屏幕子集①②原文引用 (p4)
图 5Ech7选择性激活 vs 均匀 5 层:噪声相当、成本更低①②③④原文引用 (p7)
图 6Ech10补洞避免传播空档引入噪声,半径收益递减①②③④原文引用 (p7)
图 7Ech11更稳的输入让 NRD 保留细节贴近参考①②③④原文引用 (p7)
图 8Ech11NRD 的 disocclusion fix 会过度模糊①②③④原文引用 (p8)
图 9Ech9等时对比:四场景 MAPE/FLIP 全面占优①②③④原文引用 (p9)
图 10Ech10时域与空间互补,与 SPMIS 组合最优①②③④原文引用 (p10)
图 11Ech10层数越多对重叠几何越见效①②③④原文引用 (p10)

11 一个会动的灯泡,凭什么逼出更多层

解读层 · 痛点开场

如果你想盯着一帧画面找问题,先看那盏会移动的灯,或者任何一个顶着画面边界来回摆头的物件。你会在它身后看到一团灰蒙蒙、会抖的噪点,像拖在身后的一截尾巴。把这帧暂停,噪声还在;让它继续走,尾巴跟着物件一起走。总之,这条尾巴根本不打算靠加多几个采样就能消掉,它与"算得够不够多"没太大关系。

为了说清为什么消不掉,我们得先把"这一帧是怎么攒出来的"讲一遍。实时渲染里,为了在几毫秒内交出一张图,每个像素通常只投一两条主光线。就这么点预算,直接做路径追踪会糊成一团噪声。于是业界普遍采用一招:把前面几帧里已经算出来的结果攒下来,跟当前帧的采样混在一起用。这在图形学里叫时域复用(temporal reuse)。像素不用每帧都从头重算,它背着一个"历史账本",把上一帧的好采样带进当前帧,噪声就摊薄了。

这套思路在今天的实时管线里几乎无处不在,抗锯齿、超分辨率、去噪器,全都依赖它。它有一个共同的动作:把当前像素通过运动向量映到上一帧的某个像素,去取对方攒的历史。上一帧和这一帧画面大致连续,这套搬运就成立。

可问题是,画面并不总是连续的。当某样东西挡到前面,或者一个物件移开,一小块原本被挡住的表面从镜头里重新露出来。这一小块表面在上一帧是"看不见"的,它没有留下任何历史账本。你一截取,取到的是空的,或者取到的是挡在它前面的那个东西的数据,于是这块区域的光照估算只能退回去从头起步,用极少的样本硬算。

回到那盏灯。灯每动一下,它身后就新辟出一小条战场,那里全是刚露面的新表面。这些新表面没有历史,只能靠零星的新采样撑着,于是它们比画面其它地方更吵。这就是那条尾巴。它跟运动快慢有关,跟灯从哪儿来有关,独独跟你想不想静下来多算一会无关。你把它暂停成单帧,噪声依旧,因为这一帧里这些表面本来就没有多少可用信息。

ReSTIR 家族(Reservoir-based Spatiotemporal Importance Resampling)是这条道上一套很出名的工具。它把每一个像素维护成一个"抽签罐",罐里放着"当前最值得相信的那个样本",再加上空间邻域和时间上几帧攒下来的样本,一起重采样,用极低的每像素采样数换来接近干净的画面。它比普通路径追踪好太多,可它的咽喉就卡在刚才那个问题上:一旦表面被遮挡,它的罐子在整个被遮期间都不能把历史带过去。挡板一抬,它背后的采样一个不剩,全都从头再来。

如果你要的是这个问题的第一张实景照,看图 1。它把路径追踪、单层喷管的老方案、和这篇论文的方法放在同一个 Carousel 场景里,这个场景的特产就是"大量物体动来动去、频繁产生遮挡"。

图 1
图 1 效果对照图(E 类 · 证据) · 原文 Fig. 1怎么读 ①指认:Carousel 场景,左为全景,右上 3×4 网格逐格放大同一处 disocclusion 细节;栏顶时间为各方法总耗时。②读法:逐格比「disocclusion 附近谁更稳」,看的是噪声是否被拉平而非单点大小;对照路径追踪(25.8ms / 5 spp)、Splatting(15.5ms)、Ours(22.5ms)三者的噪声一致度。来源:Multi-Layer Reservoir Splatting, Fig. 1, p.1 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 1 的读法要抓住两点。一是看右上角那组小图,那里是同一处细节的 3 乘 4 网格放大,逐格比的是"谁在 disocclusion 附近更稳"。老方案那几格里,运动物体边缘附近布满了颗粒状的噪声,像被撒了一把灰沙;新方法这格明显干净。二是看栏顶那行时间戳:路径追踪要 25.8ms(还只有 5 个样本),老的喷管方案 15.5ms,本文 22.5ms。别急着说"它比老的慢",记住对面那个噪声对比,我们后面第 9 章会在同样预算下重新排队,那里会看到,一旦老的也被允许花同样的钱,它还是补不上 disocclusion 这块洞。图 1 真正想让你记住的,是那句"更一致的噪声水平":老方案在无遮挡处干净、在遮挡处突然吵,画面不能看;本文把噪声拉平了,全图一个脾性。

过去已经有人往前推了一步,用"喷管喷射"的办法处理时域复用,把上一帧的主命中样本直接喷到当前帧对应的位置,保证每个旧样本都有一个说话的机会,不轻易把重要的历史白白丢掉。这在处理高频细节时很有效,但当样本被挡、或者上一帧根本看不见的表面重新出现时,仍然会因为缺历史而变得更吵。

这篇论文想解决的,就是这件事的最后一公里。它换了一个思路:与其在"这一帧到底要不要信上一帧"上反复猜,不如直接把那些将被遮挡的样本换个地方存起来。它给每个像素多准备了几层"深抽屉",第 1 层的下方还有第 2 层、第 3 层。一层放挡在前面、当前看得见的表面,深几层就放那些被挡在后面、当前看不见但可能马上又要露出来的表面。既然这些深抽屉里本来就留着它们的账本,等它们重新露脸时,账本直接拿出来用,尾巴就不该有了。

这篇精读要讲清楚的就是这套三层抽屉的完整机关:抽屉怎么定义、怎么把旧样本准确放回正确的抽屉、以及最后怎么只养真正用得上的那几层,别把一整个屏幕的深抽屉都从第一帧养到最后一帧。


22 一句话洞见:被挡掉的阴影,先存起来等它再亮

解读层 · 一句话洞见

先给你整篇最值钱的一句话:别把被挡掉的样本扔掉,把它存进更深的层里,等它重新露脸再复用。

这句话里的"被挡掉",正是困扰时域复用十几年的那个点。你只要把"存起来等它再亮"这九个字想通了,后面所有公式、深度范围、激活域,都只是为了让这句话在几毫秒的预算里跑得起来而精打细算的工程。

先给一个数字上的体感,后面第 9 章会逐项自己复算。论文的方法在四个场景上换了整套对比,只让它自己多花一点点时间,就把 disocclusion 区域的噪声压下去一大截。在一帧 1920×1080 的画面里,真正"被遮挡后又露出来"的像素可能只占很小一部分,但这部分恰恰是眼睛盯得最紧的地方。表 1 里有一行数据能体现这套方案的两个要点:一个叫"覆盖率的代价",一个叫"射线的削减"。

先说覆盖率。论文把"均匀摊开、全屏幕所有层都养着"作为对照组,自己的"选择性激活"只维护其中很少的域,可仍然覆盖了几乎全部真正需要照顾的 disoccluded 像素。以一帧 Carousel 为例,均匀方案要养 732 万个深度域,选择性激活只养 191 万个,差距接近四倍,却仍盖住了 88% 的 disoccluded 像素。换句话说,深抽屉不是开给全屏幕的,只开给那些"马上可能重新露脸"的位置,就够用了。

把"盖住多少"和"花多少域"放在一起看,才能读出这句话的分量。同一个 Carousel 帧里,真正需要照顾的 disoccluded 像素,均匀方案盖住了 24 万个,选择性激活盖了 21.8 万个,两者都几乎把该盖的都盖了;可为了盖这一片,均匀方案撒下去了 732 万个域,选择性激活只用了 191 万个。域是逐域的存储和逐域的追光,域少四倍,意味着存储省了约 74%,splatting 阶段的射线也少了一大截。换句话说,这篇论文没有掏东墙补西墙——省下来的每一分都落在刀刃上,覆盖掉的洞一个不少。

再说射线。给一个移位样本定层,通常要逐段追光线数它到底穿过几层表面。论文用深度范围把这活儿换成两次可见性判定,省掉了大量追光。这部分在表 1 里会看到射线数量往下掉,具体数据留到第 9 章排开。

于是整篇的可验证主线是这样一条链:问题(时域复用断在 disocclusion)→ 方案(多层抽屉接住被挡样本)→ 两个效率开关(深度范围省射线、激活域省域数)→ 证据(等时对比、消融、去噪集成)。下面的章节就沿着这条链把每一步拆开揉碎,公式自己重推,数据自己复算。


33 时域复用的家谱:从打帧缓存到喷管映射

把视野拉远一点,看看"复用上一帧"这个念头是怎么一步步走到今天的。这不是跑题,因为这篇论文的每一步都要站在前面每一招的肩膀上,你不把这套家谱理清,就看不出它替掉了什么、留下了什么。

时间回到更早的实时渲染。今天的游戏画面,几乎没有谁是完全每帧从零开始算的。谁来抗锯齿?时域抗锯齿(TAA)把历史帧的颜色和当前帧混合,边缘就不再每帧抖。谁来提分辨率?DLSS、FSR 这些超分辨率方案,把低分辨率输入往高分辨率重建,靠的也是把时间上攒下来的信息搬上来。谁来压蒙特卡洛噪声?实时去噪器(如 NRD)把历史样本和当前样本做时空滤波。这些工具共同的一个动作,就是用运动向量把当前像素映射到上一帧的对应像素,去那里取历史。

这套映射的前提,上一章已经点过,是"画面大致连续"。它一旦在遮挡处断裂,历史就取不到了。所以这个家谱里出现了一个关键的岔路口:能不能不靠"把当前帧的像素找上一帧的像素"这种回溯式搬运,而是反过来,把上一帧的样本"推"到当前帧?

ReSTIR 把每像素变成一个抽签罐,用重采样在时空间反复淘金。它先解决的是直接光照,后来被推广到全局光照,是目前实时路径追踪领域最响亮的名字之一。到了 Area ReSTIR,作者们又把积分域从"像素中心的一个点"扩大成"完整像素脚印",让我们能直接估计一个像素滤波后的颜色,还顺带把抗锯齿做了。它靠的是跟随分数运动向量,把像素反投影回上一帧的某个偏离采样网格的位置,再在那个区域里重采样。

Area ReSTIR 的车辙里有一块反复硌脚的石头:当子像素细节对应着多条运动向量时,回溯式的搬运就不灵了。而 disocclusion 恰恰就是这种情形,一条像素光线上能同时有好几层表面,几条运动向量打架,到底该去取哪一帧的哪一层?

于是出现了这篇论文直接站在肩膀上的那位前辈,Reservoir Splatting。它的想法是:别再"把当前帧的像素回溯去上一帧找",改为"把上一帧的主命中样本,顺着它跟物体的运动,直接喷进当前帧"。每个喷进去的样本,落在哪个像素,就对那个像素直接出数。这一改有两个好处:时域样本的利用率更高了,而且原来要花重金去追的新主射线的钱,被更便宜的可见性检查替代了。

但这位前辈有个坏消息,就是它是单层的。一个被遮挡后重新出现的表面,依旧没有任何被喷进去的历史可借,噪声照样亮起来。它缺的,正是"被挡住的东西别扔,存进更深一层"这个动作。这篇论文做的第一件大事,就是把 Reservoir Splatting 从单层升级成多层,让喷管不仅能把样本喷到某一层的像素上,还能喷到那个像素的更深的某一层抽屉里。现在,被挡住的样本不再只能被丢弃,它可以被安排到背后那一层,等重见天日。

把这条家谱摆在桌面上横着比,能读出一条更深的规律:从 TAA 到 ReSTIR、Area ReSTIR 再到 Reservoir Splatting,每一代真正进步的地方,都是"历史"这个资产被搬运和承载的方式。TAA 把它存在像素颜色里,问题是颜色已经被滤波过了,丢了原始分布信息;ReSTIR 把它升级成一个能反复重采样、带置信权重的样本罐,历史于是变成了"可变现的样本"而不是"已抹匀的颜色";Area ReSTIR 又给罐子配了像素脚印级别的空间精度,让它能跟高频法线贴图共处;Reservoir Splatting 则干脆把"从上一帧取历史"改成"把样本推给下一帧",从根上绕开了回溯式搬运在遮挡处的歧义。而这篇论文,是在"罐子"这条主线上再钉下一颗钉子:单罐变成一摞罐,让历史不因为一时被挡就永久报废。沿着这条轴看,本文的贡献就是把家谱里那条"怎么让历史活得更久、更值钱"的主线又往前拱了一格。这也解释了为什么它的贡献能跟前面每一代接得上茬,而不需要推翻任何一个人。


44 域:像素跟层的配对,以及一次遮挡怎么把它拆散

在动手推公式之前,得先把这套新世界里最基本的名词立起来。这篇论文把"像素-层"这个组合称为一个域(domain),写作 (像素序号 p, 层序号 i)。第 1 层域就是直接看得见的那层,第 2 层、第 3 层依次往深走。每一个域,其实就是一个抽签罐的所在,维护着它自己的样本历史。

为什么要给同一个像素开好几层罐子?回到那盏灯。假设灯前面站着一根柱子,灯在柱子后面,当前帧里灯被柱子挡住,看不见。可镜头稍微一动,柱子和灯的相对位置变了,灯从柱子侧面探出来。在灯探头出来的那一瞬,屏幕上一个像素的视线之前是柱子,现在变成了灯。这个像素在第 1 层的位置,本来记录的是"柱子的表面",此刻应该换成"灯的表面"。可灯的表面在上一帧是被挡住的,没有账本。要是我们能记住,上一帧在柱子后面、距离更远的地方,还有一层灯的样本,那么当下这一帧,把第 1 层的身份让给灯,把柱子的账本往深挪一格就好。离散的东西要连续地挪,这就是多层抽屉的用处。

图 2
图 2 机制示意图(S 类 · 原文) · 原文 Fig. 2怎么读 ①指认:上一帧/当前帧两栏;样本从兔子表面发出,标注域坐标 (像素, 层),如 (3,1)→(2,3)。②读法:跟随样本的喷管移位,从相机对重投影后的样本投射线、逐交点数层序,数到几就归第几层;该样本从第 1 层落入第 3 层。来源:Multi-Layer Reservoir Splatting, Fig. 2, p.3 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 2 画的是这个"跨域移位"的全过程,把抽象的名词落到了具体的画面上。图中一帧叫上一帧,一帧叫当前帧,中间画了一个从兔子身上发出来的样本。简单说,这个样本在上一帧属于第 1 层域(像素 3, 第 1 层),因为兔子当时直接看得见。当前帧里兔子移到了别的位置,这个样本跟着兔子的运动,被喷到了新的像素。要判断它现在应该属于哪一层,办法是:从相机朝这个重投影后的样本方向投射线,每穿过一层表面就数一格,数到它为止。图中数出来是 3,于是它被归入 (像素 2, 第 3 层)。

注意这套"逐交点计数"的动作,它既定了层,又暴露了成本:每给一个样本定一次层,都要沿着射线一路数过去,穿过几层就追几根光线。这是后面第 6 章要动手优化的第一个目标,先记下来。

图 2 里还藏着两个跟后续章节扣得上的细节。其一,图中出现的专属名词"域(domain)"不是随口的说法,它精确地指"一个像素配一层"这个整体,样本、历史、置信权重,统统挂在 (p, i) 这个组合名下。跳出图来说抽象的定义:一个域,就是一颗抽签罐安的家。全文后面说"激活某个域"、"推移某个域",说的都是对整罐状态的搬运,而不是对单个样本的搬运。提前把"域"和"罐"对齐起来,后面第 8 章实现走查里"成批搬罐子"的动作才不会显得突然。其二,图 2 里那个样本跟着兔子一起动,是"喷管映射"的直观写照,它是第 3 章那位前辈 (Reservoir Splatting) 留下的核心动作,本文只是把它从"只能跨像素"升级成"还能跨层"。

再看一眼单层的时候这里发生了什么。没有多层这个设定时,被挡住的样本如果通不过可见性测试,就直接丢弃。多层方案只是把"丢弃"改成了"往深一层安家"。这个改动的数学勇气很大,因为样本虽然换了层,但它那颗抽签罐的历史并没有丢,等到表面重新露出,第 1 层这个位置正好缺历史时,它就能顶上。这正是整篇方法的心脏。

这里有个容易想糊涂的点,值得单独拎出来:被挡住的样本为什么还要留?表面看,当前帧它看不见,留它对当前帧没直接用处;但画面是流动的,这块表面很可能在下一帧、下下帧就探出头来。牺牲当前帧一点微不足道的深层存储,换来未来某一帧一整块区域的历史不空缺,这笔账在"物体经常动、场景反复遮挡"的游戏场景里是稳赚的。这正是"为当下看不见的东西存账本"的时间价值,也是全文所有效率开关要保住的底线:只能存那些"真的可能再露"的,别为了小概率事件把全屏幕都背成仓库。


55 多层积分:把第 i 次命中也当成"直接可见"去算

上面用画面讲了道理,这一章用公式把它钉死。我们要做的,是把"这一个像素的积分"从一层推广到多层,并且每一步都自己重推一遍,不直接引用原文的式子号糊弄过去。

单层的世界大家都熟。一个像素的辐射(radiance)是个对路径空间 Ω 的积分:

(1)I_1 = ∫_Ω f(x̄) dx̄

这里 x̄ 是这条从相机出发的路径:x0 是像素上的相机位置(针孔相机),x1 是路径在场景中碰到的第一个表面顶点,再往后 x2、x3 是后续路径顶点。f(x̄) 是这个路径对像素的贡献。对第一层来说,一条路径要真的对画面有贡献,得满足一个前提:x1 从 x0 看过去必须是可见的,也就是可见性项 V(x0, x1) = 1。如果 x1 被别的表面挡住,V = 0,这条路径对第一层没贡献。这就是"第一层"被"深深限制"住的原因:它只算那些第一眼就看得见的表面。

现在把局面放宽到多层。对第 i 层屏幕空间层,论文定义了这样一个集合:

(2)A_i = { x | rank(x) = i }

这里的 rank(x) 是交点 x 沿相机光线上的次序。rank(x) = 1 表示 x 是这条光线沿途的第一个交点,即直接可见的首个表面;每再往里多穿过一层表面,rank 就加 1。于是"第一层域"就对应 rank = 1 的交点集合,"第二层域"对应 rank = 2 的集合,依此类推。只要我们能顺着射线把交点一个个数出来,层也就定了,这正是上一张图做的事。

把"路径贡献"的口径从"只看第一个可见交点"放宽成"第 i 个交点被当作直接可见",第 i 层的积分就长这样:

(3)I_i = ∫_Ω [x1 ∈ A_i] g(x̄) dx̄

对比单层的 I1,这里有两个差别。第一个差别是方括号里的"层归属"条件 [x1 ∈ Ai],它等价于 rank(x1) = i,这条路径只有在 x1 恰好落在第 i 层时才计数。第二个差别是,原来的 f(x̄) 换成了一个去掉首项可见性的 g(x̄)。f(x̄) 里本身含着一个 V(x0, x1),它强迫 x1 必须从相机直接可见;而 g(x̄) 的定义,恰恰是 f(x̄) 把 V(x0, x1) 这一项摘掉之后的产物。这个"摘掉"很关键,它意味着:在算第 i 层的积分时,我们不要求 x1 从相机直接可见,因为第 i 层的语义本身就是"这个交点被当作直接可见的那一层"。真正管它可见不可见的,是那一层前面更近的表面。

一句话收束:第 i 层的积分,就是在"把第 i 近的那个交点当作直接可见"这个假设下,这条路径本该贡献的辐射。 每一层各自的 Ai 集合,把完整的路径空间按"第一个交点是第几近"这个标准切成了互不重叠的几块。

把这一章用到的符号收进一张表,方便正读到一半就被公式卡住的读者随时回头查。

符号含义约束 / 说明
x̄一条从相机出发的路径,顶点 [x0, x1, x2, …]x0 为像素上的相机位置(针孔)
x0相机点本文假设针孔相机
x1相机光线第一个表面顶点决定层级归属的关键
f(x̄)这条路径对像素的完整贡献含首项可见性 V(x0, x1)
V(x0, x1)首交点可见性项=1 当且仅当 x1 从 x0 直接可见
g(x̄)f(x̄) 摘掉 V(x0, x1) 之后的剩余多层积分用它,不再强制直接可见
Ω路径空间对所有路径积分
rank(x)交点 x 沿相机射线的次序rank=1 为首个交点
Ai第 i 层域对应的交点集合单层严格=rank(x)=i;深层被深度范围放宽
Ii第 i 层积分把第 i 近交点当直接可见去算的贡献

这张表里藏着一个要反复回答的追问:为什么把 V 摘掉成 g,第 i 层积分里的可见性逻辑还是对的?答案是,可见性并没有消失,而是被"层归属条件" [x1 ∈ Ai] 接管了。x1 落在第 i 层,本身就说明它前面还有 i−1 个更近的表面在挡着,它"不可直接从相机见"这件事,已经被层归属隐含地断言了。我们只是不需要再重复追问它。这也是为什么整套方法能放心地把"可见性判断"从积分里抽出来,交给"然后的影子判定"去管,这正是下一章深度范围的伏笔。

那为什么这套多元积分,在 disocclusion 下就能救场?论文的论证要点是这样:当一个像素发生 disocclusion,它的当前第一层积分 I1,经常能和上一帧的某个更深层积分 I_prev_i 对得上,因为两者对应的"有效的 x1 集合"是重合的,或者非常接近。上一帧里这块表面被挡在第 i 层,当前帧它露到了第一层,可它露出来的还是同一块表面,路径空间里那一段 x1 的邻居集合几乎没变,于是历史就有了用武之地。只有当遮挡物本身的运动改变了背后的其余路径空间时,比如挡路的东西恰好挡住了射向它背后表面的光源,此时光照条件真的变了,时域复用再信任历史就不对了。这个差值什么时候会长出来,正是第 12 章边界要回过头来收拾的地方。


66 深度范围:两次可见性判定,替掉逐段追光

上一章结尾,我们已经攒下一个不小的成本隐患:要判定一个移位样本该进第几层,最直接的办法是沿着相机射线逐段追光,数它到底穿过几层表面。第 4 章的图 2 就是这么数出层序 3 的。可如果场景里一撮抠细节很多、层次很深的地方,这个"数人头"的动作能把预算吃干。这一章动手治它,用的是深度范围(depth ranges)。

先讲清作者观察到的那条裂缝在哪。原原本本的层定义 Ai = { x | rank(x) = i },其实只在第 1 层上是被严格需要的,因为最终落到画面上的、被所有人看见的,只有第 1 层的积分 A1。到了更深的层,我们未必需要每一层的归属都数得分毫不差,只要大致对就行。这个"可以放松"的空间,就是优化进来的地方。

于是论文做了这样一个工程动作:先给每个层预先算好一个不重叠的深度范围(depth ranges)Di = [Li, Ri],然后放弃"严格按 rank 数到第几层",改按"样本离相机的距离落在哪个深度区间"来定层。这样一个粗一些的划分,可能会丢掉一些样本,因为每个深度区间里只有最靠前(最近)的那个表面才被视为有效,但换来的是定层的代价大幅下降。

用公式把"较深层的域"重新定义一下,就是原文的式 4:

(4)A_i = { x | d(x, x0) ∈ D_i, V(x, x0) = 0, V(x, x0 + L_i·r(x)) = 1 }

逐个条件拆开看。第一个条件 d(x, x0) ∈ Di,是说这个点距相机的距离落在第 i 层的深度区间里。第二个条件 V(x, x0) = 0,说的是这个点从相机看过去不是直接可见的,因为它是深层的,前面必然挡着别的东西。第三个条件 V(x, x0 + Li·r(x)) = 1,其中 r(x) 是从 x0 指向 x 的单位方向,x0 + Li·r(x) 就是从相机沿这条方向走到第 i 层深度区间的近端 Li 处的那个点;这个条件断言:从那个近端点再往前看,x 是这条方向上的第一个可见交点。三个条件叠起来的意思很明确:这个点既不在第一层(被挡),又从它所在深度区间的近端一探头就能看见。 它就归第 i 层。

这个定义替换掉"逐段追光数交点",关键在于定层流程变成最多两次可见性判定:

第一问,先把一个从相机可见性的光线发出去,测 x 是不是属于 A1。如果直接可见,就归第 1 层,完事。如果不可见,走到第二步。

第二问,用 x 到相机的距离 d(x, x0),在预计算好的那一组不重叠深度区间里查它落在哪个区间 Di,然后只在 Di 这个区间的范围内发一条受限的可见性光线,测它是不是这个区间内最近的那个交点。如果是,就归第 i 层;如果它的距离没落在任何一个区间里,就判无效,不参与当前帧。这就是图 3 要画的东西。

图 3
图 3 机制示意图(S 类 · 原文) · 原文 Fig. 3怎么读 ①指认:三个样本 x1 x2 x3 及各自的深度范围条、受限可见性光线;水平粗条为预计算的深度区间。②读法:先看 x1 直接可见→第 1 层;x2 不可见但距离落在第 2 层区间、且是该区间最近交点→第 2 层;x3 不落任何区间→无效。来源:Multi-Layer Reservoir Splatting, Fig. 3, p.4 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 3 把三个样本的归宿一次画清。样本 x1 直接看得见,它走第一问就出结果,属于第 1 层,绿色标记是它所在的深度范围。样本 x2 看不见,但它的距离落在第二层的深度范围里,那条蓝色的受限可见性光线(只在这个区间内)确认它就是这个区间最近的交点,于是归第 2 层。样本 x3 看不见,又不落在任何一个深度范围里,于是被判无效。图里绿色的横条就是每个层预计算好的深度范围,它让"查层号"这个动作从"沿着光逐个剥皮"变成了"先看距离落哪个区间,再往区间深处探一下"。

这套深度范围的预计算,是每帧开始时在像素中心沿视线找到的那组代表交点 cp,i(第 p 个像素第 i 层的代表命中)的深度,被展开成不重叠的区间。因为每个像素的视线穿过的表面层次不一样,这些深度范围是按像素各自算的,不是全屏共用的同一把尺。

实现上还藏了两个要交代的细节。深度范围怎么定?论文用每个域代表命中的深度 t,直接把它往两边撑开一个很小的比例,取 [(1−ε)·t, (1+ε)·t],实测里 ε 取 0.01 就够用。另一个细节,是多个区间可能重叠:如果同一像素上相邻两个区间 [Li, Ri] 和 [Li+1, Ri+1] 撞在一起,就把它们的交界处晾到中点上,即设 R'_i = L'_{i+1} = (Ri + Li+1) / 2,保证每个深度只有一个归属。

把"两层可见性判定"的完整选择逻辑写成一个可照抄的流程,比抽象描述更能体现它到底省了什么:

# 给移位样本 x 定目标层(深度范围版)
# 前提 1:每帧开头,已按像素中心交点把每个像素的深度范围算好、去重
# 前提 2:已知 x 到相机的距离 d(x, x0) 与方向 r(x)

if 从相机向 x 投一条全局可见性光线,x 直接可见:
    target_layer = 1
else:
    D_i = 命中的那个包含 d(x, x0) 的深度区间   # 二分/线性查
    if 命中 D_i 且 在 D_i 内投受限可见性光线,x 是该区间最近的交点:
        target_layer = i
    else:
        x 无效,不参与当前帧的 splatting

对照第 4 章"逐交点数层"的老办法,这个版本的省法一目了然。老办法坏就坏在"逐段"两个字,你永远不知道要剥几层皮,运气差时一根光线能剥出几十个交点,每个交点都是一次耗时追光。新版本固定的就是两次:先问"你直接可见吗",再问"你所在区间里是不是最前那个",最多两次可见性判定就收工,跟场景里到底叠了几层表面无关。这个"成本与深度无关"的性质,是下一章把多层仓库塞进实时预算的第一块承重墙。


77 激活域:别把所有层从第一帧养到最后一帧

到目前为止的多层设计有个数学上爽快、工程上吓人的问题:如果每个像素都常备 k 层抽屉,那么全帧 N 个像素就是 k·N 个域,ReSTIR 的成本直接乘了个 k。可现实里,绝大多数像素根本用不上那么深的抽屉。室内场景里,那些来自远处房间、几乎永远不可能探到镜头前来的表面,你为了它养着一个深层的罐子,是纯浪费。这一章解决"别养用不上的层",正是论文的第三个贡献,叫激活域(active domains)。

写公式的直觉先行一步:一个样本,如果它曾经在某几帧里当过主命中(primary hit,也就是直接可见的首个表面),后来才被挡到后面去,那它未来重新露面的可能性就比较高,值得保留它的账本;反之,那些从来就没在主层亮过相的深层样本,基本不用管。这听起来像是要全局调度判断,论文却用一个极便宜的"传递式激活"把它做成了逐帧流式规则,一条规则同时管住了"激活谁"和"什么时候灭掉"。

规则是这样的。第一帧,只有主层(第 1 层)的所有域是活的。之后每一帧,凡是"活的域",其代表命中 cp,i 会被喷管喷到当前帧,喷到哪儿,就把当前帧对应位置的域 (p', i') 激活;当前帧新亮的域,自然也是激活的。因为域是存在屏幕空间的,一旦某个域的代表命中被喷出了屏幕边缘,它就自动失活,连判都不用判。这样一整条因果链跑下来:"谁以前亮过,谁就可能再亮",全自动成立,不需要任何全局知道。

但喷管本身会在屏幕上留下洞。因为样本是离散的点,往屏幕上喷的时候,有些像素可能一个都没喷到,留下空档,这些空档如果刚好是需要历史的地方,就会暴露噪声。论文用一个朴素的补洞(hole-filling)对付它:把已激活的域周围一定半径内的邻居也一并算作激活,让它们参与采样,但不让这些"补位"的域去激活下一帧的域。实测里这个补洞半径取 3 个像素。这么简单的一条,作者说就足以保住局部连续性,稳定重采样结果。

激活域的效果,用两张图能看明白。第一张看"只养一小片"。

图 4
图 4 机制示意图(S 类 · 原文) · 原文 Fig. 4怎么读 ①指认:Bistro 场景第 2 层,左为全部域、右为激活域,法线伪彩色标朝向,灰=无表面命中,黑=未激活。②读法:对比两栏黑色的多少——激活只留下很稀松的一小撮,即把计算压回「上一帧真亮过、本帧要照顾」的窄条。来源:Multi-Layer Reservoir Splatting, Fig. 4, p.4 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 4 拍的是 Bistro 场景第二层里,全部的域和激活后的域长什么样。左边是全部域,右边是激活域,中间用法线伪彩色标出每个域中心命中的朝向,灰色表示这个位置没有表面命中,黑色专门标出那些"没有激活"的域。一眼望去,激活后的那块,屏幕上一大片都是黑的,真正亮着的只是很稀松的一小撮。这正是论文想要的结果:把计算都压回到那些"上一帧真的亮过、本帧又必须照顾"的窄条上,全屏幕只维护一个很小的子集。

第二张图回答一个更尖锐的问题:这么抠门地只养一小片,够用吗?图 5 在中高难度的 Carousel 场景上,把 Reservoir Splatting、均匀五层的多层实现、以及本文的选择性激活,三架摆在一起比。

图 5
图 5 效果对照图(E 类 · 证据) · 原文 Fig. 5怎么读 ①指认:Carousel;上排为 disocclusion mask(红=发生 disocclusion),下三行依次 Splatting / Ours / Uniform 5 layers,附各自耗时。②读法:看每一行的噪声是否只出现在 mask 区——均匀五层(42.8ms)噪声平但贵,选择性激活(22.5ms)与它噪声相当却几乎减半;右下黄框是补不上的残余噪声。来源:Multi-Layer Reservoir Splatting, Fig. 5, p.7 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 5 的上半部分那一行,是所谓的 disocclusion mask,红到发亮的地方标出当前帧里真正发生了 disocclusion 的像素,让你先知道"问题长在哪"。下面是三行结果,Splatting(15.5 ms)、Ours(22.5 ms)、Uniform 5 layers(42.8 ms)。看三行的噪声表现:Splatting 的 disocclusion 区域明显更吵;均匀五层把噪声压得很平,但它烧了 42.8 毫秒;选择性激活的 Ours,噪声水平和均匀五层看齐,却只花 22.5 毫秒,几乎减半。一个关键点在掩码图里也标了出来:噪声多的地方,并不只局限于当前帧的 disocclusion 区,因为画面需要几帧时间才能从上一次的 disocclusion 里缓过来。这一点,正好是后面第 9 章复算表里"覆盖率 88%"含金量的注脚,它说明选择性激活先照顾的就是大部分该照顾的地方,而不只是刚好盖住动的那一小块。

那是不是所有 case 都被覆盖了?不是。图 5 右下的黄色小框指明了一处残余噪声,那往往是因为上一帧没有把该域的主层样本都保留下来,于是这一帧就露了个破绽。这个"覆盖不全"的短板,会原样出现在第 12 章的边界清单里。

到这里,方法的三大块已经立起来了:多层抽屉接住被挡的样本(第 4、5 章)、深度范围把定层层数从逐段追光砍成两次判定(第 6 章)、激活域只养真正会用上的那一小撮(这一章)。接下来看它在一整帧里怎么把这些零件拧成流水线。


88 一帧怎么跑完:从推移域到紧凑缓冲

公式和机制都说清了,这一章把它们落成一段能照着实现的逐帧流水线。硬件是 Falcor 框架,跑在 Intel i9-13900K 加一块 NVIDIA RTX 4090 上,画面 1920×1080。这一帧里,环境里的每件事,都有固定的先后。

每个域(一个抽签罐)在内部存两个表面命中:一个是"代表命中"(从像素中心投出去的,用来代表这个域往屏幕上的落点),一个是"子像素样本命中"(用来估计这个域的积分)。先记住这个分工,后面每一步都要用它。

帧循环的十个动作,按顺序排开:

第一步,推移激活域。 把上一帧所有激活域的代表命中,用喷管喷到当前帧,得到它们新的像素位置。新的层序 i',由"从一个假想的相机对重投影后的代表命中投射、数经过的交点"得到,也就是要重走一次喷管映射判层。这一步生产出当前帧的候选域骨架。

第二步,补洞。 围绕上一步喷出来的激活域,执行第 7 章说的 hole-filling,把半径 3 像素内的邻居补成激活态,让局部连续。

第三步,追新代表命中。 对这些激活的子域,从像素中心投新光线,拿到它们全新的代表命中 cp',i'。

第四步,真假阳性防御。 这一步很关键,因为平移是靠点的近似完成的,会误激活一些不该激活的域(假阳性),白白烧性能。防御动作是:把上一帧代表命中的"直接遮挡物"和当前帧新代表命中的"直接遮挡物"比对一下,两个遮挡物的实例 ID 不一样,就认为这个域不该被激活。这一刀切得很值,因为遮挡物变了,说明这层的历史未必还能原样套用。

第五步,算深度范围。 用刚才那些激活域代表命中的深度 t,按第 6 章的办法展成 [(1−ε)t, (1+ε)t] 的区间,再把同一像素上可能重叠的区间按下接中点的方式铰开。

到这里,当前帧的域和它们的属性都初始化齐了,接下来是采样正戏。

第六步,初始重采样。 为当前帧所有激活域生成规范样本(canonical sample)。这一步是在给"这个域对应积分"投出一个干净的估计值,是保住积分无偏性的地基,不能省。

第七步,时域重采样(多层喷管)。 先把上一帧所有激活域的样本喷进当前帧,凡是落在某个深度范围外的样本直接拒收。接着,跟 Reservoir Splatting 一样,把当前帧所有激活域新生成的样本再喷回上一帧,去算 MIS 权重。为了在时域复用里保无偏,每个激活域的置信权重要用它代表命中反投回上一帧、去复用上一帧对应罐子里存的置信权重。

第八步,空间重采样。 只对第 1 层做。论文明确说,对深层做空间复用的质量收益,不值那个额外的开销,于是砍了。

第九步,存储压缩。 把激活域都压进一块紧凑的连续缓冲,一个域一个线程去跑。会这么设计,是因为深层的域在屏幕上是稀稀拉拉撒开的,压成连续缓冲能显著提高线程利用率。

第十步,剔除背面。 背面朝向的表面本来就不贡献画面,从域构造里直接拿掉。落到实现上,等于只保留奇数次表面交点:第 i 层恰好对应沿相机光线的第 2i−1 个交点。

把这段流水线翻译成伪码,能更直观地看出哪些步是为了质量、哪些步是在省钱:

# 每帧循环 · Multi-Layer Reservoir Splatting
# 前提 1:每个域存两个命中:代表命中 c(像素中心)+ 子像素样本命中
# 前提 2:域是屏幕空间的,出屏自动失活
# 前提 3:上一帧已维护好激活域集合 active_prev

active_cur = ∅

# 骨架推进
for (p,i) in active_prev:
    c' = splat(c_{p,i})                    # 喷管映射到当前帧坐标
    layer' = count_intersections(c')       # 逐交点判新层序(第 4 章)
    active_cur ∪= {(p', layer')}
active_cur ∪= hole_fill(active_cur, radius=3)   # 补洞,只参与采样

for (p',i') in active_cur:
    c_{p',i'} = trace_from_pixel_center(p')      # 追新代表命中
    if occluder(c_{p,i}) != occluder(c_{p',i'}): # 假阳性防御(实例 ID)
        active_cur.discard((p',i'))

compute_depth_ranges(active_cur)             # ε=0.01,区间去重叠

# 采样
initial_resampling(active_cur)               # 生成规范样本,保无偏
temporal_splatting(active_prev → active_cur) # 多层喷管 + MIS 权重 + 置信回接
spatial_resampling(layer_1_only)             # 只做第 1 层

十步走到这里,一帧的活就干完了,各层的罐子改朝换代,把该带的历史带到下一帧。门禁选在第 1 章开头那个"灯后尾巴"上回头看,这条流水线换走的东西,正是那条尾巴的来源。是骡子是马,下一章拉去四个场景遛。


99 同样的预算,谁更稳:四场景等时对比

方法讲完,把自己拉出来跟老办法打一架。论文挑的战场很讲究,叫"等时对比":所有方法都用相同的预算,谁在同样的时间里画面更干净,谁赢。因为 ReSTIR 家族的天职就是在短预算里干活,不比精度而比"同样几毫秒里谁的噪声更小",才公平。

对手是谁?第一个是 Reservoir Splatting 原版。它没被设计来处理 disocclusion,于是作者又给它加了两个"临时抱佛脚"的变体,只针对 disoccluded 像素补救:一个是在初始 RIS 阶段把每像素样本数(SPP)提高(叫 more initial spp),一个是把空间复用的邻居数提高(叫 more neighbors)。这两个变体的开销都被调到跟本文方法相当,好让对比站在同一个起跑线上。也就是说,这篇论文被逼着证明:你没有靠多烧预算换干净,你的多出来的钱全花在刀刃上。

场景四个:Bistro、Subway、Emerald Square、Carousel。前三者主要是镜头在动,后者是大量物体在动,两者都能制造出 disocclusion,但成因略有不同。误差用两个指标报:MAPE(平均绝对百分比误差)和 FLIP(一个更贴近人眼感知的差异度量)。

图 9
图 9 效果对照图(E 类 · 证据) · 原文 Fig. 9怎么读 ①指认:四场景各一行(Bistro / Subway / Emerald Square / Carousel),底部 Time / MAPE / FLIP;每行多列对应不同对手。②读法:这是等时对比——同一预算下比谁更干净,必须把耗时与 MAPE/FLIP 一起读;全局指标被 disoccluded 像素稀少稀释,真正差距在放大框的局部方差。来源:Multi-Layer Reservoir Splatting, Fig. 9, p.9 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 9 是四场景等时对比的大图,每个场景一行,底部标着 Time / MAPE / FLIP。先把每一行的"Time / MAPE / FLIP"逐项读下来。以 Carousel 为例:Splatting(15.5ms / 1.863 / 0.696)、More neighbors(15)(23.0ms / 1.811 / 0.677)、More initial spp(12)(22.8ms / 1.733 / 0.668)、Ours(5 layers)(22.5ms / 1.631 / 0.624)。你看,两个变体把开销堆到跟 Ours 一样高,指标才只降到 1.811、1.733,而 Ours 用一个更像样的数字 1.631 站在后头,花的还一样多。

作者自己也补了一句很重要的话,我们要原样强调:因为误差是对整幅图算的,而 disoccluded 区域只占画面很小一块,所以在全局 MAPE/FLIP 上,本文方法的量化增益是"被稀释"的,看着不大。真正的差距在画面上,尤其是那些放大框里,Splatting 在 disocclusion 附近的高方差一目了然,Ours 把它们压下来。这个"数字不大但图上明显"的提醒,是读这张图时必须记住的判读前提,否则你会以为它赢得很勉强。

我把 FLIP 这一栏亲自复算了一遍,见下表的"降幅"列。

场景Splatting FLIPOurs FLIPFLIP 降幅(复算)
Bistro0.7550.609−19.3%
Subway0.7330.610−16.8%
Emerald Square0.6960.624−10.3%
Carousel0.4460.413−7.4%

仔细看这排数字能读出两条信息。一,四个场景 FLIP 都降,没有一个反弹,说明方法在"遮挡成因不同"的几类场景里都站得住。二,Carousel 的降幅最窄,只有 7.4%,而它恰好是物体运动量最大、disocclusion 最难啃的一个。这跟 Carousel 的量化误差本身被"全局误差稀释"得最厉害是一致的,绝不是它没在干活,恰恰相反,它干得越多,误差被摊到全图后越不明显。

表 1 给了这场对比背后更细的成本拆账。我挑了三个最能说明问题的维度自己复核。

第一维,深度域的数量被选择性激活压下去多少。 用 Carousel 看,均匀方案要养 732.0 万个深度域,选择性激活只养 191.2 万个,省了约 74%。Bistro 更狠,从 207 万个压到 26 万个,省约 87%。而又重看一遍覆盖率:虽然占比低了,但深度域盖住的 disoccluded 像素绝对数并没有崩,Carousel 从均匀的 240164 个盖到选择性激活的 218358 个,仍然是绝大多数。别被"88.0% vs 97.0%"这种占比数字骗了,占比低是因为它不为全屏服务,它只为"要动的那些地方"服务。

第二维,深度范围替追光,射线省了多少。 表 1 专门列了"有/无深度范围"两套 splatting 阶段射线数。Emerald Square 从 17.42M 条降到 9.29M 条,省约 46.7%;Carousel 从 29.32M 条降到 14.41M 条,省约 50.8%。这正是第 6 章那个"两次可见性判定替掉逐段追光"在数字上的兑现。

这时候要停下来较一个真。论文在正文里写,深度范围让 splatting 的射线"最高降 59%"。但我按表 1 各场景里最大的那一个算,只能算到约 51%(Carousel 那一格,−50.8%)。这是一个值得标出来的口径缝隙。最可能的原因是,59% 指的是某个更具体的局部路径(比如某一帧、某一种查询),或者是计算口径与我这边的"整表加总对比"不同,正文没有展开写清。这里我不替它圆场,直接把原值、我算出的降幅、和"与 59% 的差值"都摆出来,让数字自己可核。这是这个分区一贯的做法:复算要能对得上原文,对不上就把缝隙亮出来,而不是糊弄过去。

第三维,splatting 阶段的时间,深度范围的收益落到了实处。 用 Emerald Square 看,splatting 时间从 4.60ms 降到 3.05ms;Carousel 从 6.05ms 降到 3.93ms。时间下降的幅度和射线下降的幅度基本同步,说明省的就是追光那一截,落账干净。内存方面,因为维护的域变少,内存用得也少,最省的一档从 380MB 起步,都在可接受范围。

结论可以下了。在这场"同样的预算比谁稳"的较量里,Splatting 要靠砸 SPP 或砸邻居数才能跟 Ours 打个平手,而砸了之后指标还略逊一头。Ours 的多层历史,让它在 disocclusion 变得多大、初始采样多难时,都能维持住一个稳定的成本和稳定的质量。这两头,是它跟"靠加预算硬顶"的根本分野。


1010 拧参数:层数、补洞半径、和 SPMIS 搭伙

一个方法好使,不够,还得知道"哪个旋钮管什么、拧过头会怎样"。这一章把三个旋钮挨个拧一遍:层数拧多少、补洞半径拧到多大、以及跟另一种处理 disocclusion 的空间派思路怎么搭伙。

先拧层数。抽屉开多少层,直接决定能接住多少"被挡住的历史"。

图 11
图 11 效果对照图(E 类 · 证据) · 原文 Fig. 11怎么读 ①指认:Carousel 层数消融,上到下 1/2/3/5 层,各附耗时,右侧三处放大区 + Reference。②读法:看每次加层三处 disocclusion 是否逐档降噪;再看时间只从 16.4ms 涨到 23.0ms——因只维护激活域,成本不随理论层数翻倍。来源:Multi-Layer Reservoir Splatting, Fig. 11, p.10 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 11 是 Carousel 场景的层数消融,一行一档:1 层、2 层、3 层、5 层,从上到下 Time 分别是 16.4ms、18.2ms、20.2ms、23.0ms。往下看,每多开一层,图中那三处放大的 disocclusion 区域就各降一档噪声,层层见功。物理解释很直接:同一根视线上叠的几何越多,那些深处的样本就越需要抽屉来接,层数太少,接不完,露出还是吵。所以凡是"重叠几何多"的场景,就得多开几层才喂得饱。而有趣的是成本:因为维护的只是激活域,多开几层并不会让每一层都全屏烧钱,层数从 1 涨到 5,时间只从 16.4 涨到 23.0,涨的幅度远小于"多维护 4 层"这个直觉该有的涨幅。这就是激活域的价值在参数维度上的又一次体现。

为什么成本没有跟着层数翻倍?这要回到第 7 章那条"只养会再露的人"的规则。层数开得多,不等于每个像素就真把那么多层都养活了;只有那些"上一帧亮过、这帧又要照顾"的窄区,才会真正开出深抽屉。Carousel 里重叠几何最密的那几块局部,层数开得再高也有抽屉可住,而大半个画面依然是浅层区,成本自然只跟着"真开了深抽屉的地方"涨,不是跟着"理论上的最大层数"涨。于是这里吐出一条很有用的工程经验:调层数要调的,不是"全屏开几层",而是"我需要的最深的那几块局部开到几层才不吵",两者之间隔着激活域这道闸,账算得完全不一样。

再拧补洞半径。前面第 7 章提过补洞是为了对付喷管在屏幕上留下的空档。

图 6
图 6 效果对照图(E 类 · 证据) · 原文 Fig. 6怎么读 ①指认:Without Filling / Radius 1 / Radius 3 三行对比,配 Reference。②读法:看补洞半径从无→1 时噪声的下降,从 1→3 时改善已难分;结论是 3 像素是收益刚刚好的档位。来源:Multi-Layer Reservoir Splatting, Fig. 6, p.7 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 6 是补洞半径的效果:Without Filling、Radius 1、Radius 3 三行,配 Reference。看中间那行 Radius 1,已经能把大部分空档堵上,噪声明显比 Without Filling 干净;而 Radius 3 相比 Radius 1 的改善,肉眼已经难分高下。作者的结论是,补洞半径继续加大,在典型的动画和镜头运动下,收益很快就消失了。所以 3 像素是个刚刚好的档位,够用,不瞎烧。

最后看 SPMIS 这条不相为谋的路。SPMIS(Stochastic Pairwise MIS)跟本文是两种完全不同的补法:它不着眼于时域,而是尽量在空间上"挑有用的邻居借",不随机抓邻居,专挑那些手里有好样本的邻居来用。它擅长的地方是空间复用效果好、邻近像素手里确实有料的时候。可一旦邻近像素也都没料,比如光照复杂、空间变化大,它就使不上劲,而且在有亚像素几何(比如树叶)的地方,它不能保证时域稳定。

本文刚好相反,它在"把带着历史的好的时域样本存进来"这条道上发力,但也明确承认,它对"上一帧根本不存在的新出现的几何"(比如屏幕边上突然闯进来的东西)产生的噪声无能为力。于是两个短板互补:把本文和 SPMIS 拼在一起,时域也管、空间也管,就能吃到两头的好处。

图 10
图 10 效果对照图(E 类 · 证据) · 原文 Fig. 10怎么读 ①指认:Bistro;左到右 Splatting / SPMIS(3 neighbors) / Ours(2 layers) / Ours+SPMIS / Reference,附耗时。②读法:SPMIS 顾屏幕边界的 disocclusion、Ours 顾深处重度的,二者合璧那一列画面最干净——组合优于任一单方案。来源:Multi-Layer Reservoir Splatting, Fig. 10, p.10 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 10 就是这场合作在 Bistro 场景的验证:Splatting(10.0ms)、SPMIS(3 neighbors)(12.8ms)、Ours(2 layers)(11.0ms)、Ours + SPMIS(13.8ms)。Splatting 明显最吵;SPMIS 独自上阵,能改善一部分屏幕边界附近的 disocclusion,但它顾不到深处那种刁钻的;Ours 独自上阵,把刁钻的重度 disocclusion 压下去了;而 Ours + SPMIS 那行,两边的优势叠在一起,画面最干净,代价是多了 13.8ms 里的一点。这一张图把"组合优于任一单方案"这一条钉死了。它也反过来提醒我们:本文不打算一手包办,只补时域这一头从没人补好的钉子,剩下交给擅长空间的搭档。


1111 和去噪器同台:NRD 与它那个过度模糊的 DF

到目前为止讨论的都是"原始渲染输出"有多干净。但真实产品里,几乎没有人直接把 ReSTIR 的原始输出端上屏,它要先过一层实时去噪器才能看。去噪器本身就是吃时域历史的家伙,那么本文给去噪器喂一个"更稳的输入",能带来什么,就是这一章要验证的。论文用的去噪器是 NVIDIA 的 NRD(NVIDIA Real-Time Denoisers)。

先把"为什么去噪器尤其怕 disocclusion"这件事讲透,不然这章的验证你会看不出门道。实时去噪器的工作方式,本质上是"拿历史帧的干净信息来给当前帧查漏补缺":它把当前帧每个像素的噪声值,和上一帧对应像素的已去噪值做加权混合,噪声就被时间平均摊薄了。这套逻辑隐含的假设,和前面所有时域复用一样,是"上一帧这个位置确实有可用的干净历史"。可一旦发生 disocclusion,这个位置的历史要么是空的,要么属于挡在它前面的另一块表面,去噪器一抓,抓到的是一把用不得的旧账。于是去噪器要么照单全收,把错误的旧颜色糊过来,要么干脆放弃时域、退回纯空间滤波,而纯空间滤波在这种"整片区域都刚露头"的地方,没有足够的高质量邻域可借,照样压不下去。disocclusion 之于时域去噪器,就像是给一个靠背书的代办逐帧搬搬账的人,突然塞进一摞没留下任何账本的空白页。

图 7
图 7 效果对照图(E 类 · 证据) · 原文 Fig. 7怎么读 ①指认:NRD 去噪输入对比,下为 Splatting(denoised)、上为 Ours(denoised),场景含一个移动的白光灯泡制造大 disocclusion。②读法:看 disocclusion 区去噪后的色块(color blobs)与细节保留——Ours 因输入历史方差更低,去噪器能保细节贴近参考而不引入空间伪影。来源:Multi-Layer Reservoir Splatting, Fig. 7, p.7 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 7 就是这场试验。场景里有一个移动的白色灯泡光源,它离相机很近,移动时在身后制造出一大块 disocclusion 区域,正好是去噪器最怕的场景。图里上下两组,下面是 Splatting(denoised),上面是 Ours(denoised)。看细节,Splatting 那一组由于输入的时域历史在 disocclusion 区质量差、方差高,去噪器容易被带着糊出"色块"(color blobs);而 Ours 因为从被挡的深层仓库里调出了更高质量的历史样本,喂给 NRD 的是一个方差更低的输入,NRD 就能在保细节和贴参考之间取得更好的平衡,没引入额外空间伪影。一句概括:本文不抢去噪器的活,只把去噪器的输入质量垫高,让它在最难的 disocclusion 区也能干好本职。

可去噪器还有一个更深的坑要趟。NRD 自带一个专门对付 disocclusion 的选项,作者叫它 disocclusion fix(DF)。它的思路是:检测到某块区域缺乏可靠的时域去噪历史,就转向激进的时空滤波硬抹。这在理论上好像是救火,实际执行起来却会矫枉过正。

图 8
图 8 效果对照图(E 类 · 证据) · 原文 Fig. 8怎么读 ①指认:四格并排:Ours(denoised with DF)/ Splatting(denoised with DF)/ Ours(denoised, 无 DF)/ Reference。②读法:对比开没开 DF 的同款输入——DF 能压掉萤火虫,但把重度 disocclusion 区抹成平糊一团、细节全无;真正的解药是抬高输入历史质量而非硬抹。来源:Multi-Layer Reservoir Splatting, Fig. 8, p.8 · © 2025 作者(CC BY 4.0)· 原样引用,未修改

图 8 把这个问题拍了个正着。四格分别是 Ours(denoised with DF)、Splatting(denoised with DF)、Ours(denoised, 无 DF 对照)、Reference。看第一格,即使输入是质量很好的 Ours,一旦 CPU 端把 DF 打开,重度 disocclusion 的区域还是被 DF 狠狠地抹成了平糊糊的一团,细节全没了。它确实能把 baseline 的萤火虫(fireflies)压下去,但代价是把本文的好结果也一并抹糊。所以这篇论文用这张图亮出了一个观点:真正重度的 disocclusion,是时域去噪的一个根本性短板,DF 这种"硬抹"不是解药,而本文通过给输入垫更高的历史质量,比 DF 提供了一个更本质的解法。

最后要补一句量纲上的提醒,它与第 9 章那句一脉相承:这些去噪、对比的量化指标,全部是对整幅画面平均的。disoccluded 区域占画面比例小,所以任何"局部噪声大降"反映到全局指标上都只剩一小笔。读图判断时,一定要去看图上放大框和局部细节,而不是只盯总数。


1212 边界与反例:这套方法在哪些屏幕前失灵

一套方法写到"看起来万能",反而要警惕。这一章把论文自己承认的、以及从机理上推导出来的失效边界,老老实实摊开。每一条都带触发条件,触发不满则不成立。

第一条,视锥外进来的几何,不在这套方法的射程里。 本文完全在屏幕空间里工作,只存屏幕空间的信息。所以它对"从视野边缘闯进来的新几何"没有任何办法,那些地方没有上一帧可查,它照样会以噪声的形式露出来(图 9 屏幕边的噪声就是这么来的)。要去管它,得把样本维持到屏幕之外,那是本文明确不做的事。触发条件是:有几何从视锥外切入。此时不是方法退化,而是它压根不覆盖。

第二条,第一次露脸的新表面,开头注定是吵的。 选择性激活是按"上一帧亮过才可能再亮"来预测的。于是第一次出现在画面里的表面,由于没有历史,刚开始一定带着更高的噪声。它不是永远错,只是把"给第一帧的历史"这件无米之炊留给了后面的帧去补。触发条件是:有全新的、此前从未出现过的表面进入画面。

第三条,大面积"曾经亮过的样本同时被挡"时,成本会涨。 如果一堆本来亮着的样本同时被挡到后面去,因为它们都有可能重新露脸,系统不得不为它们每个人养一个域,域数会一下子多起来。论文坦诚,遇到这种情况,如果有个策略能避免每帧都去刷新深层 reservoir,成本能压下来不少。触发条件是:一次遮挡事件波及大量曾亮过的样本。

第四条,景深和运动模糊下,"是否遮挡"本身的判定要重建。 本文的层序、点定层,都假设相机是针孔、画面瞬时。一旦启用景深或运动模糊,一个样本到底被没被挡住,得在整个光圈开度、或者整个时间积分区间上去判断,这事立马变得不平凡,本文的公式直接失效。触发条件是:开启景深或运动模糊。

最后给一条从机理里推出的反例,呼应第 5 章那句"差值什么时候会长出来"。多层复用的成立前提,是被挡表面重见天日时,它所在的"路径空间邻居集合"基本没变。可如果那个遮挡物本身动了,恰好挡住了射向其背后表面的光源,那么背后表面的光照条件就真的变了,此时再信任"很久以前的深层历史"就是不合理的。这条反例的触发条件写得非常具体:遮挡物的运动,挡住了本应照到重露表面上的光。这是时域复用里"历史可信度"的天花板,任何想靠纯时域解决 disocclusion 的方法,都绕不开它。

把上面这几条放回到真实场景里走一遍,你会发现它们不是孤立的,而是经常结伴出现、互相放大。当嵌套遮挡同时发生,深层历史会连锁过期。 设想一个角色贴着整面墙移动,角色身后的墙面先被角色挡掉,而更里面的货架又被墙面挡掉,形成两层遮挡。当角色移开时,墙面第 2 层极快地露回第 1 层,此刻它还能拿到一点上一帧的口碑;可墙面身后那层货架,因为经历了两个遮挡物的叠加,它"被挡了更久、更久没在主层亮过",等它最后露到第 1 层时,那份深层历史早已观测不到最新的光照,信与不信都别扭,本文的定层与置信回接在这里只能退化为近似。这条边界说明:层数不是越深越保险,越深的层,历史越陈旧,可信度越差,方法能做的只是延缓腐坏,拦不住它一定到来。

另一处容易踩的雷是亚像素高频几何。 disocclusion 的定义依赖"像素光线上有几层表面"这个稳定的几何结构。可一旦面对远处一片树叶、栅栏这种每像素几十个镂空的结构,表面层数在帧与帧之间极不稳定地跳变,深度区间划分和"该进第几层"的问法本身就站不住。这不是论文会假装解决的,它只会提醒:在纯细分高频几何密集的区域,时域这一头的所有招(包括本文)都得退避三舍,老老实实交给空间派或者更高的采样预算。这两条连同上文的四条,才构成一个完整的失效清单:不看它,你会以为多层抽屉无所不能;看全它,你才明白它省下的每一分钱,都是用"只在历史能可信的那一小撮地方开抽屉"换来的。


1313 收口:三件贡献、两条去路

把整篇的骨架再梳理一遍,答案就立在眼前。这篇论文的核心,是把"时域复用断在 disocclusion"这个老伤口,用一个"多层接住被挡样本"的思路给缝上了,并为它配了两件省钱的工程妙手,让它在实时预算里真的跑得起来。

它给这个领域交付的东西,原作者总结成三件,我用自己的话重述一遍,好让读者记住的是"为什么"而不是"编号"。

第一件,把 Reservoir Splatting 从单层抬到多层,让被遮挡的样本可以在不同屏幕空间层之间喷管移位,于是好的深层历史样本,能在重新露脸的那个瞬间回到画面里,而不是被丢弃。这是全篇最本质的那口气。

第二件,找到一个"两层可见性判定就能定层"的办法。通过深度范围重定义积分域,那些原本要沿着射线逐段追光才能数出层数的昂贵动作,被压成最多两次可见性查询,这是把方案从"演示"推到"可实时"的成本地基。

第三件,用一个激活域掩膜,让多层仓库只在"未来很可能用到"的地方才养着,避免全屏幕层级仓库的 k 倍开销。这是让方案真正落进帧预算的最后一个闸门。

三件里,前一件决定"能不能",后两件决定"贵不贵"。

如果只允许你带走三句话,那么请带走这几条。第一句,时域复用的天花板不是样本够不够多,而是"被挡掉的历史该不该信",本文用"深抽屉存起来等它再露"给出了一个不靠猜的答案。第二句,任何看起来要多花 k 倍钱的方案,都要有一个像"深度范围只准两次判定"和"激活域只养老兵"这样的闸门,否则它永远只能活成论文,活不进帧预算。第三句,把方案塞进完整管线(接上 SPMIS、接上去噪器)之后依然成立,这才叫真的解决了问题;单拎出来能跑,不算数。

把这一篇放回实时渲染那座大山里,它的位置很清楚:ReSTIR 家族长期以来把"时域复用"做到极致,却始终绕不开 disocclusion 这个断点;本文没有推翻前面的家底,而是顺着喷管和多层积分的台阶,把断点接上了。它不打算替代空间派思路,而是把"时域这一头"补到连去噪器都受益在它脚下。如果你正要在自己的渲染器里处理运动物体身后的噪声,这一篇给你的是一套可以直接照搬的分诊思路:先问它是不是 disocclusion,再问要不要开多层,最后问这两笔账(深度范围的射线、激活域的存储)吃不吃得消你的帧预算。

未来工作,作者画了两条路。一条是把自己维护出来的这些"被遮挡层里的样本",进一步暴露给时域的上采样器和去噪器,让它们在处理 disocclusion 时也能借到这笔历史,而不是像第 11 章的 DF 那样只能硬抹。另一条是朝透明渲染走:跟踪透明表面身后的不透明样本,这样物体在透明介质后来回移动时,那里的信息也能被收回复用。这两条都在把"多层抽屉"这套资产的收益,从本文自己的输出,扩散到整条渲染管线的其它环节。

关于这套方法的出处与授权,一句话收尾:它是一篇署名清晰、以 CC BY 4.0 授权发表的会议论文,标题、作者、发表场次、DOI 与授权口径都在页面末尾的「参考与可验证性」里逐项列出,本篇正文的图与读数也都来自该论文,未作改动。


符号与术语单(附读后续读,术语一义到底)

符号 / 术语含义
domain (p, i)像素-层域,一个抽签罐的所在
层 / 抽屉屏幕空间第 i 层(i=1 为首个直接可见层)
rank(x)交点 x 沿相机射线的次序,=1 为首交点
Ai第 i 层域的交点集合
representative hit cp,i从像素中心投射、代表该域的命中
splatting / 喷管映射把上一帧样本喷进当前帧对应位置
深度范围 Di=[Li, Ri]预计算的非重叠深度区间,用于两层可见性定层
激活域只维护"可能再露脸"的域,其余不养
hole-filling对激活域周围补位,抵消喷管空档
SPMISStochastic Pairwise MIS,空间派兄弟方案
NRD / DFNVIDIA 实时去噪器,及其 disocclusion fix 选项
MAPE / FLIP平均绝对百分比误差 / 人眼感知差异度量

参考与可验证性

原始论文
Pengpei Hong, Song Zhang, Daqi Lin, Markus Kettunen, Chris Wyman, Cem Yuksel, Multi-Layer Reservoir Splatting for Temporal Reuse under Disocclusion, ACM SIGGRAPH Conference Papers 2026, Los Angeles, USA, 10 pages.
DOI
10.1145/3799902.3811232
授权
Creative Commons Attribution 4.0 (CC BY 4.0)