【PathTracing】虚幻四中针对体素进行优化的基于SV(Voxel)GF的实时光线追踪

↘锁芯ラ 提交于 2020-05-03 16:31:44

写在前面:

本文将分享我在UE4中使用蓝图复现SVGF[1](的Voxel改版)的流程。

本文将首次放出项目(可以算是开源,写得不好还请见谅)。而且不会有太多的公式,涉及的方面虽然很多但是其实在我之前的文章里都有提到。只描述这个项目是怎么用蓝图来实现的。

项目包含:

一个全蓝图和材质的光追器(包括BRDF,Pathtracing,体素求交),

一个体素化函数库(目前有点Bug但是基本不影响使用),

一个体素操作函数库

一个蓝图和材质实时去噪器。


没有额外的C++代码,全文基于蓝图。

项目写成不容易,能多点赞请多点赞。

PS:本人准备以后转到b站,在白嫖(不是)之前,关注我的b站账号并三连吧~(我会尽量更新视频/教程),项目链接也会放在b站的视频下方:

【虚幻4】基于CBR和SVGF的实时体素光线追踪_哔哩哔哩 (゜-゜)つロ 干杯~-bilibiliwww.bilibili.com图标


时间隔得比较久,实现及描述上可能会有疏漏,还请各位斧正。

目录

一、管线概述

1.GBuffer(VoxelBuffer)

2.Raytrace

3.Upsampling

4.Atrous Filtering

5.Temporal AA

6.其他(包括数据的准备)

1.Pingpong

2.体素

二、棋盘格与Upsampling

1.棋盘格

2.Upsampling

三、SV(Voxel)GF

1.Atrous Filter

2.Feature Map

3.Temporal Filter


一、管线概述

由于全程使用蓝图,效率不高,因此对原来SVGF的管线进行一点阉割,并且加上和体素有关的特性。本人在文中尽量采用理想情况(也就是用其他图形API可以实现的情况)来进行描述,并且在蓝图中最大限度地实现。

关于名称,因为有很多名词是英文的,不太好翻译。

本文里FeatureMap基本等于GBuffer或者VoxelBuffer。

Atrous的头上应该有个音标符号但是为了方便均不写出来。


有阉割的部分可能包括:

1.数据精度(蓝图里不好控制)

2.去掉方差估计

3.减少Atrous的pass次数

4.去掉部分GBuffer


Pipeline以及其Buffer占大小(均为理想状态)如下:

1.法线及粗糙度及金属度及高光及颜色及体素在法线轴上的高度坐标

Normal(3 bits)

Roughness(3bits)

Metallic(3bits)

Specular(3bits)

BaseColor(3bits x 3 = 9bits)

VoxelHeightOnNormalAxis(任意)

FeatureMap的显示,颜色没有意义,因为Encode了

法线及粗糙度及金属度及高光及颜色压缩至RB通道(9bits),体素世界坐标(投影到平面)放在G通道

这里考虑到几个问题,一是UE的贴图格式在蓝图中不好控制,不能用uint(当然浮点数转uint也会出现各种意想不到的错误,因为中间有一个DrawMaterial的Pass),综上考虑只用了9Bits的数据每通道,为了留下Exponent的尾巴,防止错误。而且这里有个优化方法,可以把材质属性变成材质ID,在着色的Pass再进行读取体素的材质,但是在蓝图的管线里较为麻烦,因此还是把各种属性当成FeatureMap存在第一个Buffer里

(不要在意这些奇怪的位数,都是为了蓝图能够正常运行做出的妥协。。。)

2.Raytrace(RGBA16)

Raytrace采用了棋盘格的渲染方式,路径追踪时对体素求交使用了八叉树加速的结构,得到一个带孔的1/4spp的光追图像

3.Upsampling(RGBA16)

将半分辨率的1/4spp的光追图像向上采样至全分辨率的图像(在后面将会详细描述)

4.Atrous Filtering(RGBA16)

利用GBuffer和FeatureMap的信息,对向上采样过的图像进行滤波多次。每一次扩大Atrous的采样的范围,同时扩大Atrous的孔径。

本文的项目把最后一次Atrous Filter放在了最后显示的Pass中,为了节省一下带宽。

5.Temporal AA

和UE的做法基本一致,在YUV空间进行Clamp,具有FireFly的效果,而且能够叠加上一帧的有效信息,同时使用GBuffer和FeatureMap的信息对无效信息进行过滤,防止拖尾。

但是没有做Reprojection,因为蓝图里不太好做。

考虑到没有RW的Texture,因此很多步骤需要用到Pingpong的方法进行贴图的读取和写入,在文章里讲述会比较复杂,具体的还请看实际的项目。

6.其他(包括数据的准备)

需要Pingpong的Buffer:

FeatureMap

Raytrace Buffer

Atrous Buffer

Final Buffer(到这里可以输出了)

体素八叉树的生成与求交:

可以参考本人之前的文章:[2]

https://zhuanlan.zhihu.com/p/55964499

但是在这个项目实际实现的时候使用了另外一种方案,采用了Step优先的方案,可以参考我的ShaderToy,因为细节比较多就不在本文里赘述:[3]

https://www.shadertoy.com/view/wtKXWV

八叉树的结构较为复杂,为了放进一张2D贴图里,本文将体素的Mipmap pyramid[4]

体素的Mipmap示例图比较难画,用一张2D的代替一下

从4D数据编码到2D的贴图中,基本思路是先把4D的Index转换为1D的Index,最后1D的Index转换为2D的Index,并且写入Texture,具体实现可以参考项目中的运用[5]

二、棋盘格与Upsampling

1.棋盘格

棋盘格渲染在游戏中已经大规模应用,因此基本的方法不在本文中赘述。

本文中的实现需要达到的目标是:

1.易实现2.能够确实降低消耗3.不带来过多的画质损耗

因此在管线中,由于无法使用蓝图控制MSAA,以及无法操控Clipspace后的像素位置(实际上是可以的,比较麻烦),以及方便控制流程,以及省下两张Buffer(等原因),本文的所谓棋盘格是使用一张全分辨率的Buffer,并且每隔一帧往里面只塞入1/4的数据,也就是只运行1/4的像素,其他的像素在跑光追之前都会被return掉。在多线程的运算中,这种操作的优化实际上不能起到4倍以上的运行速度,但是依然可以达到巨量的速度提升。

具体实现如下,将像素分为2x2一组:

渲染顺序为红->绿->蓝->黄

在一帧中,除了当前渲染的像素均为黑色。

2.Upsampling

因为使用了不一样的棋盘格,这里也用了不一样的向上采样(不一定完全最优,还请斧正)

定义 Avg 是颜色的平均值

Filter一次,全分辨率 Filter一次,半分辨率

(仅仅经过一次Filter,肉眼已经难以看出差别)

将时间域分为4帧一组:

第1帧:

有效信息只有红色部分,考虑到周围的其他红色区域,

Color_{蓝块0} = Avg(Color_{红0},Color_{红1})

Color_{黄块0} = Avg(Color_{红0},Color_{红2})

Color_{绿块0} = Avg(Color_{红0},Color_{红3})

第2帧:

此时绿色被填充

Color_{蓝块2} = Avg(Color_{红2},Color_{红3},Color_{绿0},Color_{绿2})

Color_{黄块1} = Avg(Color_{红1},Color_{红3},Color_{绿0},Color_{绿1})

可以注意到,对于已经渲染过的一个像素是不进行任何操作的,因为认为他是真实值,而其他的只是估计值。

第3帧和第4帧以此类推,不在此赘述,可以参考项目中的Shader里的实现[6]

三、SV(Voxel)GF

1.Atrous Filter

这里简单描述一下,Atrous是一种带孔卷积,它比一般的卷积有更广的感受野,直观表现为在屏幕空间中,一个像素能通过Atrous得知更多的信息。

Atrous在神经网格中的应用

Atrous被广泛运用于神经网格中,在实时计算机图形学里也有运用,如SVGF的前身Edge-Avoiding A-Trous Wavelet Transform for fast Global Illumination Filtering[7] 就使用了该算法。

在实时运算里,可以通过对孔径的大小进行Jitter或者卡点(使其在四个像素中心)来获取更多的有效信息(通常是连续的表面,因为GBuffer无法简单地通过双线性插值来获取插值后的信息),不过尽管如此,在Atrous上面可以下功夫的地方还是很多。本文的实现里会采用非整数的孔径来降低Artifact。

5+1次Filter,1spp

除了引入FeatureMap之外,以及去掉了方差估算,和SVGF的算法基本保持一致。

2.Feature Map

实际上就是GBuffer。这里有一个体素独有的特征,就是体素面的中心在该面的法线的轴上的坐标高度。通过这个高度可以轻易通过卷积来区分不同的体素,从而不会使体素都糊成一片。其中最终记录的高度坐标为:

P = abs(WorldPosition \cdot SurfaceNormal)

这里会引入一个问题,对于每一个轴,都会有两个平面(正面反面)共用一个坐标系。但是由于渲染的时候,受限于投影矩阵,不可能使那两个面同时出现在一个屏幕之中,因此可以被忽略。但是还是会有极小概率的不同轴之间的碰撞,但是由于在Atrous的Edge Avoiding中,相互垂直的法线的面基本不互相影响,因此也可以被忽略。

在Atrous以及TemporalFilter中,FeatureMap用来区分不同的体素,不一样的体素之间将不会进行插值。直观地感受便是

不仅仅是Edge Avoiding,还有像素之间的Avoiding

由于没有做方差的估算,本文的项目引入一个滤波衰减系数 \alpha = e^{-i*\sigma} ,其中 i 是当前帧, \sigma 是衰减系数,来模拟随着时间的累加,方差的减少,需要滤波的地方也随着减少的情况。

3.Temporal Filter

参考UE4的Temporal Filter。

在Atrous Filter和Temporal Filter进行的过程中,对于FeatureMap不一样的体素信息将会被丢弃,而不是进行插值。因此直观感受便是在渲染一个个的体素面,只不过是基于屏幕空间的。

金属与非金属之间不会有插值,保留了材质的边界

总结及可以改进的地方

可以改进的地方有很多,实际上有些阉割笔者在Vulkan中实现的时候是不存在的,考虑到了蓝图这种限制性的编程语言的局限性而做出的调整。目前还没有找到一种最快的八叉树求交方式,仍然在尝试各种算法。还请各位斧正。

结尾

在蓝图中实现这个项目不容易,前前后后从Vulkan移植到蓝图里花了大概有一个多月的时间,中途对整个管线在蓝图的适应性进行了多次调整,才达到目前的效果。权当一个蓝图的小玩具吧。

有很多人对蓝图表示厌恶,觉得用C++比蓝图要高贵许多。但是本人一直不这么认为。蓝图这种限制性的编程能给本人带来不一样的满足。对笔者而言,蓝图是艺术,不是技术。

最近把之前的视频都转到了b站,并且开了一个独立游戏的坑,求大伙们关注一下,项目链接也会放在b站的视频下方:

【虚幻4】基于CBR和SVGF的实时体素光线追踪_哔哩哔哩 (゜-゜)つロ 干杯~-bilibiliwww.bilibili.com图标

求关注求关注求关注~!!~!(不准白嫖

感谢阅读~

全文完

最后放一点渲染的图,均为实时渲染,1/4spp+Temporal Filter

参考

  1. ^SVGF https://cg.ivd.kit.edu/publications/2017/svgf/svgf_preprint.pdf
  2. ^之前的体素文章 https://zhuanlan.zhihu.com/p/55964499
  3. ^体素求交的ShaderToy https://www.shadertoy.com/view/wtKXWV
  4. ^GigaVoxel里的Mipmap Pyramid https://maverick.inria.fr/Publications/2013/Gue13/index.php
  5. ^体素的编码在VoxelCommon的FC_GenerateMipMap里面
  6. ^Upsampling的shader在MAT_UpSampling中
  7. ^Edge-Avoiding A-Trous Wavelet Transform for fast Global Illumination Filtering https://www.uni-ulm.de/fileadmin/website_uni_ulm/iui.inst.100/institut/Papers/atrousGIfilter.pdf
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!