Lecture 13 Real-Time Ray Tracing 2
Implementing a spatial filter
这里想做的是低通滤波
- 移除高频信号
- 会不会丢失高频中的信息?
- 噪声不一定只在高频中
- 集中在频域
- 这些filtering可以应用在PCSS、SSR上的降噪
用$$\widetilde C$$表示有noise的图像
\[K$$表示滤波核kernel,比如box filter、gaussian filter等等,滤波器在做什么由滤波核定义 用$$\overline C$$ 表示滤波完输出的图像 这里一般用Gaussian filter ![](/i/l/?n=24&i=blog/3247192/202409/3247192-20240901141200714-90974078.png) - 对于任何一个像素都取其周围一定范围,中心像素称为$$i$$,周围像素称为$$j\]-
\[j$$肯定会对$$i$$有贡献,贡献多少根据$$i$$和$$j$$之间的距离在gauss上的值
\]For each pixel i
sum_of_weights = sum_of_weighted_values = 0.0
//gauss在3sigma之后几乎没有值,但理论上gauss不管离多远都有值
//这里只取大概3sigma的范围
//这里的around包括i自己
For each pixel j around i
//贡献由i与j的距离和高斯决定,sigma是标准差,决定高斯的大小
Calculate the weight w_ij = G(|i-j|, sigma)
//input是输入图像
sum_of_weighted_values += w_ui * C^{input}[j]
sum_of_weights += w_ij
//这一步是做归一化
//滤波核不一定是归一化的,可以是任意倍gauss,只用在滤波的过程中归一化
//保证能量守恒
C^{output}[i] = sum_of_weighted_value / sum_of_weighted- ``sum_of_weights``用于归一化 - 用gauss得到的``sum_of_weights``不等于0,但是其他滤波核不一定,要注意判0 - 如果权和为0,那么就按0处理 - value可以是一个多通道的数
Cross / joint bilateral filtering 联合双边滤波
高斯滤波的问题
- 高斯滤波模糊了边界
- 而通常我们希望边界是锐利的(我们希望保留高频的边界)
所以要引入双边滤波
Bilateral filtering 双边滤波
双边滤波基于的观察
- 边界$$\leftrightarrow$$颜色剧烈变化的地方
Idea
-
如何保留边界
-
看像素$$j$$和像素$$i$$的差异是否很大
- 如果相差不大,说明不是边界,正常按高斯滤波
- 如果相差很大,就希望$$j$$到$$i$$的贡献变少
-
简单给kernel加一个控制
- \[w(i,j,k,l)=exp(-\frac{(i-k)^2+(j-l)^2}{2\sigma_d^2} - \frac{\lVert I(i,j) - I(k,l)\rVert^2}{2\sigma_r^2}) \]
-
这里将坐标拆开了,(i, j) 和 (k, l)是两个点
-
左边的分式是距离,增加的是右边的分式,右式也是一个高斯,右式的分子是颜色差异
相当于给原先的高斯乘上一个$$e^{-右式}$$(exp是e的x次方),让贡献变小
但是这种做法有个问题,它分不清噪声和边界的区别(因为我们认为噪声和边界等价了)
Joint bilateral filtering 联合双边滤波
-
Gaussian filtering:提出用像素之间的绝对距离来判断贡献是多少
-
Bilateral filtering :提出用像素之间的绝对距离和颜色距离来判断贡献是多少
-
Joint bilateral filtering:可以使用更多的标准,具体用什么标准不是固定的
-
这种思想就叫做Cross / Joint Bilateral Filtering 联合双边滤波
-
联合双边滤波特别适合用于解决(蒙特卡洛)path tracing结果的denoising问题
-
不一定非得选择Gauss,这里只需要定义一种函数,随着距离有衰减,符合要求即可
并且用Gaussian也不一定非得按一般Gaussian的写法,比如前面的\(\frac{1}{\sqrt{2\pi\sigma}}\)可以不要,指数内部也可以不写成\(\frac{x^2}{2\sigma^2}\),一倍\(\sigma^2\)也可以
- Gaussian
- 指数函数(的绝对值)
- Cosine
- ect.
-
没什么缺点
-
在渲染时有很多额外信息是可以免费得到的,G-buffer!
- Position
- Normal
- Albedo
- Depth
- object ID
- etc.
G-buffer是完全没有noise的,并且也与多次bounce无关
Example
假设有以下信息
- Depth
- Normal
- Color
- A和B:深度也可以描述成一种高斯,将深度差异也考虑进去
- B和C:考虑法线差异,B和C在法线标准下离得很远
- D和E:考虑颜色差异,保证阴影不被糊掉
- 在有noise的情况下用颜色不是很稳妥,但毕竟用颜色还是可以保证不糊掉阴影的边界
每个信息的Gauss都有一个参数\(\sigma\)来控制它,这几个贡献都是乘起来的(见双边滤波给的公式),而谁的贡献大谁的贡献小由各自的\(\sigma\)决定
如何调节\(\sigma\)?控制的标准很多,所以每一项都可以不那么严格,那么每一项一开始都可以取一个巨大的滤波核(一个很大的\(\sigma\))
Implementing large filters
对于任何一个像素,都要考虑其周围\(N\times N\)个像素
- 对于小滤波核,直接做没问题(e.g.\(7\times7\))
- 对于大滤波核,可能太耗性能了(e.g.\(64\times64\))
- 可以做FFT,图像与滤波核相乘,再做IFFT
- 但是FFT在GPU上优化得并不好,还是比较慢
- FFT在CPU上快
两种解法
Solution 1: Separate Passes
-
考虑一个2D Gaussian filter
- 拆分成功一个水平通道(\(1\times N\))和竖直通道(\(N\times 1\))
- \(N^2\rightarrow N+N\)
-
为什么一个2D Gaussian filter可以拆分成两个1D Gaussian filter
-
因为2D高斯本身就是拆开定义的
\(G_{2D}(x,y)=G_{1D}(x)\cdot G_{1D}(y)\)
-
filtering == convolution
-
将2D Gaussian拆成两个1D Gaussian相乘,\(x\)与\(y\)没关系,所以可以将\(y\)提出去
先对\(x\)卷积,再对\(y\)卷积
\(\iint F(x_0,y_0)G_{2D}(x_0-x,y_0-y)\mathrm{d}x\mathrm{d}y = \int(\int F(x_0,y_0)G_{1D(x_0-x)\mathrm{d}x})G_{1D}(y_0-y)\mathrm{d}y\)
-
理论上>复杂一点的卷积核可能就不能拆分了
实际上对于小的滤波核看起来差异不大(e.g.$ \le 32\times32$)
比如双边滤波是两个高斯相乘,就不能拆分了
-
solution 2: Progressively Growing Sizes
Idea: 用逐步增大的filter做多趟的过滤
-
一个具体的例子, a-tous wavelet transform(不要和小波联系起来)
-
多趟,每趟都是\(5\times5\)大小的filter
-
不同趟数下,\(5\times5\)filter有不同间隔
图中只是以一个点的filter为例,并不是说整个图像只有黑点需要filter,所有像素都需要filter
第一趟时间隔为1,第二趟时间隔为2,第三趟时间隔为4
\(i\)从\(0\)开始,在做第\(i\)趟时,间隔为\(2^i\)
当\(i=4\)时,间隔为\(2^4=16\),共有\(5\)个样本,则5个样本间有4个间隔,占据\(64\)个格子,总共是\(64\times64\)
现在将\(64^2=4096\)转变成了\(5^2\times5=125\)
-
-
A deep understanding
-
为什么要用一个逐步增大的filter
- 用一个更大的filter == 去除更低的频率
-
为什么可以跳着采样
-
Sampling == repeating the spectrum 采样是在频域上搬移频谱
- 采样得密集说明搬移的频谱和频谱之间距离大
- 采样得稀疏说明搬移的频谱之间距离小,可能会发生混叠
- 第一个Pass去掉蓝色部分的频率,第二个Pass去掉黄色部分的频率
不断变化的filter size是为了逐步去除更低的频率
进行分段考虑,每一段可以针对性地处理
-
为什么更高的Pass上可以间隔得更开
相当于对一个更大的filter做了一个采样
比如覆盖了\(9\times9\)的范围,并没有做filter,而是采样留下了\(5\times5\)
这个采样过程对信号往不同方向进行了搬移,搬的间隔正好是两倍的上一趟Pass留下的最高频率(正好把sample的间隔乘了2,图中蓝色部分搬移到黄色部分),正好不会出现aliasing
而如果一开始就直接做高层的pass,会因为没有将更高频的频谱去除而混叠
-
实际上这么做可能还会出现一些问题
-
这个filter不是类似高斯严格意义将高频去除的filter
尤其是考虑了联合双边滤波,很多高频信息会有选择地留下来,导致搬移的时候会出现问题
就常常会出现一些格子状的artifacts
-
-
-
Outlie Removal (and temporal clamping)
平常用蒙特卡洛方法渲染时可能会出现一些超级亮的点(outlier, rendering领域也叫firefly)
- Filtering并不是全能的
- 有时filter后的结果还是noisy甚至blocky的
- 比如说可能会将一个很亮的点扩散成一个更大的区域
- 并且虽然颜色降下来了,但可能还是超过1
- 有时filter后的结果还是noisy甚至blocky的
- Idea
- 在做滤波之前去掉outliers
- 这样能量就不守恒了
- 如果想得到完全准确的结果,就不应该做outlier removal,而是等更多的sample
- 如何处理?
- 在做滤波之前去掉outliers
Outlier Detection and Clamping
-
Outlier detection
-
对于每个像素,取周围如\(7\times7\)(工业界常用的大小)的范围
-
计算均值(也有用中位数的)和方差
-
复杂的话可以参考Variance shadow mapping (VSM)
-
简单的话直接将这些点都扫一边就知道了
-
绝大多数像素颜色应该集中在均值\(\pm\)若干个方差之间
均值记为\(\mu\),方差记作\(\sigma\),\(k\)一般取\(1\sim3\)
outside\([\mu-k\sigma,\mu+k\sigma]\rightarrow\)outlier
-
-
可能会把光源干掉了
- 那就先渲染没有光源的场景
- 在玩Outlier Removal再将光源装进去
-
跨场景中几何的边缘可能会找错分布
- 如果找的点正好是某两个物体交界处,那么就有错误
- 特殊处理
-
-
Outlier removal
其实并不是移除了,而是clamp了
将outlier Clamp到\([\mu-k\sigma,\mu+\sigma]\),大于的取最大值,小于的取最小值
- 工业界的做法会更复杂,可能会在某个颜色空间上进行,而且范围也不一定如此简单,会在3D颜色空间上用一个高斯来描述
- TAA的具体实现
Temporal Clamping
找到像素在上一帧对应颜色,与当前帧blending,如果两帧颜色相差过大,则将上一帧的结果往当前帧结果拉一点,思想类似Outlier removal
\(C^{(i)}=\alpha\overline C^{(i)}+(1-\alpha)C^{(i-1)}\Rightarrow C^{(i)}=\alpha\overline C^{(i)}+(1-\alpha)clamp(C^{(i-1)},\mu-k\sigma,\mu+k\sigma)\)
- clamp重新引入了noise,这是在noise和lagging之间的一个tradeoff
- 一般更倾向于没有lagging
- 就算无法接受带来noise,做一个更大的filter就好了,将noise转化成over blur
- 是将前一帧拉向当前帧,不能做反了
Specific filtering approaches for RTRT
Spatiotemporal Variance-Guided Filtering (SVGF)
三个因素
SVGF考虑了三个因素
-
Depth
\(w_z=exp(-\frac{\lvert z(p)-z(q\rvert)}{\sigma_z\lvert\nabla z(p)\cdot (p-q)\rvert + \epsilon})\)
- 分子表示深度差异
- 分母表示深度的梯度,\(\epsilon\)是一个微小的值,防止分母为0
- 如果深度差异大,梯度也大,二者相除,也就认为差异不大了
- 因此,一般不会直接比较两个点之间的深度差异,而是比较它们沿着面法线投影后的深度差异(或者说是其切平面上的深度差异)
只需有一定衰减形状即可,不一定是高斯,比如这里就不是高斯,只是一个指数衰减函数(防止分母为0,加上一个微小的值\(\epsilon\))
- 这里A跟B处于同一个面上,颜色也相近,应该要相互贡献,可二者在深度上却差异较大(因为处在的面的侧向的),公式中的梯度\(\nabla\)就算为了计算二者在法线方向上的深度差异,而二者处于同一平面,法线上的深度差异几乎没有,那么就认为其深度差异并不是很大
- 深度的梯度表示往某一方向上的变化率,再给一个距离就知道该方向上的深度变化量
-
Normal
\(w_n=max(0, n(p)\cdot n(q))^{\sigma_n}\)
同样也不需要这是一个高斯,只需衰减即可
这里先做了一个clamp,防止法线点乘为负,\(\sigma_n\)控制指数衰减的快慢,也就是判定法线差异是否严格(类似Blinn-Phong中控制Specular)
- 注意,如果应用了法线贴图或者凹凸贴图,这里比较的是macro normals(没有法线贴图前的normal),否则法线处处不相同就不合理了
-
Luminance (颜色灰度值)
SVGF用Luminance是因为HDR的原因,用RGB的话简单平均一下也可以,或者直接在RGB空间上算两个点的距离也可以,但是输入的图一定得是HDR的,不能先截断到1,否则denoise后的图就变暗了
先将RGB颜色值转化成灰度值Luminance,颜色差异大就不应该混合起来了
\(w_l=exp(-\frac{\lvert l_i(p)-l_i(q)\rvert}{\sigma_1\sqrt{g_{3\times3}(Var(l_i(p)))}+\epsilon})\)
这里的Var是Variance 方差,根号后的结果是标准差
通过除以一个标准差,如果标准差过大,则说明不应该过多相信颜色差异
\(\epsilon\)防止除以0,\(\sigma_l\)控制衰减速率
-
有一个问题
如图中B点在阴影中,但是可能由于噪声会导致B非常亮,而A在阴影外也比较亮,A就贡献到B,产生了错误
那么考虑B点周围的方差,如果方差比较大,就不应该过多地相信这两点之间的颜色差异
-
分母中的\(g_{3\times3}Var\)
- 计算周围\(7\times7\)区域的Variance
- 也可以在时间上通过motion vector累积起来,得到一个相对平滑的Variance
- 再在B点周围取了一个\(3\times3\)的小区域,在这个小区域再求了一次平均方差
- 颜色本身带有噪声,通过Var将噪声的影响去除了
-
Failure Cases
-
阴影滞后
光源移动,motion vector为0,那么物体阴影会沿用上一帧,导致残影
Recurrent AutoEncoder (RAE)
Basic Idea
用Recurrent AutoEncoder这样一种结构(神经网络相关),对路径追踪得到的图像做Reconstruction,也就是做滤波,是一种后处理
- 用后处理神经网络做denoising
- 使用G-buffer
- 神经网络自动累计temporal信息
- 输入noisy的图, 得到denoising后的图
关键结构设计
-
AutoEncoder (or U-Net) 结构
-
Reurrent convolutional block
每一层神经网络不仅连向下一层,还连回自己,那么跑完当前帧后,下一帧就可以用到上一帧的信息
这里不是用motion vector,而是靠神经网络学习得到的
优点和缺点
-
优点
-
在不同SPP的情况下Performance一样
Nvdia Optix中带的denoiser就是神经网络的denoiser,在高SPP下性能好
但是是denoise单张图片而不是图片序列
-
tensor code能解决性能问题
10ms左右,还是比较慢
-
-
缺点
-
有大量overblur
-
上一帧的几何边缘位置在当前帧可能会留下残影
-
亮度高时会有更多artifact
-
SVGF和RAE对比
Quality | Artifact | Performance | Explanability | Where did the paper go | |
---|---|---|---|---|---|
SVGF | Clean | Ghosting | Fast | Yes | HPG |
RAE | Overblur | Ghosting | Slow | No | SIGGRAPH |