Summary:
- 为什么实时光线追踪(RTRT)需要降噪
- 用联合双边滤波对RTRT的结果进行降噪(附伪代码和结果)
- Deferred Hybrid Ray Tracing 和 naive Path Tracing之间的区别
- 论文Edge-Avoiding À-TrousWavelet Transform for fast Global Illumination Filtering 的
为什么需要降噪?
光线追踪的效果非常真实:
比如,渲染Cornell Box,不需要复杂的技术,只用光线追踪就可以实现软阴影、color bleeding、全局光照等效果。下图是我之前实现的Path Tracing算法:
然而,对于这个比较小的256x256的场景,在CPU下 SPP = 128时,一帧画面需要多达40s时间渲染:
就算是用GPU进行优化,也达不到实时(30fps)的要求。
于是,实时光追采取的方案是:用较小的SPP(比如1SPP)迅速渲染出每一帧的带有大量噪点画面,然后利用单帧降噪(联合双边滤波)和多帧降噪(Temporal Accumulation)提升画面质量:
这套Denoising方案可以得到比较不错的结果:
联合双边滤波
说到降噪,能想到最简单的方案之一就是高斯滤波,然而高斯滤波只是单纯的加权平均一块区域的像素值,在降噪的同时,也模糊了不同物体之间的边界:
于是就有了双边滤波,不仅考虑两个像素之间的距离,还考虑了两个像素之间的颜色差。双边滤波假设:如果两个颜色之间的颜色差别较大,那他们就属于两个物体,那么,在滤波时不应该对彼此有贡献:
然而,对于上面的图片,降噪结果貌似还可以;但是对于1SPP生成的实时光追图片,双边滤波的效果就不行了,因为噪点太多了,双边滤波的假设并不成立。
观察高斯模糊和双边滤波,究其本质,是在用不同的度量(距离,颜色)来计算两个像素之间的相似性。但是由于实时光追的图片中大量噪声的存在,颜色这个度量本身是有噪声的,所以颜色并不是一个好的度量。
那么有没有其他不带噪声的度量呢?有的,比如G-buffer中的Normal, Position(世界坐标系下的位置), Depth等等:
假设我们的联合双边滤波考虑了像素之间的距离 $|\mathbf{i}-\mathbf{j}|^{2}$,像素之间颜色的差别 $|\widetilde{C}[\mathrm{i}]-\widetilde{C}[\mathrm{j}]|^{2}$ 和 法线方向的差别:
那最终 $i,j$ 像素彼此之间的贡献值就是:
其中 $\widetilde{C}$ 是光线追踪的带有噪声的结果,$\sigma_*$是超参。联合双边滤波的伪代码如下:
def JBF(frame, normal): //frame 原始噪声图像,normal G-buffer中的法线
filtered = new [][]; //和 frame大小一样的数组
for each pixel (x,y):
sum_of_weighted_values = 0
sum_of_weights = 0
for adjacent pixel (i,j):
weight = compute_J((x,y), (i,j), normal, frame) //按照上式计算的J
sum_of_weights += weight
sum_of_weighted_values += weight * frame(i,j)
filtered[x][y] = sum_of_weighted_values / sum_of_weights;
return filtered;
原始噪声图像:
联合双边降噪结果:
更多降噪技术 A-Trous Wavelet
在联合双边滤波的基础上,有很多更快更先进的滤波算法。这里对 Edge-Avoiding À-TrousWavelet Transform for fast Global Illumination Filtering进行了实现,这篇文章结合了A-trous算法和联合双边滤波,通过多趟较小的滤波器模拟大滤波核的卷积操作。效果如下,在我的电脑上速度达到了联合双边滤波的8-10倍。
补充知识: Deferred Hybrid Ray Tracing
Deferred Shading
联合双边滤波需要G-buffer中的信息,G-buffer是Deferred Shading中的一个概念。
一般而言,最简单的Shading方式是Forward Shading,它的伪代码可以这么写:
1. Vertex Shading
2. Rasterization
for each triangle:
for each pixel in triangle:
if pass depth test:
3. fragment shading
color = fragment_shader(...); // fragment_shader需要做 1)片元属性插值 2)Lighting着色
frame_buffer[pixid] = color;
Forward Shading有很明显的缺点:太慢了,调用了太多次fragment_shader,其中大部分调用是不必要的,因为会被后续更靠前的fragment覆盖,一个最极端的例子就是从场景最后方的三角形开始往前渲染。
为了解决这个问题,Deferred Shading出现了,它的伪代码可以这么表示
1. Vertex Shading
2. Rasterization
for each triangle:
for each pixel in triangle:
if pass depth test:
3. write to G-buffer
normal, position, albedo, ... = fragment_geometry_shader(...);
g_buffer_normal[pixid] = normal;
g_buffer_position[pixid] = position;
g_buffer_albedo[pixid] = albedo;
3. Deferred Shading
for each pixel:
frame_buffer[pixid] = fragment_lighting_shader(
g_buffer_normal[pixid],
g_buffer_position[pixid],
g_buffer_albedo[pixid],
...
);
其核心思想是将昂贵的光照计算移到后面,使得 光照计算的次数 与 场景中的物体个数无关,只与像素个数有关。
更多Deferred Shading的知识可以参考 [1] [2] [3]。
Hybrid Rendering
Path Tracing算法是一种纯粹的蒙塔卡罗光线追踪算法,和光栅化可以说是没有任何关系;但由上文可知,联合双边滤波降噪所需的G-Buffer是光栅化的产物,光线追踪中为什么又有光栅化呢?
现在,为了尽量加快速度,工业界常用的算法并不是纯粹的光线追踪算法,而是一种混合了光栅化和光线追踪的算法,Hybrid Rendering。
Hybrid Rendering 会先进行一遍光栅化得到G-buffer,再从G-buffer的每个像素出发,进行光线追踪,整个过程如下图所示:
Acknowledgement
这篇文章部分内容来源于GAMES202 闫令琪 ,文中的部分截图来自于课中使用的幻灯片,非常感谢闫老师深入浅出的课程以及实现框架的助教们。