七 实时光线追踪 Real-Time Ray-Tracing¶
本课程为闫令琪老师的 GAMES202-高质量实时渲染
此为个人的课堂笔记,如有疏漏,请多指正
1 RTX 硬件性能的突破与限制¶
实时的光线追踪的理论也基于离线渲染的光线追踪,并没有增加新的算法理论
实时光线追踪领域的突破是2018年 NVIDIA 正式发布了图灵架构的 RTX 硬件光追显卡,硬件性能的突破使在实时渲染中真正应用光线追踪成为可能
不过,即使是得以突破的硬件性能,在每秒能追踪100亿根光线的情况下,除以了分辨率和帧数后,再去掉其他后处理和Shader的消耗,最终在实时应用情况下也只能做到1 SSP (sample per pixel),即每个像素点一个采样数
RTX 的实时光追实际上也只做了至多弹射两次的光线路径追踪,因此对于1个 SSP 来说,它需要追踪的路径有:
- primary hitpoint:从camera出发打出一根光线打到的交点。这一步在光栅化时即可得到
- primary shadow ray:和光源之间连接进行 light sampling 并判断是否有遮挡
- secondary ray:在 hitpoint 根据材质采样出一个方向打出一根光线,它会打到一个物体上从而得到 secondary hitpoint
- secondary shadow ray:从 secondary hitpoint 与光源连接判断是否会被光源看到

综上所述,RTX 硬件能够实现的光追是十分简陋的,无论是采样数量还是采样路径长度都远远不够(低到令人发指),这种程度的光追必然会产生非常明显的噪点,如下图所示:

因此,实时光线追踪 RTRT 最关键的技术便是降噪算法,主要包括时间域 Temporal 和空间域 Spatial 降噪算法
2 时间域 Temporal 降噪算法¶
降噪的方法一般是增大采样率,越大的采样率越能还原出原始的信号,降噪的质量越高,但是对应的性能开销也会增大。因此寻找一个质量和时间都在可接受范围内的降噪算法便是实现实时光线追踪的主要目标
绝大多数降噪算法,包括各种 Sheared 方法、离线渲染方法、深度学习方法在实时光追这里都不适用
时间域 Temporal 降噪算法是工业界目前使用的一种降噪算法,其本质上也是增加了采样数,但是是在渲染帧之间的时间轴上增加的,而非暴力在空间中增大采样数
1)时间域 Temporal 降噪算法的核心思想¶
- 前一帧已经被降噪好了,并且可以被复用
- 通过运动矢量 (motion vector) 找到当前帧的像素点对应到上一帧所在的位置
- 通过时间轴的累积来隐式地递归增大采样 SPP 数

2)后向投影 Back Projection¶
寻找第 i 帧的像素 x 在第 i-1 帧的世界空间位置的过程称为后向投影 Back Projection
后向投影的主要步骤如下:
- 获取像素 \(x\) 的世界空间坐标点 \(s\),如果有 G-buffer,那么可以直接根据像素坐标 \(x\) 获取其世界坐标 \(s\);而如果没有 G-buffer,则可以通过矩阵逆向变换得到 \(s\),即有 \(s=V^{-1}P^{-1}E^{-1}x\),\(E\) 是视口变换矩阵,\(x\) 包含深度值 \(z\)(课程中还多加了一个模型变换 \(M\) 的逆变换,但个人感觉好像没必要)
- 设前一帧 \(s\) 点在 \(s^{prev}\) 处,若物体没有在世界空间移动,那么 motion vector 就是零向量;而若物体发生了移动,设变换矩阵为 \(T\)(即 \(s=Ts^{prev}\)),则有 \(s^{prev}=T^{-1}s\)
- 最后把前一帧的 \(s^{prev}\) 通过前一帧的 MVP 矩阵投影回屏幕空间,得到 \(x^{prev}=P^{prev}V^{prev}s^{prev}\),从而得到像素 \(x\) 对应前一帧所在的屏幕位置 \(x^{prev}\)(也同样把课程里的一个模型变换 \(M^{prev}\) 去掉了)
注:Geometry buffer(几何缓冲区)简称 G-buffer,它缓存了在渲染过程中的屏幕空间的一些额外几何信息,包括但不限于世界坐标、直接光照、法线、反照率等
3)时间域 Temporal 的滤波¶
通过上述的后向投影找到前一帧的采样值 \(C^{(i-1)}\) 后,以一定的权重 \(\alpha\) 与当前帧的采样值 \(C^{(i)}\) 进行线性混合,从而得到时间域滤波的结果
假设滤波后的结果为 \(\overline C\),未滤波的结果为 \(\tilde C\)。在时间域滤波之前,通常会对其进行空间域的滤波(在后一小节会讲解):
然后进行时间域的滤波得到结果:
滤波混合权重通常取 0.1~0.2,即 80% 到 90% 的滤波值来源于前一帧的结果。这种基于时间域的降噪采样方法计算量小、性能高效,而且滤波的结果十分优秀:

需要注意的是,虽然看起来降噪之后的结果比没降噪之前要“亮”上许多,但是实际上降噪是保持能量守恒的,降噪前的图像看起来暗是因为那些特别亮的噪点超出了屏幕的亮度表示范围而被 Clamp 了。如果使用 HDR 显示器就可以看出有噪声的图的正确的亮度了
4)时间域 Temporal 降噪算法的缺陷¶
时间域 Temporal 降噪算法有两个关键缺陷,其分别会造成一系列的问题:
- 上一帧内容不存在时
- 像素点没有 motion vector 时
(1)时间域 Temporal 降噪算法的关键缺陷之一¶
当后向投影超出屏幕空间之外时,也就是说当前帧的内容上一帧不存在时,该算法的处理就会出问题,主要表现在以下三种情况:
- 渲染场景(或者摄像机视角)的瞬间切换,即相邻两帧之间渲染了完全不同的内容(缺失了 burn-in period)
- 在一个狭长通道(例如走廊),镜头不断往后移动,画面不断涌现通道两边的新内容(“倒退走”是该算法最容易看出问题的情况,是由于 Screen Space 信息有限导致的)
- 背景原先被遮挡的地方突然出现(例如前景物体突然消失或光速飞走,即 disocclusion 的区域会存在问题,本质上还是 Screen Space 信息有限导致的)
后向投影超出屏幕之外直接不进行时间域滤波处理
而对于未超出屏幕之外的情况,如果不对上述的情况做进一步的处理,那么就会出现明显的 Artifact——拖影现象(Lagging)。这种拖影现象不仅会出现在场景中移动的物体上,而且也会出现在光照着色发生变化的地方(例如场景中的光源发生了变化,那么场景不同区域的阴影、光照反射均会发生变化,这些变化也会产生拖影现象)

对于上述“拖影现象”主要有两种解决方案:
- 一种解决拖影现象的方案是 Clamp,将上一帧的信息 Clamp 到这一帧上,使信息不会超出本帧实际情况太多,具体的算法不展开,在之后的空间域滤波中有类似情况可以参考
- 另一种解决拖影现象的方案是基于物体检测 Detection 的方法。对于场景中的物体,我们都给他一个 ID 值(这个 ID 信息可以通过光栅化映射到每个像素上),在后向投影时通过比较当前帧当前像素的 ID 与上一帧像素的 ID 是否一致来判断是否投影到了其他区域上。如果不一致,那么就可以对混合权重 \(\alpha\) 进行调整(相应地增大空间域滤波的比重),但这种方法会在相应的区域重新引入噪声(因为减少了没有噪声的上一帧信息的使用)

(2)时间域 Temporal 降噪算法的关键缺陷之二¶
对于某些相对比较静态的场景,如果某些像素点没有 motion vector 时(几何情况未发生变化),但是其上的阴影信息、间接光照信息等信息发生改变时(shading 情况发生剧烈变化),由于上一帧的内容的混合权重往往在 80%~90%,导致这些着色信息的变化速度明显过慢,以至于人眼可以轻易分辨
- 对于阴影信息,一旦光源高速变化,这个缺陷就会导致阴影发生明显错位,阴影的变化速度跟不上光源的变化速度
- 对于比较 Glossy 的表面上会存在的间接光照信息,一旦场景物体高速变化导致间接光照信息变化,这个缺陷就会导致着色点的间接光照结果发生明显滞后
3 空间域 Spatial 降噪算法¶
噪声在图像中通常表现为高频信号(或亮或暗),因此降噪算法本质上就是设计一个低通滤波器 (low-pass filter) 来去除这些高频的噪声。不过低通滤波也会带来一些问题:一是对于高频信息可能包含的有用的信息会丢失;二是对于低频信号中可能存在的噪声无法处理
对于接下来的内容,同样记噪声图像为 \(\tilde C\),空间域滤波核为 \(K\)(滤波核并非一定要固定不变,可针对每个像素使用不同的滤波核),滤波降噪后的图像为 \(\overline C\)
1)高斯滤波 Gaussian Filtering¶
空间域滤波本质就是卷积,即对每个像素的周围邻域的像素做一个加权平均的过程,伪代码如下所示。权重之和sum_of_weights
是为了最后的归一化,防止能量不守恒。也正因此,滤波核的各种参数并不需要十分规范,是要符合特定分布即可
For each pixel i
sum_of_weights = sum_of_weighted_values = 0.0
For each pixel j around i
Calculate the weight w_ij = G(|i - j|, sigma)
sum_of_weighted_values += w_ij * C^{input}[j]
sum_of_weights += w_ij
C^{output}[I] = sum_of_weighted_values / sum_of_weights
传统的高斯滤波器是一种常见的低通滤波器。虽然能够显著地去除高频噪声,但该滤波器带来的过度模糊的效应使得可能有用的高频信号丢失,表现为图像中的物体边界被平滑掉了:

2)双边滤波 Bilateral Filtering¶
为了保持图像中的物体边界不被模糊,可以使用双边滤波 (Bilateral Filtering)。双边滤波引入了对于“颜色项 color”的考虑,认为颜色变化特别剧烈的地方是边界:如果二者颜色差距不是特别大就继续用高斯处理;而如果像素 i 和像素 j 之间的颜色值差距过大,则认为这两个像素分别在边界的两边,从而让像素 j 给 i 的贡献变少,具体做法是在高斯的滤波核基础上增加一个项:
其中,\((i,j)\) 是滤波中心坐标,\((k,l)\) 是邻域像素坐标,\(I(i,j)\)是指 \((i,j)\) 处的像素取值。使用双边滤波后图像的边界得以保留:

3)联合双边滤波 Joint/Cross Bilateral Filtering¶
双边滤波是在高斯滤波的基础上增加了对于“颜色项 color”的考虑,基于此思想,将更多维度的信息加入滤波考虑过程中,就是联合双边滤波 (Joint/Cross Bilateral Filtering)。联合双边滤波利用了 G-buffer 中的世界坐标、深度、法线、反照度等信息作为参考加入滤波过程中,并且 G-buffer 是完全不会有任何噪声问题的,比上述的 color 更加可靠。例如下图中:AB点之间通过深度 depth 信息可以判断其之间的贡献很小;BC点之间通过法线 normal 信息可以判断其之间的贡献很小;DE点之间通过颜色 color 信息可以判断其之间的贡献很小

需要注意的是,高斯形状的滤波核分布并非是唯一的选择,类似的指数分布和余弦分布也是可选的分布函数:

4)工业界在滤波过程中的技巧¶
(1)二维滤波拆解为两个一维滤波 Separate Passes¶
滤波通常需要对周围邻域的纹素进行采样,采样的数量取决于滤波核的大小。纹理采样是一个非常耗时的操作,因此对于大滤波器,实时渲染领域通常会用一些技巧来降低滤波所需的纹理采样数量。一种技巧就是将二维的滤波拆分为两个一维的滤波过程,从而使得滤波采样的数量从 \(N^2\) 降低到 \(2N\)(其中 \(N\) 是滤波核大小)

以高斯滤波器为例,一个二维的高斯滤波器可以被拆分成水平方向和垂直方向的一维高斯滤波器:
上式成立的基础是下式,即高斯滤波器是可拆分的,左右完全等价:
但是对于双边滤波器和联合双边滤波器,理论上是不能等价拆分的,但是工业界往往会进行强行拆分,即使会存在一定的 Artifact,但是一定程度上是可接受的
(2)依次逐渐增大滤波器的滤波范围 Progressively Growing Sizes¶
当某些情况下需要用一个超大滤波核进行滤波的时候(比如 64 × 64),它的采样开销是特别大的。工业界会以固定大小的滤波器多次滤波、依次逐渐增大滤波器的滤波范围来实现这种超大滤波器的滤波过程。例如,假设我们固定滤波器大小为 5,而真正想要实现的滤波范围是 64×64,那么用 5×5 大小的滤波器滤波 5 次,每次滤波采样之间间隔为 \(2^i\)(其中 \(i\) 是迭代次数,从 0 开始):

利用这种方法,一个 \(64^2\) 复杂度的采样过程就被减小到了 \(5^2\times 5\) 的复杂度
上述这个方法的目的是为了保留低频的信息(避免因为过大的 Filter 导致低频信息丢失),具体数学原理涉及到了搬移频谱等问题,此处不做深入,详细内容可以参考 GAMES101-光栅化(深度测试与抗锯齿):

4 噪点去除 Outlier Removal¶
用蒙特卡洛方法渲染一张图时,得到的结果会出现一些噪声点过亮或者过暗,这些过亮或者过暗的点如果经过空间域降噪算法处理的话,会导致画面有亮斑或暗斑,所以必须在滤波之前处理掉,这些噪声点就称为 Outlier
Outlier 噪点不能通过滤波器去掉,必须要通过一些手段识别出这些噪点,然后再针对性地去除。识别检测的算法很简单,其核心的思路可以总结为:对于每个像素 \((x,y)\),对以该像素 \((x,y)\) 为中心的邻域(例如 7×7 大小的邻域范围)统计像素的均值 \(\mu\) 和方差 \(\sigma\),像素取值 \(I(x,y)\) 超出 \([\mu-k\sigma,\mu+k\sigma]\) 范围的就是 Outlier 噪点。其中 \(k\) 是可调整的参数,可以通过该参数来调整区间范围
对于这些超过 \([\mu-k\sigma,\mu+k\sigma]\) 范围的像素,直接令像素值 Clamp 到 \([\mu-k\sigma,\mu+k\sigma]\) 范围内即可(例如大于 \(\mu+k\sigma\),那么令像素取值为 \(\mu+k\sigma\)),因此准确来说这种方法是一种对噪点的 Clamp 而非 Removal。这种 Clamp 方法在也被应用到时间域降噪算法中解决拖影问题。在时间域滤波中,滤波公式为:
拖影现象产生的根源来自于 \(\overline C^{(i-1)}\) 与 \(\overline C^{(i)}\) 并非是同一个着色点,因此Clamp方法就是对 \(\overline C^{(i-1)}\) 做限制:
其中 \(\mu\) 和 \(\sigma\) 是当前帧 \(\overline C^{(i)}\) 邻域范围内的像素均值和方差。这种方法使得 \(\overline C^{(i-1)}\) 和 \(\overline C^{(i)}\) 不至于差距过大,一定程度上解决了时间域滤波的拖影问题
5 工业界在 RTRT 中使用的降噪方法¶
1)时空间方差引导滤波 Spatiotemporal Variance-Guided Filtering (SVGF)¶
Spatiotemporal Variance-Guided Filtering (SVGF) 是在基本的时间域降噪算法和空间域降噪算法的基础上增加了方差分析和一些 tricks。本质上它也是一种联合双边滤波,主要考虑三个因素:深度 Depth、法线 Normal 和颜色 Luminance
(1)Depth¶

\(exp(x)\) 是返回 \(e\) 的 \(x\) 次方,由于公式返回的是 \(-x\) 次方,所以 \(p\) 和 \(q\) 之间的差异越大,互相之间的贡献越小:
分母中的 \(|\nabla z(p) \cdot(p-q)|\) 代表深度的梯度 * 两点间的距离。考虑这样一个情况:当 \(p\) 和 \(q\) 之间的深度差异特别大,但是是由于深度梯度导致的(比如上图中的 A点 和 B点),\(p\) 和 \(q\) 仍然需要考虑它们之间的贡献,这个贡献不应该因为它们之间深度差异很大就抹除。在这样一种情况下,就需要 \(|\nabla z(p) \cdot(p-q)|\) 这个分母项(深度的梯度 * 两点间的距离)来使得整个 \(x\) 的值仍然较小,最终的计算贡献才能较大。此处还可以理解为:虽然 \(p\) 和 \(q\) 的绝对深度差异很大,但是投影到其法线的切平面上的深度差异很小(例如上图中,如果 AB 所在的表面是光滑的,则可以认为二者的投影到其法线的切平面上的深度差异为0)
\(\epsilon\) 是一个额外项,其作用是避免分母为0或接近0的情况,即两点足够接近导致最后的数值太大的情况
\(\sigma_{z}\) 是一个用来控制指数衰减的快慢的参数,或者理解为控制深度 depth 的影响大还是小
(2)Normal¶

直接使用法线的点乘结果就可以基本表示 \(p\) 和 \(q\) 之间的差异,点乘结果越大说明法线方向越接近
\(\sigma_{n}\) 是一个用来控制指数衰减的快慢的参数,或者理解为控制法线的影响大还是小
需要注意的是如果场景应用了法线贴图,就需要使用剔除掉法线贴图的最原始的场景来进行上述计算
(3)Luminance¶

在考虑颜色差异时,最简单就是应用双边滤波。但是这会存在一个问题:当图像存在噪声的时候,对一些极亮或者极暗的点的颜色如果也进行滤波处理就会出现错误。因此,对于这些噪声点,在实际滤波的过程中就不应该参与贡献。SVGF 就利用方差解决了这一问题
分母中的 \(Var\left(l_{i}(p)\right)\) 表示计算 点P 周围一定范围内的方差;同时可以利用时间域得到上一帧的 Variance 进行一个插值;\(\sqrt{g_{3 \times 3}\left(\operatorname{Var}\left(l_{i}(p)\right)\right)}\) 则表示对周围 3x3 区域内的方差做一次空间上的滤波得到最终的 Variance。这个过程就是利用两次空间域和一次时间域的滤波得到 点P 的一个精准的 Variance 值
综上所述,SVGF 可以得到很好的 RTRT 结果,但是它仍然无法解决时间域滤波导致的残影问题
2)Recurrent AutoEncoder (RAE)¶
Recurrent AutoEncoder (RAE) 方法基于神经网络,是一种后处理的技术,输入 noisy 的图和 G-buffer,利用 Recurrent 方法自动累积 temporal 的结果来进行滤波得到一张 clean 的图。具体过程涉及神经网络在此不做详细解释

对上述两种降噪方法进行对比如下:

需要注意的是,虽然目前看起来 RAE 方法的速度和质量仍然比不上 SVGF,但是考虑到未来实时光追的 SSP 数量增加会显著增加 SVGF 的计算量,并且硬件发展已经有支持神经网络计算的 tensor core,RAE 方法未来的上限很可能会比 SVGF 更高
本篇笔记主要参考了以下这篇博客,感谢 WC Yang 大佬的分享:
高质量实时渲染:实时光线追踪 | YangWC's Blog
2023年7月 ziao德