目录
一、Real Time Ray Tracing(RTRT)
RTX
光路样本
二、Industrial Sulution-temporal
motion vector
G-Buffers-几何缓冲区
Back projection
Temporal Accum./Denoising
三、Implementation of filtering
Gaussian filtering
Bilateral filtering-双边滤波
Joint Bilateral filtering-联合双边滤波
四、Implementing Large filters
Separate Passes(拆分实现)
Progressively Growing Sizes
五、Outlier Removal
Outlier Detection and Clamping
Temporal Clamping
六、Specific Filtering Approaches for RTRT
SVGF-Basic Idea
SVGF-Joint Bilateral Filtering
七、RAE(Recurrent AutoEncoder)
AutoEncoder
Comparison
RTX其本质上只是硬件的提升,并不涉及任何算法部分,在显卡加装了一个用来做光追的部件,由于光线追踪做的是光线与场景的求交,结合games101我们知道光线要经历BVH或者KD-tree这种加速结构,也就是做一个树的遍历从而快速判断光线是否与三角形求交,这部分对于GPU来说是不好做的,因此NVIDA设计了专门的硬件来帮助我们每秒可以trace更多的光线。
1 sample per pixel就是每个像素采样一个样本.从而得到最后的光追结果。
至少要有四条光线才能构成一个最基本的光路:
path tracing本身是一种蒙特卡洛积分的方法,本身会产生噪声,我们采样的样本越多,其噪声越小。
RTRT最核心的技术正是 降噪。实时光线追踪与path tracing相比,只是进行了一算法上的些简化,算法的核心思路不变,它本身的突破是由于硬件的能力提升。
即使是1spp得到的这么糟糕的结果,通过降噪我们仍然可以得到一个很棒的效果:
目标为1SSP下:
很多方法在这不适用:
首先假设整个看的场景的运动是连续的,就是camera以某种轨迹看向不同的物体,帧与帧之间有大量的连续性。
物体在帧与帧之间是如何运动的,知道某个点在上一帧里点对应的位置。
本质上是时间上有记忆的一种复用,找某一点在不同帧上的对应关系来得到结果。
在screen space我们可以得到很多的信息,可以从camera看去通过screen space生成深度图,法线图,Albedo图等,也可以存储per pixel depth,normal,世界坐标。
通过Back projection方法求出了motion vector
frame i: 当前帧
frame i-1:上一帧
我们找的不是像素的位置,而是当前帧像素里包含的内容在上一帧哪一个像素里。当前帧(frame i)中蓝点这个像素我们所得到的点的世界坐标,投影到上一帧(frame i-1)中对应的是哪个像素。
得到了motion vector之后,我们就可以把当前帧(noisy的图)和上一帧(没有noisy的图)线性结合在一起。
首先对当前帧进行自己的降噪,让他不那么noise,然后在时间上,使用当前帧的结果和当前帧对应的上一帧(已经降噪了的)做线性blending.其中表示平衡系数,当前帧的贡献,在(0.1-0.2)之间。
时间上的降噪的过程:
缺点:
避免残影:
阴影问题
光源从左到右移动,而场景几何并不发生移动,motion vector是0,但是由于我们一直在复用未移动的帧结果,就会导致拖影的阴影出现。场景不动的情况下 motion vector为0,如果shading改变会出现错误的显示。
地板是不动的,因此其上面的每一个像素点的motion vector为0,当我们移动物体时,其地板需要一定的时间适应,之后再在地板上反射出当前场景中的物体,也就是反射滞后。
滤波Filtering:模糊操作,从一个有噪声的图得到干净的图。采用的低通滤波器只保留低频的信息,除掉高频的信息。
为了给光线追踪由于蒙特卡洛产生的噪声降噪时,用的是高斯的滤波器,它的滤波核为类似于正态分布,中心值高,向两边衰减:
对于任何一个中心像素i
我们需要定义 权值和(sum_of_weights),加权贡献值的和(sum_of_weighted_values)
对于中心像素i周围一圈的任意像素j(包括像素i本身)
根据像素i和像素j之间的距离和高斯的σ找对应的j贡献给i的值(权值) w_ij
将权值w_ij与像素j对应的颜色值相乘得到j的加权贡献值累加
将权值w_ij累加到sum_of_weights
进行归一化sum_of_weighted_values/sum_of_weights从而得到像素i最终的结果
通过高斯我们得到了一个整体都被模糊的结果,但是我们想让边界仍然锐利,双边滤波(Bilateral filtering)将颜色变化特别剧烈的地方认为是边界。
保留边界的信息(高频):我们认为颜色变化特别剧烈的地方认为是边界,如果二者差距不是特别大我们继续用高斯处理j到i的贡献。如果像素i和像素j之间的颜色值差距过大,我们认为这两个像素分别在边界的两边,从而让像素j给i的贡献变少,就不会出现边界的模糊情况。
i和j代表一个像素,k和l代表另一个像素,前半部分为高斯滤波,如果ij之间的差距过大,则让j给i的贡献变小,I(i,j)表示第一个像素的值,I(k,l)表示第二个像素的值,他们之间的差异就是分子部分,如果差异过大,就相当于在原本的高斯核上乘上了一个指数(负距离的平方)距离差异越大,整体接近0。
通过双边滤波我们对左边的原图进行处理,可以发现山上的细节和湖面的细节被很好的模糊了,但是边界仍完美的保留了下来,这就是我们想要得到的结果,保留了低频信息但也保留了边界,但有可能分不清噪声和边界。
Gaussian filtering判断两个像素之间的绝对距离,Bilateral filtering两个像素之间的距离,和颜色之间的距离。G-BUFFER完全没有噪声,与G-buffer有关。
在RTRT中得到的结果有很明显的噪声:
如果用高斯,我们是通过两个像素之间的绝对距离找对应贡献,双边滤波的话在考虑距离的同时还会考虑两个之间的颜色变化,因为A和B都很noise,双边滤波得到的结果是不准确的。
引入深度:
从G-buffer得到各自的深度信息,我们可以看到B的深度比较浅,而A的深度比较深,所以我们不希望A有太多的贡献到B上。
引入法线:B,C二者之间的深度是差不多的,BC法线朝向是不一样的。
联合双边滤波的本质就是在kernel里多算几组不同feature下的贡献,并将其相乘得到最后的结果。
滤波:对于任何一个像素,我们需要考虑他周围N * N个像素的贡献值,加权平均之后写回这个像素上。如果是large filter,也就是如果这个N * N很大,就会使得filter变得特别慢。
有一张图,我们先将它在水平方向上filter一遍,之后再在竖直方向上filter一遍,也就是将本来一趟N * N的,分成两趟来做,水平的1 * N和竖直的 N * 1。
如果我们使用N * N的filter,对于任何一个像素我们需要访问N^2个纹理。但如果我们将其拆分为两趟,第一趟访问N个纹理,第二趟访问N个纹理,总共访问了2N个纹理。
2D的高斯filter拆分为两个1D的高斯filter:高斯函数本身就是可拆分的
filtering == convoluation 滤波 == 卷积
我们不希望对于任何一个像素进行N * N次的纹理查询,采用一个逐步增大的filter,比如先用一个小的filter,然后用中号的filter,最后是大号的filter,通过多趟的filter得到N*N的filter得到的结果。
多趟操作,每趟都是5 * 5大小的filter,第一趟是i=0,第二趟i=1……不同趟数的5*5的filter中间是有间隔的,在第i趟时,样本之间的间隔是2^i,但每次都要采样5个。
假设要做5趟,每趟都是5 * 5的filter,那么i在第五层时候,样本之间的间隔是 2^4 = 16,第五层一共要五个样本,也就是4个间隔,大小为64。也就是说在第五层时候我们相当于做了一个64 * 64的filter,但我们对于这个像素来说,我们做了五趟,每趟5 * 5大小的filter,一共做了 125次的纹理查询,对于 64 * 64 = 4096次这个数字来说是很小。
1.为什么要逐渐增大filter,而不是一上来就使用第五趟中间间隔16个样本的filter?
2.为什么可以采样间隔那么多的样本?
用蒙特卡洛方法渲染一张图时,得到的结果会出现一些点过亮或者过暗,如果有一个点过亮,在经过这个7*7的filter处理过后,会影响一块区域,使得它这一块区域变亮。
Detection:对于每个像素,我们都取他们周围一个小范围的区域,例如7*7或者5*5,然后把这个范围内颜色的均值(mean)和方差(variance)给算出来,我们认为正常的范围在均值+-若干方差内,超过这个范围的我们认为他是outlier。
Clamping:如果在范围内我们找到了outlier的点,我们把这个点的值给clamp到接近范围的值。
当motion vector为0导致的残影现象,当前帧与上一帧中对应的信息差异过大时,我们把上一帧中的信息值给clamp到接近当前帧的信息值。
如果上一帧对应的值超出这个范围,则把他clamp到范围内,再和当前帧做线性blending,从而得到当前帧noisy-free的结果。
ground truth和svgf结果非常接近,SVGF的方法与在时空上降噪的方法差不多,加了一些variance analysis和tricks.
Depth:
A,B侧向我们,所以深度差距比较大,按照深度来决定贡献不太合理,可以看沿着法线的那个面投影出来的深度差异。exp(x)是返回e的x次方。
深度的梯度就是往某一方向上的变化率,因此当我们知道A和B之间的距离,之后用 深度梯度 X 距离 = 实际深度变化量(垂直于平面法线上的变化 * 距离 = 实际深度变化量)
Normal:
我们用两个点法线向量求一个点积,由于求出来的值有可能是负值,因此使用max()把负的值给clamp到0。如果场景中运用了法线贴图来制造凹凸效果,我们再判断时运用平面原本的法线,而不是为了制造凹凸效果而改变过的法线。
Luminance:
在考虑颜色差异时,最简单就是应用双边滤波里给的颜色差异来考虑,比如我们将RGB转换为grayscale(灰度),这种颜色我们称其为luminance。由于噪声的存在会出现一些干扰,也就是B点虽然在阴影里,但是可能刚好选择的点是一个噪声,也就是其特别亮,此时A特别亮,B也特别亮,那么A和B就会互相贡献。
SVGF中V-variance方差:我们在考虑A点是否贡献到B点时,先看A和B点之间的颜色差异,并且除以B点周围的一个标准差。在标准差大的时候更不愿意去相信颜色差异。
B点的variance:在点的周围取一个7*7的区域算出区域里的variance,同样的操作在时间上累积下来(平均),找上一帧中对应点的variance,从而通过motion vector在temporal上把variance信息给filter下来,这样先spatial filter求出variance之后,再随时间平均从而得到了一个比较平滑的variance值。最后在使用variance时如果不放心,我们再在周围取一个3*3的区域做一次spatial filter得到variance。
spatial filter --> temporal filter --> spatial filter
SVGF会产生“残影“现象:当一个场景固定,我们只移动光源时候,阴影会随着光源的移动而变化,当前帧会复用上一帧的阴影。
AutoEncoder,是一个漏斗形的结构,输入经过若干层神经网络后变成一个很小的东西,之后再将小的东西不断地展开,类U型神经网络。
每一层都有连向他自己的。可以利用历史的信息,每一层神经网络有一个recurrent连接,也就是每一层神经网络不仅可以连向下一层,也可以连回自己这一层。-从之前的帧中积累(并逐渐忘记)信息