Games202 实时光线追踪与杂项
Real-Time Ray Tracing
What is RTRT
实时渲染的领域一直都呼唤着光线追踪。
Ray tracing is the future and ever will be. ——industry
2018年,NVIDIA推出了RTX架构,这标志着RTRT正式登上渲染工业界。它可以做出软阴影,做出反射(实际上就是Glossy全局光照),做出GI。总而言之,RTX就是一种能trace光线的硬件架构。
RTX本身是硬件上的一种突破,在显卡上专门增加了一种光线追踪的结构,实际上是光线和BVH等加速结构的求交。这个过程在GPU上不好完成,所以NVIDIA设计了专门的RT Core来加速光线的trace。在RTX中,每秒能trace 10Giga的光线,大体来说一个像素能用一个样本(1spp)来采样得到结果。
1spp path tracing由1 rasterization(primary) + 1 ray(primary visibility) + 1ray(secondary bounce) + 1ray(secondary visibility) 。这是最基本的光路情况,这四条光线构成样本。在第一步的过程中,一般直接实行一次光栅化,来代替所有的primary ray。
path tracing本身噪声非常巨大,所以1spp 是Extremely noisy的。因此,RTRT的核心技术是降噪(Denoising)。

总体来说,RTRT的目标是1SPP下降噪:
- Quality(no overblur, no artifacts, keep details)
- Speed(<2ms to denoise one frame)
这根本不可能!以往的降噪研究有一些
- Sheared Filtering Series 切变滤波(SF,AAF,FSF,MAAF)
- Offline methods (IPP,BM3D,APR)
- DL series(CNN, Autoencoder),深度网络跑一遍需要几十毫秒,代价太大
Idustrial Solution
RTRT的核心想法就是:Temporal,也就是时间上的滤波方法。这里可以用一种递归思维去考虑,当前帧的前一帧是已经滤波好的。如果场景的运动是连续的,可以用Motion Vector来找到一种一一对应:

Motion Vector给出了上一帧物体的位置,而上一帧已经降噪好的颜色是可以拿来复用的,换言之这就相当于增加了SPP!每一帧对下一帧的贡献会不断下降,这就是时间上的复用。
首先要介绍G-Buffer(Geometry Buffer)。可以把很多结果保存起来,比如Direct Illumination、Normal或者Albedo,这些信息可以直接存储下来,相当于渲染中的一些免费信息。用一些图来存储这些信息就是几何缓冲区,储存的是屏幕空间的信息。
我们要找的是像素与像素的对应,想找到$x$帧的像素$p$在$x-1$帧的对应的物体的世界坐标投影到屏幕之后看到的结果。换言之,就是 找一个像素,使得两个像素对应同一个世界空间中的位置。那做法就呼之欲出了:
- Back Projection
- 如果有G-buffer中存储的世界坐标,直接复用
- 如果没有,使用$V^{-1}P^{-1}E^{-1}x$逆变换
- Motion:如果上一帧$s’$,$Ts’ = s$,那么$s’ = T^{-1}s$
- Projection:$x’ = E’P’V’s’$
对于当前帧来说,
$$
\bar{C}^{(i)} = Filter[{\tilde C^{(i)}}]
$$
然后再线性组合
$$
\bar{C}(i) = \alpha \bar{C}^{(i)} + (1-\alpha)\bar{C}^{(i-1)}
$$
一般$\alpha$取$0.1-0.2$——也就是$80%\sim 90%$的信息都来自上一帧!
时间上的复用很好用,但是很多时候不可用!
(1)切换场景
- 第一性问题:如何解决场景的切换,也就是第一帧问题
- 光源突变问题:比如蹦迪,光线突变,颜色会出现变化
- 镜头切换问题
切换场景需要一段时间来预热(burn in period)
(2)倒退
当人往后走的时候,边缘上会出现新的物体,有大量新物体出现,但是在之前帧找不到对应
这个问题归根结底是Screen Space的通病
(3)去遮挡
比如一个物体刚刚被遮挡,现在被遮挡物移开,那么motion vector对应的是之前的遮挡物(disocclusion问题)
这个问题归根结底也是Screen Space的信息。比如:

上一帧的信息又像是显示了一点上一帧的东西,就形成了Lagging
解决方案很简单:Adjustments to Temp. Failure
第一种思路是Clamping
考虑$\bar{C}(i) = \alpha \bar{C}^{(i)} + (1-\alpha)\bar{C}^{(i-1)}$,把$C^{(i-1)}$先向$C^{(i)}$拉近,比如Clamp到$C^{(i)} \pm 2\sigma$
第二种思路是Detecting
判断一下Motion Vector,用一个Object ID,物体有自己的颜色,如果检测到不可用的点,可以调整$\alpha$;也可以变强Filter。
但这就相当于,我们重新引入了Noise。
(4)着色

如果物体不变,面光源移动,物体的Motion Vector是不变的,就会导致上一帧的信息复用。
再考虑这个场景:

如果移动椅子,地板不变,反射结果的Motion Vector也不变,就会导致很严重的滞后效应.
Spatial Filtering
滤波本质是一个模糊操作,把noisy的图变成干净的图。这种滤波实际上是低通滤波,去掉一些高频信号。这样就会有两个问题:
- 删除高频有效信息
- 保留低频噪声
我们这里假设只保留低频,并约定滤波核是$K$。
我们常常使用Gaussian滤波核:

对于一个像素$i$,周围像素$j$的距离以Gaussian函数的结果来进行贡献,得到的就是Gaussian滤波。实现如下:
1 | for each pixel i |
这是一个非常标准的实现,实际上我们只需要考虑滤波核的形状。同时由于Gaussian滤波核足够大,可以只在$3\sigma$之内考虑。
Bilateral Filtering
Gaussian滤波的结果会让任何一个点都均等的糊掉,所有东西都会被糊掉。但正常情况,我们希望保证边界基本锐利——边界就是高频信息。所以我们引入一个新的滤波核:双边滤波。
我们假设边界是剧烈变化的颜色。如果两个点的颜色相差很小,那么就进行Gaussian滤波;否则我们就不希望j能贡献到i。所以我们基于Gaussian滤波核进行修正:
$$
w(p,q)=\exp \left( -\frac{||q-p||^2}{2\sigma_d^2}-\frac{||C(p)-C(q)||^2}{2\sigma_c^2} \right)
$$
这样我们就保证了边界高频的同时一个较好的滤波效果。
但这个能直接用在Denoising上吗?因为两个点本来噪声程度不同,可能差距就很大,所以容易分不清噪声和边界。SVGF对其进行了一个解决。
Joint Bilateral Filtering
高斯滤波提出了一个标准:像素之间的绝对距离。双边滤波则提出了两个标准:颜色之间的距离。那能不能找出更多的标准呢?
联合双边滤波特别适用于蒙特卡洛积分产生的噪声!
我们有一些信息是可以免费得到的:坐标,深度,……也就是G-Buffer。G-Buffer是完全没有噪声的,因为它和bounce没有关系。我们也不一定使用高斯函数,只需要一个随着距离衰减的函数就可以。

现在我们考虑G-Buffer中有深度、法线、颜色这三种信息,下面这个图:

只考虑颜色显然是不合适的,会有很大差异。但A和B的深度不一样,完全可以用深度将二者区分;B和C的法线差异很大,完全可以用法线将二者区分;D和E的颜色差异比较大,可以用颜色的距离进行区分。
Large Filters
联合双边滤波的时候,我们经常取很大的滤波核。这样就需要一个大的滤波器。比如$N\times N$的区域,对比较小的Filter,直接算一遍开销并不大。那对于一个很大的滤波核如何解决呢?有两个思路。
Separate Passes。我们可以先水平的Filter一次,再竖直的Filter一次:

这是因为2D高斯函数是用1D高斯函数进行定义的,滤波就是卷积,所以
$$
\iint F(p_0)G_{2D}(p-p_0) dp = \int\left( \int f(p_0) G_{1D}(x_0-x) dx \right) G_{1D} (y_0-y)dy
$$
但是双边滤波的x和y怎么拆分?实时渲染嘛,随便拆拆。32x32看不出来什么区别。
Progressively Growing Sizes。可以用一个逐步增大的Filter,这种方法叫做a-trous wavelet。

第0趟的时候考虑$5\times 5$,第1趟隔一个考虑$5\times 5$,第$i$趟隔$2^{i}-1$考虑$5\times 5$,……这样实际上做$64\times 64$,只需要做5层$5\times 5$。这样4096次被简化到了625。
小Filter表示去除比较小的频率,所以我们要逐步增大Filter。而采样的本质是对频谱进行搬移,采样距离大则搬移距离远。不断增大的size可以不断去除更大的频率,在第二次采样的时候的间隔正好是第一次采样的时候的最大频率。所以搬移过程不会发生Aliasing,这种做法是比较稳妥的。
由于联合双边滤波会留下一些高频信息,所以可能看到很多格子状的Artifact,但这个问题可以再做一个小的Filtering来解决。
Outlier Removal
我们渲染出来结果有的时候会出现特别亮的点,尤其是在Monte Carlo积分中,而这些点都不好处理。有可能一个比较亮的点在Filter之后,反而扩散成很亮的一个区域,下场比较严重。

这些像素我们称为Outliers,而且需要在Filtering之前处理掉。虽然这样可能能量不守恒,但是这就是实时。
我们取一个$7\times 7$的区域,计算均值和方差。如果像素颜色在$[\mu -k\sigma, \mu + k\sigma]$,那么就认为这个像素是outlier,给他进行clamp。
Spatialtemporal Variance-Guided Filtering(SVGF)
有三点思想比较重要:
对于深度,做
$$
w_z = \exp \left(-\frac{|z(p) - z(q)|}{\sigma_z|\nabla_z(p)\cdot(p-q)|+\varepsilon}\right)
$$
这是一个指数衰减的函数,$\varepsilon$用来防止除以0,在空间上,考虑两个点:

$A,B$在同一个面上,相互之间应该是有贡献的,但是这个面是侧向的,所以深度差异很大。为了解决这个问题,可以将二者投影在法线上,比较投影距离。
对于法线,用
$$
w_n = \max(0, n(p)\cdot n(q))^{\sigma_n}
$$
如果法线相同就得到1,$\sigma_n$控制衰减快慢。如果场景使用了法线贴图,使用的也是没有贴图之前的法线。
对于颜色,用Luminance,
$$
w_l = \exp\left(-\frac{|l_i(p)-l_i(q)|}{\sigma_l \sqrt{g_{3\times 3}(\operatorname{Var}(l_i(p)))}}\right)
$$
具体来说,计算方差的时候要在7x7中计算,然后在motion vector上filter一个temporal信息。然后在周围的3x3区域内再做一次平均,得到精准的Variance。(Spatial + temporal + spatial)
SVGF很多时候倾向于overblur。SVGF之后有一个改进叫ASVGF,倾向于noise。
Recurrent AutoEncoder(RAE)
RAE使用一个post-processing的神经网络来denoising,用到了一些G-Buffer的信息。
这个神经网络结构比较像U字形,比较适合做图像上的操作。它有一个Recurrent核,可以复用信息。输入是G-buffer,输出是denoising结果。

在跑神经网络的时候,一些信息会残留到下一帧。训练神经网络的时候要训练单张的图,得到一些连续的帧。由于没有motion vector的信息,所以不会有倒退的问题。
RAE不是很完美,无法处理一些复杂的几何边缘。同时,场景中会存在大量的overblur。

RAE的优势在于对于不同spp,表现是一样的。NVIDIA optics的denoising实际上就是RAE拿去recurrent核,效果很好。而如今tensor core的引入,已经可以达到10ms左右的速度。
Industrial
Temporal Anti-Aliasing (TAA)
TAA实际上是先于rtrt出现的。走样的终极解决方案就是使用更多样本,也就是MSAA;TAA则是从上一帧复用一些sample。它的思路和rtrt是一模一样的。
如果场景运动了,就要考虑物体上一帧的位置,这也就是motion vector的作用;temporal的信息不可用的时候就进行clamp。
对于AA,要说几点:
1、 MSAA(multisample)、SSAA(supersampling),SSAA要更直观:先把场景按几倍进行渲染,然后再降采样,这个方法是正确的,但是会造成四倍计算开销。MSAA则在SSAA基础上进行近似,对于同一个primitive,所有样本都只做一次shading。MSAA在实现的时候会维护一张表,求出平均位置然后进行shadng。同时,MSAA支持Sample Reuse,对一些临近像素只做一次。在一些游戏中,设置120%渲染、200%渲染,实际上就是SSAA方法。

2、图像空间的AA,从FXAA到MLAA,现在比较流行的是SMAA(Enhanced Subpixel morphological AA)。SMAA先找到一个锯齿,然后拟合实际边界,根据边界占比填入shading:

效果比较好,处理一个1080p的图只需要1ms左右。
3、G-buffer 绝对不能反走样!
Temporal Super Resolution
这个操作主要是为了提高分辨率。Nvidia提供了一个DLSS来完成这个操作,DLSS1.0 几乎全靠猜,没有任何其他信息,对每个场景做一个单独的神经网络。DLSS2.0更多的利用了temporal信息,核心思想是应用TAA。
这样有一个非常严重的问题:如果有temporal failure,那不可以使用clamping了,否则会导致有些小的像素值是根据周围的点猜出来的,反而变得很糊,这对于分辨率提升来说是不可接受的。
所以关键是找到一个更好的复用temporal方案。

DLSS网络没有输出混合颜色,而是输出了temporal信息怎么用。DLSS可以让帧率下降,但是DLSS自身需要比较快。如何提高网络性能,NVIDIA也给出了解决方案。AMD叫FidelityFX Super Resolution,是类似的东西。
Deferred Shading
延迟渲染是节省着色时间的思路。原来的光栅化由于Z test的存在,一些被遮挡的物体会被着色,浪费了时间。所以它的思路是两个Pass:
- Pass1,不进行Shading,只更新深度缓存
- Pass2,和记录深度进行比较,然后进行着色
这样有一个问题,就是无法进行MSAA。但可以使用TAA或图像空间AA来解决问题。
此时,每个光源仍然需要一次Shading。Tiled Shading进行了解决:把屏幕切成若干条,然后判断是否和圆相交。

由于平方衰减的存在,只需要渲染能影响到的光源,而无需渲染所有光源,这样就进一步减少了光源数量。
如果在深度上也进行切片,把3D空间拆成网格,那么可以进一步减少光源数量。

Level of Detail Solutions(LOD)
在计算过程中,对Mipmap,只要选择一个正确的level,就可以降低计算开销。这就是cascaded思路。
例如对Shadow Map,一个texel覆盖的范围可能比较大,那离camera越远的地方可以越用粗糙的Shadow map,生成两种或多种不同分辨率的Shadow Map就可以解决这个问题。
但突然切换层级的时候,可能会出现一个artifact。所以重叠一部分会进行一些clamp,来解决这个问题。
类似的,也有Cascaded LPV,在不同精细的格子上进行传播。
Geometric LoD也是LoD的一个领域,比如根据物体的高模和低模,在不同远近进行渲染。同样会存在transition artifact,这可以用TAA来解决。这就是UE5的Nanite。
Global Illumination
对于软件的RT,常见的技术有
- 近处物体,HQ SDF
- 整个场景,LQ SDF
- 有强点光源,RSM
- Probes (DDGI)
硬件RT则使用
- RTRT
- RTXGI
More to Read
- Texturing an SDF
- Transparent Material
- Particle Rendering
- Post Processing
- Random Seed and Blue Noise
- Foveated Rendering
- Probe Based GI
- ReSTIR
- Smokes, SSSSS
- ……
Games202 实时光线追踪与杂项