前言
这周无疑是属于《黑神话:悟空》的一周,不过由于上周已经写了一篇关于魔兽世界的流水账了,这周还是回头更技术文章——而且话题是有关虚幻5的lumen的,所以也算和《黑神话:悟空》有点关系。
近年来虚幻引擎的高质量渲染方面确实已经把Unity甩开很远了。之前更过两篇是介绍引擎整体提升的,其中很重要的一部分就是网格管线的设计。而今天介绍的这篇主要是介绍一个中间层面的技术——光照数据缓存(Radiance Caching)。
高清引擎中的光照计算是遵循物理法则和公式的,整体思路没有脱离辐射度量学(Radiometry)的范围。简单来说它研究的就是:光能量的传递过程中微表面吸收和发出的能量——由于发射出的能量是球体范围的,因此基于距离的衰减也可以表达为“单位面积可以接受到的光能量的分布变少了”;材质的性质也可以表达为不透明介质对能量吸收和发射的程度(散射介质是另一个复杂话题了,比如之前介绍的云)。最终整体的能量获得可以表达为对微表面能量的积分,这套体系中发出的能量就被称为Radiance,接收的能量则是Irradiance——前者常被翻译为辐射度,后者常被翻译为辐照度。(这两个词后面尽量保留英文原文,以避免过于类似而混淆)
我个人对此的了解虽然也仅停留在Games101和202课程的程度,不过我觉得以此为基础概念性的来理解工业化过程中的一些光照渲染设计已经足够了。所以这里提到的缓存其实是对中间状态光能量的缓存。
为什么要做这个缓存其实不难理解——为了以空间换精度的方式进一步提高画质。一起研学这篇分享主要是理解“如何缓存”的过程。
原文档是以PPT为主,每页下面搭配了一行左右的解说稿。本文还是以翻译原文PPT页及解说稿为主,打星号的部分则是我个人的补充。由于篇幅原因还是拆分成上下两篇(反正留点业余时间玩黑猴),这是其中的上篇。
1 设计背景介绍——Background
在光照渲染中,最终像素点的光照可以通过对接收的光辐射代入BRDF,并积分得出。
*BRDF——双向反射分布函数(Bidirectional Reflectance Distribution Function)是用来定义给定入射方向上的Irradiance如何影响给定出射方向上的Radiance。
*例如表面越光滑,越接近镜面反射,则出射能量越集中在与入射对称的角度;越粗糙则越接近漫反射,出射能量会均匀分布在半球甚至球形范围的任意角度;介于两者之间的材质有时候也可以用“各项异性”或“各项同性”以及更多其它的复杂模型来表达。
- 这种积分一般是通过蒙特卡洛积分方法,以离散的样本来尽量近似估计微表面接收的光辐射。
- 对某一个方向的光辐射的估计也是需要光线追踪的(因为要考虑多次弹射的情况)。
即使有着两层BVH等加速结构,光线追踪仍然是相对较慢的。
*BVH之前有文章介绍过,这里不展开了。
- 通常来说每像素只能接收2帧发出一根射线(用分时缓冲方式和插值等方式来提高精度)。
- 但高质量的全局光照需要(每像素每帧)上百根射线才有比较好的采样效果。
*图中展示了室外100根射线的效果,而室内则需要更多射线。
已有的实时渲染方案——辐照度场
在实时渲染领域,之前的研究主要分成了2类。其中一类是辐照度场(Irradiance Fields)。
- 从一小组探针中翻出射线——通过Tatarchuk 2012提供的世界空间网格的方案来分布。
- 预计算Irradiance
- (以半分辨率甚至更低分辨率来渲染光照)通过插值的方式来达成全分辨率精度
- 探针剔除以避免漏光(主要是排除一些不应该受光照的采样位置)
辐照度场的问题
- (由于采样步长等原因)漏光或过度剔除
- 探针分布不能一概而论
- 光照更新较慢(不能适应瞬间的变化)
- 这套方案特有的显得很“平”的观感——比起空间光探针方案,遮挡物附近的Irradiance相对更高频一些(整体会更亮)
*简单来说这套方案就是,在采样数有限的情况下,想办法通过更好的近似光场来减少质量损失。所谓“场”可以理解成是空间中一些有规律的点。
已有的实时渲染方案——屏幕空间降噪
另一类方案是屏幕空间降噪(Screen Space Denoiser)。
- 逐像素射线追踪——(采样)cosine分布、每像素1射线
- 通过空间数据和分时重用的方式来降噪(后面介绍了2种对应方案及出处)
*基于分时的方案在Games202系列课中也有介绍,有兴趣的可以去看看。这套方案简单来说就是通过空间插值和分帧缓冲及插值的方式,来保证1帧内的采样数符合性能需求。
屏幕空间降噪的问题
即使通过固定采样率运行了几帧,得到的输入数据还是有很多噪点(需要进一步进行图像降噪)。
Noise is not constant. Near a bright light source there’s less noise, and further from a bright light source the noise increases as fewer of the rays actually hit that light source.
噪点的形式也不是一成不变的。靠近亮光源处,噪点就较少;远离亮光源处早点就增加,因为会有更少的采样射线命中光源。
*其实还有光源大小、数量等等很多因素。针对如何更高效地命中光源也有很多优化设计,本文介绍的也是其中之一。
2 方案概述——Our Approach
Instead of tracing from every single pixel on the screen, we bundle up our rays and we trace from a much smaller set of pixels. This is effectively Screen Space Radiance Caching.
相比于从每个像素发出射线,我们从一个更小的像素点集发出射线——这是屏幕空间光照缓存提升性能的核心(减少一次运算的像素点数量)。
图中是以较低采样率接收光照的结果,这也能有不错的基础效果——因为入射的radiance是连续的,尽管几何体的法线不是连续的。
尽管光照采样是低分辨率的,我们仍在带入BRDF时使用全分辨率,这样也能得到细致的间接光照效果。
*左边下面一张仅展示了GBuffer中的法线缓冲,这仅仅是示意。计算材质的BRDF一般还至少需要表面颜色(Albedo)、粗糙度(Roughness 或高光度 Specular)之类的信息。
在光照缓冲探针之间,我们采用了一定的过滤(filter)方式——后续会介绍到这样做的一些优点。
在重点方向我们会有更多采样射线。我们有一定的估计入射光来源的策略,会针对光源方向发出更多射线(后面提到的重要性采样)。
针对远距离光源我们有一套独立的采样方案被称为世界空间光照缓存(World Space Radiance Cache),这能为我们带来更稳定的远距离光照表现。
*这部分介绍会留在下半部分。
*图中展示了两种算法得到的结果差距,左边是每像素2个射线的屏幕空间降噪算法;右边是本文中的光照缓存算法。可以看到右侧的效果在一定的屏幕空间(或图像)降噪策略后是可以接收的,而左侧则完全不行。
*屏幕空间和图像空间的区别在于,屏幕空间还可以使用GBuffer中的数据作为插值或过滤的依据。
3 管线集成
*这部分开始介绍了这个方案的工业化管线集成及一些要处理的细节。
最终集成到管线中的核心是屏幕空间光照缓存(Screen Space Radiance Cache),以及作为(远距离)后备选择的世界空间光照缓存(World Space Radiance Cache)。
两者都是在相对最终分辨率较低的像素分辨率上执行,集成到其它部分时则是全分辨率——例如法线计算、插值和集成、基于分时的过滤器等(如图)。
接下来介绍一些屏幕空间光照缓存的具体步骤:
- 在GBuffer中放置探针
- 生成射线——追踪——在探针空间中执行过滤
屏幕探针结构
屏幕空间探针(的采样结果)被排布成一个图集的形式,每一个探针采用八面体展开的布局。
每个探针是8x8的分辨率,并采用64根射线追踪。
八面体的布局中提供了标准化分布的世界空间方向。
一种八面体的示意图
It’s important that we use World Space directions here because it means that neighbors have matching directions, and we can find a matching direction in a neighbor very quickly, and I’ll talk about why that’s important later.
很重要的是我们采用世界空间的方向,这意味着相邻的探针也有对应的该方向,并且我们可以很快查找到相邻的同方向的结果——后续会介绍为何这个结构如此重要。
After we do our tracing from the probes, we have Radiance and HitDistance and we store those in the atlas for further processing.
在从探针进行射线追踪后,我们可以得到Radiance(这里是接收到的光照能量)和HitDistance (射线命中距离)——将它们存储到缓冲图集中以作为后续过程的数据。
屏幕探针放置
在实际放置屏幕探针时,我们采用统一化的网格——每16像素放置一个探针。
可以看到图中橙色部分是初步执行后插值失败的像素。(*插值失败的依据原文没有给出,应该是基于一些容差维度)
之后我们把格子的分辨率加倍,并在之前插值失败的格子中间加入一个新探针,再执行插值测试。
继续执行直到余留很少的插值失败的像素。
*这个步骤的核心思想参考了被称为 Hierarchical Refinement (层级精进)的方案。
屏幕探针放置
最终在像素层级我们采用泛滥填充(Flood fill)的方式处理最后的少量像素——而不是还为之分配采样探针。
自适应采样
这个算法是一个自适应的采样过程,为了能保证它总能在实时渲染中有效,我们需要设置一个上限(以避免极端情况过度消耗性能)。
与此同时,我们也不希望把(不同分辨率放置的)探针分开处理,因此我们把自适应的探针排列在图集的底部(例如8像素格子的就排在16像素格子的下方,如图)。当我们达到图集空间的上限时,我们无法放置新的探针,则相对的就采用泛滥式填充算法以处理仍然插值失败的像素点。
屏幕探针抖动
由于我们仅是每隔N个像素放置了一个探针,因此我们也需要分帧抖动放置探针的网格(进行微小的偏移),以及八面体内射线的方向。
我们的探针直接分布在屏幕像素上(而不是基于世界空间换算的更小单位),以确保没有漏光问题或是像素和探针之间的色差。
不过由于探针是放置在屏幕空间的,我们必须通过一个分时过滤器(temporal filter)以隐藏遮挡计算上的差异(明暗边缘不能有变化)。
后续我也会提到一些这套方案的其它副作用。
插值
要通过屏幕空间光照缓存通过插值计算出屏幕像素(光照值),我们以像素平面的距离作为光照探针的权重项——像素平面通过像素点的法线和位置推导得出。
这能避免前景的射线运算错误地“漏光”至背景层。
插值
在从光照缓存中插值的偏移量中我们也做了抖动处理——不过抖动的范围还是被限制在原始像素的同一平面上——确保抖动不会导致插值失败是很重要的。
抖动能使探针之间的空间上的光照差异相对被分散开一些,这也有助于提高最终光照的分时稳定性——通过扩展TAA(Temporal Anti Aliasing 分时抗锯齿)的邻接裁剪步骤。
管线验证
*在管线组装起来后,得到的结果与离线路径追踪结果一致。
但是当降低到我们的性能预算(1/2射线每像素)时,在室内空间的噪点会比较多。
*后面的部分也主要是关于一些进一步的改进和补充。
4 重要性采样
*在射线数量给定的前提下,这个部分解释了如何提高检测的预判准确度。
回到渲染方程中的蒙特卡洛部分,我们想要将射线以更符合函数中积分的分布方式来分配。
但是如何预估呢?尤其考虑到是入射光线(能量、方向)是首要需要得出的项。
在屏幕空间光照缓存中,我们可以通过查找上一帧的方式来对光照有一个很好的预估。
.相比于执行昂贵的屏幕空间查找方案,我们可以将当前屏幕位置映射到上一帧(高清渲染中的常见方案,基于空间变换的插值预判),并对相邻4个探针结果做平均。
光照缓存数据中的射线已经基于位置和方向进行了索引化,而这是使查找能很快速的原因。
对于重映射失败的点,例如(上一帧)屏幕外或被遮挡的点,我们有世界空间的光照缓存作为备选(相应的是一种精度较低的补充方案)。
本页底部右侧的图片展示了八面体数据布局中的入射光线数据,对于这个探针来说,光线主要从三个方向而来。
对于BRDF来说,我们可以使用插值后的屏幕光照探针数据来计算全部像素的BRDF——其它项都能从GBuffer中得出(结合光线方向)。
对于放置在平的墙面上的探针来说,大约一半的射线会得到0值的BRDF(背面),我们也不需要这些方向的射线——由此我们可以将它们重分配到更重要的方向。
不过比起单独做每一项(光照、BRDF)的重要性采样,更好的方式是可以对整体的乘积做重要性采样。
结构化重要性采样
这就是引入结构化的重要性采样的原因。结构化的重要性采样分配了相对少量的层级结构的区域——基于概率密度函数(Probability Density Function)。
这实现了很好的全局分层效果,不过本页提到的采样点放置算法需要离线预处理一些数据。尽管如此,我们仍可以采用这种“层级结构+阈值”的思想。
*关于概率密度函数可以去看看Games101。
并且我们发现这种方式能很好的与我们的八面体探针mip四叉树完美的映射。
On the left we have calculated the product between the incoming lighting and the BRDF, and on the right, we’ve subdivided Octahedral texels where the PDF was highest.
图中左侧的是计算好的入射光和BRDF的乘积,右侧则是在高PDF(这里是概率密度函数的缩写)部分再分割后的八面体栅格。
集成到管线
为了将它集成到我们的八面体探针管线中,我们需要为射线追踪步骤加入一些中间存储和计算步骤,使得每个追踪能各自计算射线的方向。
在进行追踪并计算radiance之后,我们需要将非统一化网格得到的radiance与统一化探针的布局进行最终整合(如图)。
射线生成算法
这里是一个实际的计算着色器(compute shader)中实现结构化重要性采样的射线生成算法:
- 首先,我们对于每个八面体栅格计算光照和BRDF的乘积。
- 我们从统一分布的射线方向开始,以确保一开始追踪时线程组所有线程都能处于繁忙状态。
- 之后我们基于射线的PDF进行排序。对于每3个低于剔除阈值的PDF数据,我们将其去除(refine 这里指释放出一些射线供更需要精度的地方)并转而超采样(分成4个)的高PDF射线方向。
可以看到图中左侧的一部分BRDF系数是0,我们剔除了这些射线并把这些数量重新分配到光照和BRDF值都最高的一些方向(超采样分成了更小的格子,以提升精度)。
图中示意了这一算法在世界空间生效的过程。
左侧图中是统一化发出的射线方向,一半都浪费在与墙壁相交且与最终光照结果无关。
右侧是重分配这些射线到重要的光照方向后的结果(白线)。
从图中的视图模式已经能看出这样做带来的明显噪声减少。
改进方向
我们还可以对这个算法做一些改进。
相比于剔除光照中PDF较低的射线,我们仅剔除BRDF较低的射线——这是因为本身光照的PDF这里就是一个估计值,且受影响于噪声。
(某一帧中)我们可能错失了上一帧中的某个微小光源,但我们仍应该追踪那个方向。(*这其实就是基于离散方向射线采样的其中一个问题)
BRDF相对而言就是更精确的,它的数据都来自GBuffer(全分辨率),因此是无噪声的。
在学习了空间过滤技术(spatial filter 下篇中会提到)后我们能更激进地进行剔除。我们能剔除BRDF值远大于0的值,减少它们的权重以把射线分配到更多较暗的角落(以提高精度)。
这给我们带来了另一种把不重要的射线分配到重要方向的方案。
*图中展示了结果对比——每帧每像素1/2射线。
这里是这套重要性采样方案的简单总结:
- 我们通过上一帧的光照数据指导这一帧的射线方向,并以远距离光照指导这一帧的射线方向(下一篇介绍的世界空间光照缓冲)。
- 我们把射线集成为探针(包含前面提到的那种数据结构),这使我们能实现更智能化更高效的采样过程。
结语
下一篇会包含Spatial Filtering 、World Space Radiance Cache、Full Resolution Steps等几个部分,以及集成后的性能表现。
这里面展示的一些工业化细节虽然是以经验性为主的,但也是很重要的。其实从卡马克那个时候开始,尖端的游戏引擎要考虑的一直是如何优化管线结构,让本来实时跑不动的图形学理论或离线渲染方案能运行起来。
回到光照缓存这个功能来说,逐像素的光追采样就是质量不如这个方案的,而这个方案中的1/16的分辨率、八面体探针(的图集)、探针8x8的分辨率(以及基于这个分辨率的64根射线)、重要性采样中的中间层(光照与BRDF的乘积)等——这些算法和数据设计才是虚幻引擎的开发者们在特定的硬件条件下通过实验摸索出的性能和效果的平衡点——这个过程是无关物理理论的,它是属于引擎独有的“工业化”过程。
而由于硬件情况一直在变化,因此可以说引擎的“工业化”是一个永不过时的过程。任何时候要打造最顶尖的高质量画面,都需要很多引擎开发者不懈的努力。不过总的来说,由于对分时策略比较依赖,因此各种高画质游戏在画面激烈变化时都不可避免有一个从“糊”到清晰的过程(这也是有时候用动态模糊遮一下的原因)。
下周会更这篇分享的后半部分。
最后是资料链接:
Radiance Caching for Real-Time Global Illumination 的PPT文件