《堡垒之夜:大逃杀》第四章中的虚拟阴影贴图

Andrew Lauritzen和Ola Olsson
大家好,我们是渲染工程师Andrew Lauritzen和Ola Olsson,主要从事虚幻引擎5中的阴影开发工作。自从我们着手开发UE5以来,虚拟阴影贴图(VSM)就一直是我们的工作重点。VSM在设计上能够很好地与Nanite的虚拟化几何体搭配,无论是摄像机附近的微小几何细节还是地平线,VSM都能为其高效、精确地渲染阴影。

在这篇文章中,我们将介绍虚拟阴影贴图在《堡垒之夜:大逃杀》第四章中的一些使用细节,并特别关注最近的技术改进以及内容方面的注意事项。VSM的设计专门考虑到了Nanite,它与Nanite的光栅化过程紧密结合;在许多情况下,对阴影的优化直接关系到Nanite的性能。因此,在继续之前,我们建议你阅读博文《〈堡垒之夜:大逃杀〉第四章中的Nanite技术》,因为其中提到的许多技术也很重要——其中一些技术主要是为虚拟阴影贴图设计的。

考虑到第四章大量使用了Nanite技术,选择虚拟阴影贴图是一件自然而然的事。与传统阴影贴图相比,它们的性能和质量都更好。光线追踪阴影目前并不是一种恰当的选择,因为当不得不为《堡垒之夜》中的重要动态变形和动画重新构建加速结构时,这种阴影的性能并不理想。此外,光线追踪阴影使用的是细节较少的Nanite几何体版本,虽然这种版本非常适合用来实现某些效果,但如果应用到直接阴影中,则会缺乏精细细节。

除了性能和质量外,我们在第四章中还为阴影设定了几个更具体的目标,我们会在下面的小节中进行讨论。
 

太阳阴影(定向光源)

《堡垒之夜》的主要游戏场景在户外,玩家在游戏中看到的大部分阴影都是太阳造成的。太阳也是最具挑战性的光源之一,因为在理想情况下,从距离玩家几厘米的地方到地平线,所有阴影在视觉上都需要保持一致。在《堡垒之夜:大逃杀》中,这种规模上的差异不仅存在,在最初的跳伞序列中就得到了体现。

之前,《堡垒之夜》使用了各种阴影技术,试图在平衡性能的同时覆盖所有这些情况。玩家附近有一些传统的阴影贴图级联。除了这些,还有距离场阴影和一个“远方”级联的混合,它覆盖了贴图的大部分,但不是所有。玩家角色会得到一张按每个物体划分的专用阴影贴图。屏幕空间接触阴影将铺设在这一切事物的上方,尝试恢复一些被低分辨率技术遗漏的细节,并填补远方所有缺失的阴影。

虽然混合使用这些阴影技术在过去取得了足够好的效果,但这样只能在远方生成低分辨率的模糊阴影,而且不同技术之间的过渡区域往往很显眼。Nanite的许多视觉优势源自它为细节层次级别的过渡提供的时间稳定性。进入不同的距离范围时,如果阴影消失、闪现,或切换到另一种技术,时间稳定性就会被破坏。

虚拟阴影贴图使用一条统一的路径,取代了所有这些技术。

虚拟阴影贴图缓存的注意事项

虚拟阴影贴图可以满足我们的质量要求,但我们过去的《Lumen in the Land of Nanite》《黑客帝国觉醒:虚幻引擎5体验》演示在很大程度上依靠缓存阴影贴图页实现良好的性能。《堡垒之夜》的目标不仅仅是将性能提升到比之前的演示更高的水平,但有两个附加约束条件严重地削弱了缓存阴影贴图的能力:动画变形的数量过多(主要是树木);太阳持续移动。

最初,我们实施了一系列改进措施,包括将“优化的WPO”标记纳入考虑范围(在Nanite的博客文章中讨论过),用来帮助减少动画几何体的失效情况。这起到了帮助作用,但不足以达到我们的性能目标。

我们试过使用一些简单的优先级模式直接限制失效情况的发生,但这会在页边界产生太多伪影。虽然对于传统的阴影贴图,这些模式效果良好,因为对它们来说,最大的成本来自对远方几何体的渲染,而Nanite和虚拟阴影贴图能够更好地将开销用到屏幕像素上。结果是远处阴影的成本相对较低,但在某些情况下(例如走近一棵树并抬头看它时)会出现一些最糟糕的结果,以树为例,几何体的不连贯足以降低阴影页的效率,并且Nanite的性能也会因为严重的过度绘制而降低。
简单地限制或跳过阴影页的更新会在靠近摄像机的地方产生明显的伪影。我们试过使用一个“基本姿势”将一些物体绘制到阴影页缓存中,然后将阴影查询重新投射到该姿势上。这产生了一种类似于烘焙光照的效果,即阴影跟随动画物体四处移动,就好像被粘在了表面。不幸的是,没有简单的方法可以将自身阴影(例如叶子在其他叶子上产生的阴影)与其他物体在树上产生的阴影分开,这践行起来会得到很糟糕的外观。

第二个约束条件引发了更大的问题。当日时间系统是《堡垒之夜》中的一大特色,太阳的方向会持续变化。为了确保完全正确,我们需要在光源方向发生改变时丢弃所有缓存的阴影贴图数据,但既然太阳移动得并不是太快,在这方面“作弊”是很正常的。然而,在几个因素的共同作用下,在虚拟阴影贴图中“作弊”会更加困难。

首先,这不是仅仅旋转阴影数据就能解决的,需要将阴影页的下落位置完全参数化。即使太阳移动得非常缓慢,但对于摄像机附近的阴影页来说,这种效果仍然相当明显:
《堡垒之夜》中的太阳移动会导致阴影页表逐帧发生显著变化。
这反过来又说明了,我们必须至少将缓存数据从一帧中重新投射到下一帧中,这增加了额外的开销,消耗了我们最初从缓存中节省下来的资源。重新投射密集阴影贴图相对简单,虽然在边缘会丢失一些数据,但相对模糊的阴影输出通常可以隐藏这些小问题。

遗憾的是,使用虚拟阴影贴图,如果它覆盖的所有源页都在前一帧中得到了映射,我们就只能重建新页,否则最终将缓存一个部分无效的阴影页。VSM的稀疏性为它带来了很高的效率,其概念上密集的阴影数据存在明显的“空隙”。这进一步减少了可复用的缓存页数量。

VSM通常还具有更高的分辨率,并且使用比传统阴影贴图更清晰的过滤,因此问题和不精确的地方将更加显眼,并且不太可能被宽泛的模糊化处理隐藏。最后,对于失效和重新投射来说,最坏的情况是:观察树木这种杂乱物体时,数据中的“空隙”非常多,几乎没有页面可以被重建。

这些问题,加上常见的不良案例,让我们彻底更改了《堡垒之夜》中太阳阴影的策略。

无缓存的太阳阴影

既然在《堡垒之夜》中无法进行缓存的情况是不能避免的,这些情况就必须符合性能预算。为了达到这个目的,并实现60FPS的目标,我们将太阳阴影的有效分辨率目标降到了之前演示中的一半左右。这确实会造成视觉上的影响,但阴影的分辨率仍然高于之前技术所能达到的程度(前面的比较已经表明了这一点)。我们还添加了一个控件,通过假设我们永远不会缓存定向光源页(r.Shadow.Virtual.Cache.ForceInvalidateDirectional),减少记录次数和无效情况的开销

其他游戏如果要做出这种决定,需要权衡的地方会有所不同。放弃阴影贴图缓存并降低阴影分辨率肯定会对视觉效果产生影响,对其他游戏来说,这可能不是最佳选择。也就是说,在《堡垒之夜》中,即使在60FPS的帧率下没有缓存定向光源阴影,我们仍然会对所能达到的质量水平感到满意,因为比起以前的解决方案,它依然存在明显的改进。

非Nanite网格体

既然决定停止努力为太阳实现缓存,渲染到阴影贴图的原始性能就变得至关重要。要获得良好的VSM渲染性能,最重要的第一步就是使用Nanite生成一切。非Nanite对象可能会引起严重的性能问题,如果它们的多边形数量很多,尺寸庞大,或与许多虚拟阴影贴图页重叠时,问题将尤其显著。

由于《堡垒之夜》中的内容很多,我们创建了一些快速但丑陋的调试输出,帮助找到最大的问题制造者。通过设置r.Shadow.Virtual.NonNanite.NumPageAreaDiagSlots -1,游戏将输出调试文本,识别覆盖大量阴影贴图页的非Nanite实例:
地形是最大、最先出现的问题制造者。于是我们添加了简单的Nanite地形支持,这主要是为了改进虚拟阴影贴图的性能,有效消除地形带来的问题。

在《堡垒之夜:大逃杀》第四章中,我们能够将绝大多数几何体转换为Nanite,大幅提高阴影性能。仍然会有一些非Nanite的阴影投射对象(尤其是,其中包括玩家模型),但它们很少,而且足够小,一般不会造成性能问题。

为了支持各种体积效果,虚拟阴影贴图还维护了一个分辨率非常低的贴图,它覆盖了整个视锥,称为“粗页”。Nanite出色的LOD使渲染粗页变得非常高效。然而在以前的版本中,非Nanite几何体造成了变化无常的大量开销,这主要是由不必要的顶点处理导致的。为了减轻这个问题带来的影响,我们在粗页中加入了对小型对象的过滤,让我们能够在发布的《堡垒之夜》中启用这一功能。这在虚幻引擎5.1中也是默认启用的。

植被

改进植被是《堡垒之夜:大逃杀》第四章中的主要视觉目标之一,但它也是造成性能问题的主要原因之一。

归结起来,为无缓存的阴影优化渲染性能在很多方面与优化主视图相同,这篇Nanite博文已对此做出详细介绍。对阴影性能影响最大的优化措施是取消Alpha测试,并尽可能降低全局位置偏移的成本(它在《堡垒之夜》中主要是预先计算的)。有了这些非常精细的Nanite网格体,这些功能的成本已经比较高了,但当它们跨越多个阴影裁剪图级别时,我们可能还需要将网格体群集栅格化几次。

我们为阴影通道实现了全局位置偏移距离剔除(相对于主摄像机位置,而不是光源),部分原因是为了与主视图保持一致,但也是为了适度提高性能。

在第四章之前,《堡垒之夜》已经使用了一个“阴影代理”系统,在早期阶段,我们不确定在阴影通道中使用具有30多万个多边形的完整树木网格体是否会达到我们的阴影性能预算,因为即使阴影和主视图要渲染的像素数量差不多(或者在某些情况下,阴影的像素数量更多),阴影分配到的帧预算通常会比主视图少。阴影代理的实现方式是在树木Actor上放置两个静态网格体组件:一个用于主视图,它禁用了“投射阴影”;另一个是隐藏组件,用于阴影代理,启用了“投射隐藏阴影”。

在第四章中,阴影代理网格体复杂得多:三角形数量通常超过6万个。
(左)基础树木网格体(超过30万个多边形)|(右)树木阴影代理网格体(超过6万个多边形)
代理经过调整,对有阴影的树木只会造成最轻微的视觉影响,因为放弃太多细节会严重降低树冠的光照深度感。和已发布的游戏中一样,阴影代理失去了一些细节,但不是特别明显,除非你直接比较两个画面,否则难以察觉。
(左)有阴影代理网格体的树木 |(右)没有阴影代理网格体的树木
值得注意的是,在添加Nanite的“保留区域”选项,并实施Nanite的许多可编程光栅化优化之前,我们就对阴影代理进行了评估。在后续过程中,我们没有时间重新做出彻底评估。此外,《堡垒之夜》中已经存在阴影代理系统,而且非Nanite平台仍需继续使用该系统。

这就是说,我们并不是完全确信在今后,阴影代理对性能来说是必需的。与传统直觉相反,它们对远处(甚至是中距离)植被的性能并没有产生显著影响,因为Nanite仍然会在这些范围内为两种网格体设定相同的三角形密度。Nanite阴影代理唯一发挥重要作用的地方是,当树木距离摄像机非常近时,代理会略微减少阴影通道中渲染的几何体细节。

根据我们在第四章发布后所做的一些概念验证工作,我们非常确信,我们不再需要由美术师生成的阴影代理网格体。要获得类似的性能和质量,我们最需要的或许是能够在这些网格体上限制Nanite对细节层次级别做出选择。我们会继续评估是否仍然需要这种能力,因为对阴影代理做出最初的评估后,我们又完成了其他各种优化工作。

另一项主要面向树木的质量改进是,我们采用其他方式评估了《堡垒之夜》树木次表面材质上的阴影项。以往,这些材质会在叶片面向光源的一面使用标准的“硬”阴影项,然后在背面使用较柔和的衰减,粗略表示光线透射进叶片。

在以前的阴影技术中,阴影太过模糊,这种细节不会造成太大的影响。但使用虚拟阴影贴图时,就会新出现几处明显的伪影,因为为了柔化着色效果,这些树木上的叶片法线经过处理,成了更接近一种“球形”的法线,并没有使用符合它们几何形状的法线。这导致了不连续——着色法线从背面翻转到了正面,相关联的阴影项也被切换了。

为了解决这个问题,并在阴影项中实现更柔和的整体外观,我们实现了另一种次表面阴影模式,它只使用一个阴影项,其中包含了粗糙的透射衰减,忽略了着色法线。此外,我们可以根据材质的不透明度,随意拓宽阴影的过滤锥度,创造内部散射的感觉。这种新模式可以通过r.Shadow.Virtual.SubsurfaceShadowMode 1启用,未来版本的引擎中可能会默认启用它。
(左)经典次表面模式 |(右)改进的次表面模式(r.Shadow.Virtual.SubsurfaceShadowMode 1)

草地

出于视觉和性能方面的考虑,我们在《堡垒之夜》中处理草地的方法不同于树木和灌木的处理方法。

和其他游戏中的常规情况类似,在以前,《堡垒之夜》中的草地不会被渲染到阴影贴图中,它纯粹依赖于屏幕空间“接触阴影”。通常而言,之所以这样做,部分原因是一般阴影贴图的分辨率绝对无法捕捉到草地细节。

有了基于Nanite几何体的草地和虚拟阴影贴图,我们对此进行了重新评估,但出于几个原因,最后决定仍然只使用接触阴影。

首先,美术师希望有权控制草地将投下多深的阴影,而虚拟阴影贴图无法提供这种功能。这样做的目的是为了在某种程度上模拟光线透射进草叶,此外,由于叶片比现实中大,这样也能抵消一些不协调感。其次,新的草地使用的是真正的几何体,这种质感很好地提升了视觉质量,接触阴影也因此得到了更好的深度缓冲。最后,将草地渲染到虚拟阴影贴图中的性能成本虽然可控,但并不低。

在一些情况下,尤其是对较大的花朵和蕨类植物来说,有完整的虚拟阴影贴图看起来会更好。但总体上的结果是相似的,而且美术师更喜欢屏幕空间方法所提供的细腻强度控制。对比视觉质量之后,我们决定将这些性能留到其他地方,实现更大的视觉冲击。
(左)虚拟阴影贴图渲染草地阴影的成本较高,也与美术风格不搭。|(右)屏幕空间接触阴影可以捕捉到类似的效果,而且能够通过降低强度模拟更多透射。

水体

水体表面的阴影也需要注意。虚拟阴影贴图通过分析摄像机的深度缓冲区并将这些采样位置投射到光源的坐标帧中,确定哪些页是需要的。单层水体和其他传统的“前向渲染”技术不能起到深度缓冲的作用,因此在某些摄像机视图中,或许没有可用的高分辨率阴影数据让你对水体表面进行查询。

我们与水体渲染团队合作,为水体实现了“深度预通道”模式,用来为虚拟阴影贴图提供必要的数据(此外也能够与其他一些系统更好地整合),你可以通过控制台变量r.Water.SingleLayer.DepthPrepass 1启用该模式。启用该模式后,水体表面也会对页做出标记,为高质量的虚拟阴影贴图提供保障。

局部光源

《堡垒之夜》拥有大量局部光源(既有聚光源,也有点光源)。从大型顶灯到灯具,再到宝箱等事物的“效果”光线,它们用途广泛。在夜间,这些光源在视觉上更加明显,但它们经常与太阳或月亮等定向光源一起出现,这两种光源加在一起,不能超出阴影预算。在给定的帧中,经过剔除,通常会有十几处具有阴影的局部光源。
《堡垒之夜》使用“效果”光线吸引人们注意宝箱。
从质量角度看,虚拟阴影贴图的功能天生多于之前的解决方案。首先,它们可以直接捕捉更多细节,同时消除困扰传统阴影贴图的偏斜伪影。
(左)阴影贴图在墙壁上有过于模糊的半影和偏斜的伪影。|(右)虚拟阴影贴图修复了伪影,拥有可变的半影。
其次,它们尊重光源的“光源半径”,能够产生更多物理上可信的阴影半影——在接触点处锐利,在远离接触点的地方柔和。由于这个参数之前被忽略了,我们需要进行检查,并为《堡垒之夜》的许多光源设置合理的光源半径。
(左)阴影贴图的阴影均匀、锐利,但缺少小细节的阴影。|(右)虚拟阴影贴图拥有区域光源半影和接触硬化。
要在这些局部光源中实现高性能,在帧中将涉及到两个方面:渲染它们,并将它们重新投射和过滤到场景中。这两个方面需要我们在《堡垒之夜:大逃杀》第四章中做一些工作。

局部光源缓存

与太阳不同,对《堡垒之夜》中的局部光源来说,阴影贴图缓存很有用。局部光源通常不会移动,它们周围的几何体相对都是静态的。此外,为了避免远处光源渗透墙壁,计算它们的阴影很重要,但在每帧中更新它们并不是那么重要,因为这对视觉的影响很小。当我们需要为十几处光源执行更新时,利用高分辨率、近场阴影的基础机制会变得成本高昂,因为每处光源都贡献了少量的阴影像素。

于是,我们增加了一项支持功能,当局部光源在屏幕上小到只能覆盖一个阴影贴图页(128×128纹素)时,你就可以将它归类为“远处光源”。这种模式可以通过控制台变量r.Shadow.Virtual.DistantLightMode 1启用。对于远处光源,我们省略了大部分页表机制,因为它们实际上是“密集”的阴影贴图。此外,我们限制了这些光源的更新频率(r.Shadow.Virtual.MaxDistantUpdatePerFrame),即使几何体已经移动,在两次更新之间也必须依赖缓存的页。
局部光源的可视化。黄色光球小到可以成为“远处光源”,而蓝色光球会得到完整的虚拟阴影贴图。
在《堡垒之夜》中,我们能够在每一帧中只更新一处远处光源,这只会产生极小的视觉问题。离摄像机较近的光源仍然会得到完整的虚拟阴影贴图,阴影页以常规方式更新和失效。

阴影投射的性能

在光源循环中,存在另一个影响阴影性能的重要方面,那就是对每处光源阴影的计算和应用。长期以来,小型局部光源一直是这里的一个痛点:它们影响到的像素数量相对很少,但我们仍然需要执行每处光源的多个通道,它们之间存在依赖关系,导致GPU无法得到充分利用。

虽然这并不是什么新问题,但是使用虚拟阴影贴图时,我们可以使用一些手段解决它。虚幻引擎5有一个可选的“单通道投射”模式,它能够起到帮助作用,但必须结合使用群集着色。很遗憾,在《堡垒之夜》中,启用群集着色意味着性能的倒退,它甚至会抵消这种模式的大部分好处。在虚幻引擎5.1中,我们分离了两者,允许你在不使用群集着色的情况下启用单通道投射。

激活“单通道投射”后(r.Shadow.Virtual.OnePassProjection 1),它将提前在单个通道中计算局部光源的阴影,结果会被存储在一个临时缓冲区中,不会交叉计算阴影和光照。此外,我们还重构了一些其他通道(半透明体积注入),让它们提前发生,而不是放到循环内部。

最终的结果是,如果给定的光源只需要一个虚拟阴影贴图(并且不使用会影响阴影遮罩的光源函数或其他通道),你就可以通过一次管线化的绘制调用完成绘制,这将大幅改善小型局部光源的性能。
(左)在传统的光源循环(1.56毫秒)中,每处光源都有几个带有GPU屏障的交叉执行通道,在GPU空闲时会浪费性能。|(右)经过优化的“单通道投射”光源循环(1.08毫秒)会预先计算所有阴影,因此每处光源都可以通过管线被单独绘制出来,不存在屏障。

总结

除了这篇文章中描述的改进外,我们还在引擎中实现了另外一些常规改进,它们的适用范围不局限于《堡垒之夜》。今后,我们还会继续打磨剩余的粗糙部分。

总体而言,尽管我们最初对缓存和性能有一些担心,但《堡垒之夜:大逃杀》第四章中虚拟阴影贴图的外观和表现都让我们相当满意。很高兴看到人们喜欢这款游戏和新的图形画面,我们也期待看到其他开发者会如何在自己的虚幻引擎游戏中使用这些功能。
如需了解更详细的技术信息,请参阅虚拟阴影贴图文档

    立即获取虚幻引擎!

    获取全球最开放、最先进的创作工具。
    虚幻引擎包罗万象,并提供完整的源代码访问权限,开箱即用,诚意十足。