前言
这周无疑是属于《黑神话:悟空》的一周。在这之前,使用虚幻5的规模较大的游戏项目似乎还是《堕落之主》《最终幻想16》等——这些游戏当时也面临了一定的性能上的取舍和挑战,后者直到最近上PC还是有严重的性能问题。
总的来说,这几年真正的第三方敢于用虚幻5的lumen新特性去做高画质高精度的3A游戏的还是不多,可见虽然这个引擎能达到的上限高,但使用门槛也是一点都不低——更不要说做扩展开发了。不过我认为,这就和这个世界上的大多数有技术含量的事情一样,克服了门槛和实现了一定规模,就可能孵化出《黑神话:悟空》这样的作品。而且,Epic作为引擎的提供者,肯定是乐见这样的高质量产品越来越多的。
这篇文章是两篇文章的下篇,上篇中介绍了这个技术方案的提出及管线架构,以及部分核心结构和采样方式的设计。这篇会继续介绍剩余的技术细节。
原文档是以PPT为主,每页下面搭配了一行左右的解说稿。本文还是以翻译原文PPT页及解说稿为主,打星号的部分则是我个人的补充。Radiance 和Irradiance两个词也还是尽量保持英文原文。
1 空间过滤
*其实Filter翻译成过滤感觉也是缺乏特别准确的对应,狭义的过滤感觉可能是“按一定规则排除一部分数据”,而广义的Filter可以理解成“输入一些东西,然后按一定规则输出”,例如信号系统、图像系统等。
在光照缓存空间过滤
相比于在屏幕空间过滤,我们通过光照缓存(Radiance Cache 上篇介绍过)来执行过滤——因此我们从光照缓存的图集中读取,同时也写入数据。
由于屏幕空间的光照缓存是降低采样的,这使我们能实现性能较好的大范围空间过滤器。
探针空间3x3的核的效果相当于相同屏幕尺寸的屏幕空间中48x48的核(“核”指需要输入多少个数据源,以及对应的计算方式)。
当过滤入射光照时,我们可以忽略法线的区别,因为入射光照不受接收者的法线影响——因此我们仅需要深度作为权重。
从相邻格收集光照数据
当从相邻格收集数据,我们可以快速查找到与收集方向匹配的radiance——因为在光照缓存中,射线已经被基于位置和方向做了索引(上一篇提到的八面体结构)。
但我们仍需要一个启发式的结构,以排除那些可能导致漏光的相邻格的radiance(通过定义误差权重 Error weighting)。
我们可以通过重映射相邻射线命中的位置,计算该点和我们目标方向的夹角,以排除那些夹角过大的radiance来源。
这样能高效地过滤远距离的光照,同时保持(正确的)局部阴影。
图中展示了空间过滤器的效果,我们可以在不额外增加射线的情况下,实现在平面表面上很好的降噪效果(右侧墙面)。
但我们为此损失了部分毛巾折叠部分和贴墙部分的邻接阴影(影子变淡了)。
保持邻接阴影
这是由于基于角度误差的容差值方式(不完全适用于远距离光源)导致的。
远距离光源没有视差而且(在符合角度时)从来不被排除,导致了漏光。
我们找到的解决方案是:在重映射位置前,将相邻的命中距离收紧至(clamp 有点对齐至的意思)当前格的命中距离。
*这里简单解释一些,邻接阴影的计算实际上也是在评估单位面积接收光能,接收的更多就更亮。虽然整个采样由于精度和近似一定程度上有偏差,但这里做的就是经验性地排除一些明显是“多出来”的能量计算。
这依然能带来很平滑的远距离光照效果。
同时我们也能够“找回”毛巾折叠处以及贴墙处的邻接阴影。
最终过滤效果比较
*图中展示了最终的空间过滤效果,左侧是过滤前,右侧是过滤后。
2 世界空间光照缓存
*相比于屏幕空间缓存直接缓存探针采样光源的结果,世界空间缓存最大的区别是探针自身的光照采样方式和精度不同。
课题:远距离光照
如果仅仅通过屏幕空间探针来追踪光照缓存,我们会面临一个问题就是在远距离光照上噪声过大。
较小的光亮物的采样噪声随着距离增加而变大——与此同时,长距离不连贯的采样又非常慢,是性能上无法承受的。
Distant lighting is changing very slowly, both in time and in space, which is an opportunity to cache across frames, and an opportunity to reuse those distant rays for neighboring Screen Probes.
远距离光源在时间和空间上通常都变化得很缓慢,因此是可能被跨帧缓存(而不失真)的——也有可能重用相邻屏幕探针的远距离射线。
*仅看这里原文的措辞不太清晰,可以结合后面实际方案设计来看。
解决方案:对远距离光照分开采样
我们采用的解决方案是,对远距离光照使用一套单独的采样方案。
我们使用的世界空间光照缓存(World Space Radiance Cache)参照了James McLaren的‘The Technology of the Tomorrow Children’方案。(*这里是2015的一篇SIG讲座分享,The Tomorrow Children: Lighting and Mining with Voxels)
这套世界空间光照缓存方案能给我们带来稳定的误差——这使它能易于被隐藏(通过过滤)。
*粗略去看了下那篇分享,大致上远距离光照探针采样就是采用的以前介绍过的Voxel Cone Tracing(锥体体素采样),那么对一定锥体范围内的多光源就可以基于插值计算方式来采样。虽然计算细节不能百分百确定,但肯定是采用的一种覆盖范围更广但精度相对更低的采样方式,并用分帧的方式降低重新计算时的压力。
管线集成
我们在需要从世界空间探针插值的屏幕探针位置周围放置探针。
然后我们通过世界空间探针来追踪射线以计算入射光照,并对光照做插值以解决屏幕探针无法很好处理远距离光照的问题。
连接射线
We’re effectively connecting two rays, actually multiple World Probe rays because of interpolation, but we can just look at one World Probe ray and consider how they connect.
我们能高效地连接两条射线——这实际上是多条世界空间探针射线(由于需要计算插值),不过我们可以仅仅考察一条世界空间探针射线是如何与屏幕探针射线进行连接的。
避免“自光照”问题
在进行世界空间探针追踪时,首先我们需要加入一个偏移值(offset)以确保避开插值的覆盖空间。
Any positions within the interpolation footprint are going to use this World Probe ray for distant lighting and must not pick up their own lighting.
任何位于插值覆盖空间的位置都将使用世界空间探针射线对远距离光照进行采样(而不是采样到它们自身的光照)。
*这里主要是解释如何避免采样到光照物自身的光照能量,会产生“自光照”的原因也是由于这种世界空间的采样方式是相对低精度的。
连接射线
When we trace the Screen Probe ray, and connect it with the World Probe ray, we need to make sure the Screen Probe ray covers the interpolation footprint, and also the distance that the World Probe ray skipped.
当采样屏幕探针射线,并于世界空间探针射线连接时,我们需要确保屏幕探针射线位于插值覆盖区内,并基于距离阈值得出世界空间射线需要跳过的距离(*即避开插值覆盖空间的半径,从一定距离开始才采样远距离光照数据)。
课题:漏光
不过在前述的内容都实现后,我们会遇到“漏光”问题。
这是由于两条射线是从不同原点发出的,尽管平行但它们是错开的。
图中的这条世界空间射线是应该被剔除的,但因为这种视差(parallax)的情况而保留了,就出现了漏光情况。
*这里parallax翻译成视差其实也不完全合适,整体来说是一种“虽然平行但是观测结果又不一致”的更广泛的概念。
解决方案:简单球面视差
We can solve this by using the World Space Probe ray whose origin best matches up with our Screen Probe ray, instead of the World Probe ray whose direction matches.
我们可以通过调整世界空间探针射线原点的位置,以更好匹配屏幕探针射线并解决漏光问题——而不是使用同一球面同方向的射线。
这可能带来一些方向上的误差,但消除了射线间的错开问题,避免了导致漏光的根源。
回顾前述,可以看到前面很多情况会有的漏光问题都被新的简单球面视差方案解决了。
稀疏覆盖
我们的世界空间光照缓存是稀疏的(sparse)。由于我们仅在需要作为屏幕探针插值补充的一些位置放置探针,因此我们将它们保存到以摄像机为中心的3D裁剪纹理(后文保持用clipmap这个词)中。
这些clipmap被间接存储到探针图集中(storing an indirection into the probe atlas)。
Clipmap在图集中的分布受屏幕尺寸(可视范围)约束,这一点的重要性在于:我们既不希望放置太多探针,也不希望放置太少。
图集
我们的八面体探针图集仍是存储Radiance和TraceDistance,就像之前屏幕空间一样——除了这次我们的每个探针采用了更高的分辨率(32X32),因为世界空间探针相对屏幕空间有着更少的数量。
放置和缓存
为改进放置策略,首先我们标记出后续要从clipmap中间结构中插值的位置。
对每一个标记的位置——各自都是一个世界空间探针,我们既可以重用上一帧的追踪结果,或者对于没有任何可重用的情况,我们可以在图集中为之分配一个新的探针,并追踪新的射线。
我们也重新追踪了一些(命中缓存的)重用的射线的子集,以便传播式地更新光照的变化。
*这里子集和传播式(propagate)都指向了一种不一次性全部更新的分时策略。
课题:较高的变量开销
目前为止描述的这套算法的问题是它会带来一些故障。
我们有着过高的变量开销,因而任何时候摄像机快速移动、或是绕过角落时,我们都需要显示大量未缓存的为止并追踪大量射线。(*一种有效的基于缓存的分时策略一般来说不能导致性能问题,否则就是不可用的)
不过我们可以为这种开销制定一个上限值——通过为全分辨率下的追踪数量指定一个预算上限的方式。
基于无法命中缓存而需要的额外追踪数,如果超出预算,仍会运行但需要在较低的分辨率下执行;基于更新光照信息而产生的额外追踪数,如果超出预算,则会被完全跳过。
这为我们提供了可控的最大开销,使整个方案得以实时运行。
重要性采样
就像屏幕探针,我们也可以(对世界空间探针)做重要性采样,只是这次我们没有对入射光线很好的估计方式了。我们仍然能基于BRDF做采样。
我们从屏幕探针来估算BRDF,之后将探针随机分配(dice up)到待追踪的tile(trace tile)中——我们需要追踪更高分辨率,因而无法负担排序并执行单独的射线,因而追踪以tile为最小单位执行。
我们基于和BRDF相对应的分辨率来生成trace tile,并在其上执行结构化的重要性采样。
我们只在靠近摄像机的位置对trace tile进行超采样,并且这种超采样能带来(每个世界空间探针)高效的4000个追踪数。
这为我们带来极为稳定的远距离光照——尤其是当与每帧1像素1射线的屏幕空间降噪(Screen Space Denoiser)方案相比。
探针之间的空间过滤
在追踪后我们仍可对光照缓存的探针之间做空间过滤。(*基本思路参照前面空间过滤一节)
这次的问题是我们无法预设探针之间的可见性状态——相邻的探针可能被放置在墙壁的另一侧。
避免漏光
Ideally we would like to re-trace the neighbor ray path through our probe’s depths, the depths that we stored off from ray tracing.
理想情况下我们希望通过存储的(可能把射线追踪隔开的)探针深度来判断,并重追踪相邻的射线路径。
This turns out to be too expensive, but we can solve most leaking by doing a single occlusion test to a position along the neighbor ray’s path.
这种方式开销过大了,不过大部分时候我们可以通过对相邻射线上位置的遮挡检测来解决漏光问题。
这种遮挡检测近乎是“免费”的,因为我们没有追踪任何新的射线——仅仅是重用了探针追踪过程中得到的深度值(进行比对)。
图中展示了世界空间缓存的结果。对于近处的2米采用的是屏幕空间光照缓存,更远处则都是采用的世界空间光照缓存。
分时稳定性改进
这一方案的最大好处是降低了基于屏幕空间分时采样(射线精度高但样本不足)的噪声情况,提高了采样稳定性。
我们也使用世界空间光照缓存来指导屏幕探针的重要性采样(上篇中也提到了)。
它也被用于前向渲染中的头发、间接光照等模块,并用于改进多次弹射算法的质量。
3 全分辨率下的集成步骤
*了解过渲染管线的对其中一些模块应该不陌生,这里的新事物主要是光照缓存和其它部分的集成方式。这里主要是降低精度采样后会面临的一些精度损失问题与弥补方式。
(管线中)剩余的流程是Bent Normal、插值、集成和分时过滤。
*这里Bent Normal相对来说是图形计算上的比较新的事物。UE中给出的说明如下:Distance Field AO produces a bent normal which is the direction of least occlusion.
*简单来说,它的方向指向法线半球内遮蔽最小的方向, 模长代表受遮蔽的程度。
最终集成
回到积分公式,现在我们已经计算出了入射光照(基于屏幕空间光照缓存,低分辨率下的),需要在全分辨率下与其它几何项做整合。
蒙特卡洛积分的噪声
一种减少采样噪声的方式是对BRDF做重要性采样来获得射线方向,之后采样我们的光照缓存、八面体图集以获得radiance。
由于这些采样实际上是不连续的,并且我们也无法承受太高的采样数,因此得到的初始结果是有噪声的。
我们可以对光照缓存使用mipmap——这也被称为可过滤的重要性采样(Filtered Importance Sampling),不过这会导致“自照亮”问题。这会(错误的)把背面半球的光照计算到正面半球的光照数据中。
将光照探针转换为三阶的球谐函数
相应地对于更高质量我们使用球谐函数(Spherical Harmonics)作为漫反射的集成方案。
We convert the octahedral radiance into the Spherical Harmonic at the lower resolution of the Radiance Cache, and then the full resolution pixels load the SH coefficients coherently.
我们将低分辨率的光照缓存中的八面体radiance转化成球谐函数的形式,并在全分辨率像素中连贯地读取球谐系数(用于光照计算)。
这样我们可以高效以及高质量的做SH漫反射集成了。
*之前也介绍过,球谐函数(SH)是“传统的”光照探针的做法,阶数越高则保留的高频信号越多。高频意味着光照在空间的激烈变化(例如图像的高频部分就是明暗分界处等),低阶球谐函数无法很好描述这种变化,但性能上非常省——最终只用一组系数就可以定义出整个球面探针采样的结果。
粗糙表面的高光
光线追踪的反射对于高粗糙度的表面来说是开销非常大的(各个点可能反射角度都不同,需要很大采样数),并且它们需要追踪额外的射线。
Thinking about the GGX lobe, at high roughness it becomes wide and converges on diffuse.
想象GGX波瓣(lobe),在高粗糙度下它会变得很宽并(近似)覆盖漫反射的范围。
*这里主要是说用一定的方式来近似粗糙高光(反射),可以避免用昂贵的光追方式来做反射。GGX是一种微表面模型( microfacet models),是以函数模型的方式来以一定系数描述微表面接收光照后的出射角度范围与能量的;波瓣则是以图形来描述这种分布的一种方式。更多信息可以去看看Games101,有讲微表面模型的部分。
*之所以不用讨论光滑表面高光(反射),因为这时基本只用按镜面反射算一次就行了。
粗糙表面的高光——重用屏幕探针
相比于追踪额外的射线,我们可以重用屏幕空间光照缓存。
我们基于GGX波瓣函数来生成重要性采样的方向,并可以直接从屏幕空间光照缓存中采样(用于高光计算)。
这就自动利用上了之前所有的采样和过滤中的工作成果(其实就是设计这个缓存结构时都考虑到了。原文用到了leverage一词,直译是杠杆作用、以杠杆的方式完成)。
降精度的采样丢失邻接阴影
*下一个要处理的问题就是丢失邻接阴影信息。
全分辨率下的Bent Normal
我们可以利用全分辨率的Bent Normal来减轻邻接阴影的问题,因为它自带方向性的遮挡程度信息(前面介绍过)。
我们通过屏幕空间射线追踪的方式计算Bent Normal,其中的追踪距离等同于屏幕探针之间的距离。
*这里的思想类似于计算屏幕空间的AO。
(邻接阴影计算)与屏幕空间光照缓存的集成
之后我们把全分辨率的的Bent Normal与屏幕空间光照缓存结合——使用“水平方向的间接光照”(‘Horizon Based Indirect Lighting’)估计方式。(*这里很类似HBAO的思想)
This treats the Screen Probe GI as far-field Irradiance, and the Bent Normal represents the amount of near-field Irradiance, with the multi-bounce approximation from the paper giving the near-field Irradiance.
这个方案把屏幕探针的全局光照视为远域(far-field)的Irradiance,然后Bent Normal代表近域(near-field)的Irradiance——计算是考虑上了图中的论文给出的近域内多次弹射情况。
*结果比较,右侧是补充了邻接阴影的结果。
对于全分辨率的细节小物体,阴影也能正确表现了。
分帧过滤器
对于分帧过滤器,由于我们不是每像素都设置探针(每帧都采样),因此需要抖动探针位置。(*上一篇中介绍过)
这种抖动需要一种稳定的分帧过滤方式来隐蔽,因此我们采用深度排除(depth rejection)而不是相邻对齐(neighborhood clamp)的方式。
深度排除尽管能提供稳定的结果,但仍然在光源变化时性能很慢——这会表现为运动物体及周围物体的间接光照的更新可能不够及时。
在追踪高速移动物体时切换至快速更新模式
*最终的方案是在达到速度阈值时,降低分帧过滤权重,提高空间过滤权重。
*最后有一节性能总览这里略去了,有兴趣的可以去看看原文。
结语
上一篇我也提到过,目前渲染高清化的这些技术中,突破性能瓶颈的主要思路就是基于时间的超采样和基于AI推测的超采样,但两者都对于画面突变的情况适应力较差;而导致需要超采样的其中一个原因,就是单帧的光线追踪开销还是太高了。我很好奇下一个世代的渲染技术要如何突破这方面的瓶颈,但至少目前我还想象不到(但至少不能全指望AI效率的提升)。
另外,不透明渲染还和这套整体“高清化”的架构无关,是另一套优化方式和另外的叙事。实时上不透明渲染要达到更高质量只会是越来越耗的——例如水体、粒子特效、透射材质等(之前一个主题介绍的云就是如此)。
最后谈一点题外话,关于《黑神话:悟空》的引擎运用上的小问题:最明显的是部分场景的运行效率不正常的低,且往往不是处于极值状态的一些场景;游戏中还有一类常见的物理系统碰撞盒遇到极限值导致卡死的问题,例如第三章某个BOSS后出去的斜坡,直接走不跳的话大概率就会遇到。
而根据身边统计学,这游戏在PS5上的运行视觉效果相对也差一些。可能由于没法太精确的定制画质,因此整体的环境视觉质量是不如PC端的;很多人会拿这个游戏的整体PS5观感和《战神:诸神黄昏》比较,那么通用商业引擎的整体运行效率肯定也是比不过第一方定制化开发的(开头提到的《最终幻想16》等更是这个情况)——况且场景美术资源的精度也不是一个量级的。
依稀记得在《黑神话:悟空》的早期一些访谈中,主创曾谈到他们想实现肌肉破坏流血、以及毛发染血后粘合变形的效果——当时主创似乎认为这种特性可以归纳为一种“怪物肌肉材质”,是可以模块化开发出来的,但一定程度了解3D游戏开发的应该能知道,这是需要高度定制的而且很复杂的一个组合需求;它大概率不会是人们对物理世界的了解那样是一套统一法则的实现。我很高兴最终在多年的磨合中游戏科学团队能实现引擎运用上的“取长补短”,实现了项目管理和开发效率上的成功,而并没有像我了解过的其他一些单机开发项目一样陷入“细节地狱”。
整体来说,这款游戏无疑是取得了空前的成功,但舆论也不能上头了觉得这是“技术上”的空前成功——目前来说只能说这是游戏艺术上的空前成功(游科自己应该是很清楚这一点的,他们总的来说是比较低调和谦虚的)。说到底引擎技术基本还全是别人几十年的积累得到的,我们距离当作工具来用明白都还有一定距离。
即使世界处于反全球化的浪潮,有些文化领域(例如游戏)其实已经深度交融了。在世界的其他玩家玩《黑神话:悟空》的体验中,我能感受到有些人不是为了热度,而是真的被这个游戏的真诚打动了。我认为世界在一定程度上还应该是求同存异的,我们能有《黑神话:悟空》这样伟大的游戏,也应该感激作为引擎的虚幻5。
最后是资料链接:
Radiance Caching for Real-Time Global Illumination 的PPT文件
The Tomorrow Children: Lighting and Mining with Voxels 的MP4视频地址