RealtimeGI 实战篇(下)|ReSTIR 时空重采样降噪管线
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
承上启下
在上一篇文章中,我们从0到1实现了一套完整的软件光追管线。我们使用稀疏的体素来存储场景的Material信息和Radiance,体素的颜色并不直接运用到屏幕像素上,因为体素的精度仅能表达粗粒度的Lighting信息。要想获得像素级别的光照频率,我们必须从屏幕像素开始发射光线进行Final Gather。

即使距离场追踪已经足够快速了,为了满足实时这一命题,我们的预算仍然是1/4SPP(半分辨率1SPP),巧妇难为无米炊,在如此紧巴的光线预算下,基础的蒙特卡罗积分会产生巨大的BIAS,因此本篇文章的重心将放在如何从低SPP的图像重建出清晰干净的信号。

一、初始样本生成
在开始编写降噪器之前,我们要为降噪器提供初始样本作为输入信号,我们执行最普通的蒙特卡洛采样即可。为了获得像素级别的细节,同时避免了体素追踪的自遮挡现象,我们需要先从屏幕空间利用Depth Buffer进行追踪。对于屏幕追踪失败的像素我们才回退到距离场追踪。

1.1 Mini G-Buffer
我们的光线追踪发生在1/4分辨率下1SPP,和SSAO类似,在开始追踪之前我们也要先对G-Buffer进行降采样。我们沿用SSAO的代码,根据深度加权计算法线的平均值到RGB通道,同时将深度一起存储到A通道。在后续的Pass中我们一次采样就可以获得采样点的法线和世界坐标。

1.2 屏幕空间追踪
在UE引擎中已经实现了基于HZB的射线追踪,并且网上也有很多相应的教程,这里笔者就不重复了。对于屏幕追踪,我们将命中点重投影回上一帧,从上一帧的Scene Color中获取Hit Radiance。

我们重点关注如何组合屏幕追踪与距离场追踪。我们当然可以在屏幕追踪着色器内直接进行距离场追踪,但是每个Wavefront内各个Pixel屏幕追踪的命中情况各不相同,这会产生巨大的Divergence。和上一篇文章中的Voxel Lighting部分类似,对于屏幕追踪失败的像素,我们单独将它们挑选出来排列到紧凑的Buffer中单独进行距离场追踪。

我们将屏幕分为8x8的Tile并配套一个线程组,每个Tile内的每个像素分配一个线程进行屏幕追踪。每个线程将屏幕追踪的成功与否写入到Shared Memory,然后由第一个线程统计并负责在紧凑Buffer中开辟空间,最后各个线程将追踪失败的像素写入Buffer。

1.3 距离场追踪
距离场追踪是一个Indirect Dispatch,我们根据屏幕追踪中确认的追踪失败的像素数目来决定派遣多少组线程组。对每个像素我们从紧凑Buffer中读取其PixelCoord,然后从Mini G-Buffer中重建其世界坐标以及射线方向,根据World Normal等信息决定一个BIAS将光线的起始点偏移,最后开始距离场追踪。

有了距离场的追踪作为兜底,我们能有效地弥补Screen Trace几何信息不足的问题。不过此时的Hit Radiance还不足以直接拿来使用,这就轮到降噪器出场了。

二、ReSTIR理论知识
ReSTIR是由NVIDIA提出的先验的降噪算法,核心思路是为每个像素尽可能地保留“最优秀”的样本来降低图像的整体方差,ReSTIR先随机采样得到一堆“普通样本”,在普通样本中选出1个“最优样本”作为降噪器的输出样本。备选的普通样本数量越多,选出的最优样本就越好。

ReSTIR将每个“普通样本”的亮度作为其被选中的概率,通过重采样重要性采样(RIS)抽样方法,在普通样本中抽出“最优样本”,并无偏地估计其对蒙特卡洛抽样的贡献。本质上是从普通样本的分布(Source PDF)中,生成了符合周围环境光照的最优样本的分布(Target PDF)。

直观地理解,按照这个策略我们抽到的最优样本都是很亮的,更符合样本点周围环境光照(Target PDF)分布规律的,相当于在对光照的分布进行重要性采样。每个像素会随着光照环境的变化而改变采样策略,这减少了图像的BIAS。

选出N个普通样本意味着N次Trace,没有办法每帧进行,ReSTIR退而求其次将普通样本的采样分摊到时域进行。通过蓄水池抽样(WRS)算法渐进式地从历史样本数据流里选择最佳样本,这可以保证每像素每帧只产生一个新样本(光追一次)的预算,以匹配Realtime这个命题。

除了历史帧的样本,同一帧内周围邻居像素产生的样本也可以进行复用,本质上是重建了一条当前像素到邻居像素光线命中点的光路。有了时间和空间上的复用,我们只用每像素一条光线的代价就获得了成百上千光线的效果。

三、ReSTIR工程实践
3.1 整体流程
在笔者的实现中,蓄水池完全沿用了NVIDIA论文的实现。其中光线生成点、命中点的法线都经过了八面体映射编码,这样我们通过2张RGBA16的贴图和两张RGBA32的贴图就能存储全部的数据。

来看一下整体的管线流程。在第1小结中我们通过混合光线追踪生成了初始样本,我们会尝试用这个初始样本去更新Temporal Reservoir,再将其作为当前帧Spatial Reuse的输入,以及下一帧的Temporal Reuse的输入,这样确保Temporal Reservoir的样本都来自同一个像素具有相同的Domain。最后我们从Spatial Reservoir中根据RIS estimator的公式计算蒙特卡洛积分的结果,再加上时间空间的Filter进行最后的降噪。

可以看到整个流程还是比较简单和清晰的,基本是按部就班地按照NVIDIA论文走下来。不过其中也有一些值得注意的细节,让我们接着往下走。
3.2 时间重采样
时间重采样的第一步是要将当前像素重投影回上一帧的屏幕空间。而重投影总是存在一些计算精度误差,直接用上一帧的UV是无法准确地找到上一帧相同位置的像素。如果贸然使用重投影的UV就会产生如下的Artifact,在相机移动时尤其明显。

因此这里我们需在重投影之后多加一步像素搜寻,我们在3x3的邻居内查看每个Reservoir的光线起始位置,找到“光线起点世界坐标”与“当前像素世界坐标”差距最小的历史帧像素,以此精确地找到重投影后原汁原味的历史帧像素。

精确地找到历史帧像素之后,我们从中取出Temporal Reservoir并评估Reservoir与当前帧像素的几何相似度,以此来决定历史的有效与否。这将在相机移动时帮助我们快速更新掉已经失效的历史值,减少了鬼影与延迟。最后我们将Initial Sample产生的新样本融入进Reservoir中,并更新其RIS权重。

有了时间重采样,整个采样器的效率得到了极大地提升,但是还不足以作为最终的图像输出。采,就多敛!时间重采样的20个样本作为启动资金,我们已经准备好在空间维度上更进一步,牵扯样本数目更多的重采样。

3.3 空间重采样
空间重采样的思路和时间重采样类似,也是将相邻像素的样本合并进当前像素的Reservoir,因为我们近邻搜索的对象是Temporal Reservoir,而每个Temporal Reservoir都是满载20个样本的,因此每次合并相当于浏览了20个样本,效率非常之高。

强烈安利一下这个视频,讲解的十分清晰易懂:
https://www.youtube.com/watch?v=gsZiJeaMO48
我们仍然从搜索周围的像素开始。首先随机采样UV Offset拿到周围像素的Reservoir,根据当前像素的位置和法线计算两个像素之间的几何相似度,以此决定是否拒绝邻居Reservoir的合并。空间上的复用相当于重建了当前像素到邻居Reservoir样本点的光路,因此我们还需要小心地检查光路与采样点的半球可见性,以此屏蔽一些负值极值的情况,因为那种角度不可能存在对着色点有贡献的光路。

接着我们根据复用路径的几何信息计算一个Jacobian行列式,用来缩放因光路的变换而导致的立体角微元dω的(原论文称为Measurement Density)变换。笔者这里的理解是,路径的复用相当于把邻居像素x'的光照函数L(x')进行了换元,换成了用本地像素x作为自变量输入的版本L'(x),而Jacobian行列式正是用来衡量两个坐标系之间进行转换(换元)所带来的积分微元的面积变化。

一个直观的理解是,如下图所示绿色区块为2D屏幕上的积分微元dxdy大小,它的面积在坐标系发生线性变换之后相应地被拉伸了,而这个线性变换所对应的Jacobian行列式就描述了其面积的改变。

强烈安利一下这个视频,讲解的十分清晰易懂:
https://www.bilibili.com/video/BV1zq4y1o7hR/
我们复用邻居像素的Lighting信息计算渲染方程的积分,自然而然地会需要计算∫ L(x) dω,而像素之间的dω并不相通,所以需要Jacobian行列式来进行缩放。
有了Jacobian行列式,我们将邻居样本的Target PDF进行缩放,然后根据邻居Reservoir的样本数目计算一个合并权重,这相当于重复Reuse了N次近邻样本,最后合并Reservoir更新蓄水池样本数目以及RIS estimator权重。这里要注意限制Jacobian的大小以避免出现Firefly现象。

因为Temporal Reservoir已经看过了20个样本,假定我们再进行8次空间重采样,此时每个Reservoir都相当于看过了20x8=160个样本,采样质量有了极大的改善。

四、时空降噪器
在时间和空间维度上对光线样本进行重采样已经得到了令人满意的结果,但是其输出并不能直接用于最终图像,我们仍然需要常规的降噪器来磨除噪点和抖动。这里笔者直接做了一个最基础的时空滤波,该部分的代码非常简单就不过多展示。
首先出场的是Temporal Filter,它和TAA的计算流程非常类似,采样当前帧像素3x3范围颜色,在YCoCg空间下计算颜色包围盒对历史帧像素的颜色进行钳制,校验像素之间的Geometry Similar决定是否进行历史混合。

紧接着是Spatial Filter,我们使用3x3的A-Trous滤波器对图像进行过滤,每一轮次Spatial Filter输入为上一轮次的Spatial Filter的输出。我们使用逐步倍增的Filter半径以覆盖更大的区域,对每个采样像素我们使用深度和法线的相似度来作为Edge Stopping Function。

在笔者的实现中通过5次3x3的A-Trous近似了18x18的高斯滤波盒,此时的结果已经足够平滑作为最终的输出了。

五、Contact Details
在第4小节中我们用了力大砖飞的Spatial Filter来压制图像的噪声,尽管我们设置了Edge Stopping Function,但由于巨大的Filter半径,在近距离或者Geometry比较高频的地方,我们丢失了非常多的细节,包括光照的方向性、间接光照投射的间接阴影等细节全部抹平了。
在本小节中,我们将利用各种手段尝试恢复一些Contact Details。即使有的方法不是基于物理正确的,但是对于提升最终的图像质量仍然有帮助。
5.1 空间滤波引导
只有深度、法线作为Edge Stopping Function,我们的Spatial Filter也没有办法分辨这些信号是单纯的噪声还是由于几何的变化引起的自然现象,因此我们需要引入额外的信息作为Guidance。这里笔者参考了Kajiya渲染器的思路,在Spatial Filter的采样权重中额外考虑中心像素与Sample像素的SSAO的数值差异。

这里SSAO的实现笔者做了一个HBAO上去。在Temporal Filter之前渲染AO,将AO值存储到Diffuse Irradiance贴图的A通道,然后用Temporal Filter进行降噪。有了Filter Guidance,我们的Spatial Filter的结果会更加锐利,GI的方向性也越强。

5.2 空间蓄水池校验
细心的读者应该注意到了,在第3小节我们实现的Spatial Resampling流程中,笔者在复用周围像素时忽略了像素彼此之间的可见性,因此Reuse的结果并非无偏。表现在画面上就是Indirect的遮蔽结果产生了漏光。

漏光的原因在腾讯HSGI中也有提到,周围邻居像素的Hit Point,对于当前像素不一定是可见的。如果贸然连接复用(相当于建立和邻居像素Sample的光路)就会丢失Occlusion信息。

和HSGI一样,我们在结束了Spatial Reuse之后,对Spatial Reservoir中仍然幸存的那一个天选之子Sample进行可见性测试。出于性能考虑,我们并不发射完整的Hybrid Ray,只在屏幕空间利用半分辨率的Depth Buffer进行简单的线性Ray March。
对于通过了Visibility Test的邻居Reservoir我们直接用RIS公式Resolve出Irradiance,否则我们回退到使用Temporal Reservoir的样本计算Irradiance。我们根据SSR Ray March命中点的距离,和邻居Sample命中点的距离计算该样本的置信度,这和DDGI中通过Depth计算Probe遮挡异曲同工。

因为在第3小节我们做了严格的重投影,因此Temporal Reservoir能够严格保证产出自同一个像素,所以得到的结果是无偏差的,这极大地缓解了间接遮挡丢失的现象。

5.3 间接阴影
虽然Spatial Reservoir Validation能够重建正确的间接遮挡,但是我们还有Spatial Filter不加任何验证地平滑一切颜色。对于一些比较细的物体比如椅子腿投下的细长的Indirect Shadow仍然会被不正确的Filter掉。

幸运的是在5.2小节我们进行了Reservoir Validation,而Spatial Reservoir样本中存储的方向代表了当前像素接受光照最强的方向。我们将5.2小节中计算出的Reservoir置信度(针对近邻像素Sample的可见性测试)直接输出并认为是Indirect Shadow的信息,它和Color一起在Temporal和Spatial Filter中接受降噪,在最终合成阶段运用在Irradiance上。

虽然不是那么物理正确,但是其带来的视觉效果却更加能凸显场景的光照变化关系。并且我们只是通过了开销极低的屏幕空间步进实现的,实践表明这是个性价比不错的路子。

六、Specular Indirect
Specular能够大大提升场景的真实感和质感,而除了RTX外主流的Specular计算方法或多或少都不那么完美。比如SSR缺少屏幕外的信息,IBL和反射球则依赖烘焙或是只支持Static的场景。对于一个完整的GI方案来说,Specular是不可不品鉴的一个环节。

6.1 总体流程
在笔者的实现中参考了寒霜引擎的方案,不得不感叹老牌劲旅确实强,差不多十年前的方案到现在都很奏效。因为笔者省略了根据粗燥度做屏幕分块Trace的部分,但是整个流程基本与原PPT一致。大致分为Initial Sample,Ray Resolve,Temporal和Spatial Filter这四个步骤。

6.2 初始样本生成
话不多说,我们仍然从本文实现的软件光追起手(即屏幕空间+距离场的光线追踪),和Diffuse不同的是我们的射线是按照GGX分布进行重要性采样。此外笔者根据寒霜PPT的分享也对BRDF进行了截断,保证在高粗糙度下大部分的光线能收束聚集到镜面反射方向周围以减少噪声,在Screen Trace命中时我们根据命中距离和Roughness从上一帧的Scene Color的Mip中获取颜色,以弥补BRDF截断带来的图像过于清晰。

6.3 Ray Resolve
紧接着我们进行Ray Resolve,从半分辨率的Initial Sample Texture生成全分辨率的Specular Indirect Texture,核心思路是全分辨率下每个像素随机地选取周围UV,根据Jittered UV从半分辨率Texture获取若干个Ray Sample,将全分辨率像素和Ray的命中点进行重新连接,根据光路的信息估算Irradiance。

对于每个全分辨率下的像素,我们通常选取4个Rray Sample进行连接,假装我们在进行4SPP的采样并且产生了4个Sample。这样我们不仅能够获得全分辨率逐像素的Roughness、Normal细节,而且全分辨率每像素4SPP相当于一条Ray我们掰成了16条来用。

值得注意的是,对于Irradiance的估计我们使用的是一个奇怪的加权平均。这在寒霜的分享中也有提到,是一种叫做Ratio Estimator的抽样方式,它的核心思路是将渲染方程中对Lighting的估计和对BRDF的估计拆分开来,对Lighting仍然使用蒙特卡洛随机抽样进行估计,对BRDF则使用有解析解(LUT)进行速查表。

BRDF我们可以通过Image Based Lighting(IBL)中的预积分BRDF图来近似计算,将其FG项单独提出来。因为我们拆散了渲染方程,为了保持结果的准确我们要在分母上同时除以一个FG项。这个FG项其实就是预积分的红绿图,在最终合成阶段我们再乘回去。

理解了数学上的原理,上文代码中权重计算的代码就呼之欲出了。其实就是一个BRDF的计算,我们根据全分辨率像素的位置作为光线起点,半分辨率的Ray Sample作为光线终点,连接一条光路并用逐像素的Normal和Roughness评估BRDF,最后加权平均。

经过复用后的图像已经初具雏形,但是仍然存在一些噪声。在此基础上我们也需要进行Temporal和Spatial的Filter来稳定最终的图像。

6.4 时空降噪
我们首先进行Temporal Filter,值得注意的是重投影时我们不再根据光线起始表面的坐标进行重投影,因为Filter的信号是倒影中的虚像,我们要针对虚像的位置进行重投影。

如果还是使用光线起始表面做重投影,我们会得到非常奇怪的历史混合。

如果使用虚像位置做重投影,可以看到虚像的拖影指示了正确的Motion。这个拖影我们在Temporal Filter中加上和TAA类似的近邻颜色包围盒对历史颜色进行钳制就能解决,这里没有开启是为了直观的体现重投影的运动方向。

事实上我们同时采用了两种不同的重投影策略。对于光滑的表面我们使用虚像位置进行重投影,对于粗糙的表面我们和Diffuse信号类似仍然使用光线起始点进行重投影,最后根据粗糙度来决定两种重投影混合的比例。

有了Temporal Filter能极大地压制噪点,现在的结果已经基本可用了。

最后我们再进行Spatial Filter进一步消除噪点和Firefly,这里的Spatial Filter和Diffuse有点不同。我们根据粗糙度来决定filter的范围,然后在范围内随机选取像素,用深度法线和UV距离衰减确定双边滤波的权重,然后累加。

代码非常简单,唯一值得注意的是我们在计算相邻像素颜色时要进行ToneMapping,在累加结果之后再反向ToneMapping回去。因为Specular波瓣形状会产生非常多的尖峰,经过ToneMapping能极大地降低结果的闪烁。我们在上文Ray Reuse Pass加权平均颜色时也用到了这个技巧。

现在的Specular结果已经准备好使用了。

别忘了我们在Ray Reuse Pass中把预积分的BRDF单独提取出来了,我们在最终合成Pass需要把它加回去,这样才是完整的渲染方程。

七、性能
我们仍然用EPIC商店的免费场景Modular Asian Medieval City为例,在半开放的室外以保证射向天空的Ray行进了足够的距离。

笔者的电脑为3060 Laptop(挣韭者),渲染分辨率为1712x1024,光线追踪分辨率为856x512,此外为了开发方便控制台变量默认开启r.Shaders.Optimize=0,没有试过Cook后的情况。
对于Diffuse流程,性能的大头主要是三块:初始样本生成时的混合光线追踪,ReSTIR 时空重采样,以及最后的空间降噪。

对于Specular流程,主要是在初始样本生成、Ray Reuse和全分辨率的时空降噪。

八、结语
多么吉列的Coding!我们终于完成了Diffuse & Specular的Gather和降噪管线!总结起来就是我们实现了一套,性能不如Lumen、效果细节不如Lumen、响应速度不如Lumen、可伸缩性不如Lumen、降噪稳定性不如Lumen、漏光控制不如Lumen、各种Shading Model、Lighting Feature的支持鲁棒性不如Lumen,总之就是不如Lumen的实时GI方案。
当笔者一路攀登以为达到了山顶,却发现Unreal大神早早就站在了山顶,并给出了完美的GI方案。这种压迫感与窒息感不禁令我想起英雄联盟s8 CG中,掌门攀登到了空无一人的山顶,却发现飞神早已恭候多时。诚然,民科笔者业余时间瞎折腾捣鼓破烂轮子,是不可能与EPIC的全职天才工程师们精雕细琢的工业明珠相提并论。正视差距,但不宜妄自菲薄,更重要的是我们一路走来从中收获学到了什么。

通过实践ReSTIR、时空降噪等算法,笔者也深刻理解了“绝知此事要躬行”的道理,真正Coding起来还是有非常多要注意的地方的。同为降噪方案,笔者认为仅对于Indirect Lighting来说,Lumen Screen Probe Gather + SS Bent Normal的策略要好于ReSTIR,相对地ReSTIR在纯光追模式下(类似离线渲染用NEE,直接+间接光拉一块a了)具有更好的表现。最后,令笔者感到意外的是,实际用下来UE引擎的图形编程体验也远非网上讨论的那般不堪,在玩熟玩溜了之后会发现这都不是事儿。
UE引擎之路道阻且长。但是踏上取经路,比抵达灵山更加重要。回头才发现最难的那一步,就是最初的第一步。路途艰辛却难掩喜悦,笔者将一路走来的所见所闻记录于此,希望与大伙进步共勉。
九、代码仓库
https://github.com/AKGWSB/UnrealEngine/tree/4.27-akgi
引擎部分的代码位:
Engine\Source\Runtime\Renderer\Private\RealtimeGI
着色器部分位于:
Engine\Shaders\Private\RealtimeGI
十、参考与引用
HSGI: Cross-Platform Hierarchical Surfel Global Illumination
ReSTIR GI: Path Resampling for Real-Time Path Tracing
【论文翻译】ReSTIR GI: 实时路径追踪中的路径重采样
Stochastic Screen-Space Reflections
这是侑虎科技第1754篇文章,感谢作者AKG4e3供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/long-ruo-li-21
再次感谢AKG4e3的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)