Origin
zhuanlan.zhihu.com
Tags
简悦
项目
收藏夹
创建时间
收藏类型
Cubox 深度链接
更新时间
原链接
描述
作者:Colin Weick, Emily Zhou
00 前言
我们今天的讨论主要围绕的是《孤岛惊魂 6》这款游戏,这是《孤岛惊魂》系列的最新作品。如果你对《孤岛惊魂》不熟悉,那么我来简单介绍一下,这是一个开放世界的第一人称射击游戏系列,每一部作品都在一个新的环境中展开。在这一部作品中,我们将玩家引入到了一个名为 Yara 的虚构加勒比海岛屿,这个岛屿的灵感来源于古巴和其他一些国家。在这里,玩家将扮演 Dani Rojas,一个当地的反抗者,他正在努力推翻独裁者 Anton Castillo 和他的儿子 Diego 的政权。

《孤岛惊魂》系列的一大亮点是其充满活力的开放世界——每个世界都有自己的生命,许多系统不断地相互作用。为了给玩家带来热带岛屿的体验,我们需要在《孤岛惊魂》中加入一些新的元素——一个完整的,动态的天气系统。
为了让你对动态天气有个概念,下面我会快速预览一下作为玩家在《孤岛惊魂 6》中可能会看到的情况。


我们将向你全面展示我们的天气系统。因此,将许多主题压缩到以下几个类别中。首先,我将介绍灵感来源和核心控制。然后,我将通过覆盖每种资产类型来解释如何处理材质的湿度。接下来,将逐步介绍支持天气的每个渲染特性的技术细节。最后,我将以一些最后的想法作为结论。
01 灵感
对于我们的灵感来源,我们参考了各种热带地区的天气,比如古巴。
岛屿地点通常具有非常独特且多样的热带天气。在项目开始时,我们对热带风景和天气进行了研究,以便当玩家探索这个世界时能够为他们提供一种真实的体验。我们很快发现,需要同时融入这里展示的标志性晴天天气……

… 以及热带天气的另一面,包括大雨和雷暴。我们需要模仿天气可能急剧变化的方式,有时在几小时内就会发生。然后将这些想要突出的元素,包括在概念艺术中。

Yara 被设想为一个理想的热带天堂,它应该能够让玩家真实地感受到我们游戏中的奇幻世界,提供一个令人信服的逃离现实的场所。然而,我们的艺术指导希望将那完美无缺的天气与预示着不祥的雷暴形成对比。当玩家被困在风暴中时,他们应该能真实地感受到雨水和湿气,这种感觉应该是令人信服的。当然,亚拉是一个岛屿,所以海洋是这个等式的关键部分,即使对于天气也是如此。

考虑到这一点,让我们将这个方向转化为实现天气的目标。希望有一个包含不同天气状态的集合。希望天气是动态且多样的。需要一个过渡合理的系统。例如,暴风雨应该由乌云密布开始,然后是留下的水坑。当然,所有的东西都需要高效并且符合我们的预算。必须强调的是,这个天气系统将用于一个开放世界的游戏。我们的决策经常受到数据和性能预算的影响,以及需要支持一天中的所有时间和世界上的所有地点的需求。
02 核心控制
让我们继续进行实现,从核心天气控制开始。
天气管理器是我们天气系统的核心代码,它基本上是在幕后驱动天气的东西。它包含用于定义和控制天气的信息,其中一些被暴露为天气数据库中的设置。可以将天气管理器视为系统的后端,天气数据库视为前端。从天气预设开始,概述我们系统的设置。

在项目中,天气类型的集合被称为天气预设。这些是在天气数据库中使用天气管理器暴露的参数定义的。为了了解我们可以通过这些参数实现什么,逐步查看一些最终的预设,并比较它们的一些值。

对于 “少云” 预设,我们使用了较低的云覆盖值,如右下角所示。

相比之下,“破碎云” 预设使用了更高的云覆盖度,以呈现出大片蓬松的云朵。

接下来我们可以看到,“薄雾” 预设在远处增加了轻雾效果。

相比之下,我们的 “浓雾” 预设将雾效的值调整得更高。

接下来的几个预设将展示雨强度的递增,从 “小雨” 开始…

然后是 “中雨”…

接着是 “大雨”,这将使雨强度达到最大,并使天空变暗。

这就留给我们最强烈的预设,“雷暴”,它增加了闪电。
现在我们有了天气预设的基础模块,接下来需要创建一个天气模式或者循环。一开始,我们的初步想法是收集并使用来自像迈阿密这样的城市的真实世界气象数据。我们从 2013 年的一段时间内获取了每个小时的天气描述。虽然使用真实世界的数据来驱动预测是很好的,但我们希望有更多的艺术控制权。最终采用的方法是一个类似方法,但我们自己选择天气预设,并设计了每个区域最多 5 个全天的循环。这种自由也让我们能够在最好的地方展示天气预设!
现在我们需要花些时间来讨论区域。《孤岛惊魂 6》的世界被划分为 3 个主要区域:西部、中部和东部,每个区域都有自己的视觉特性。

西部是干燥区,中部是湿地,东部是丛林区。为了增强这些区别,我们希望每个区域的天气都有所不同。
请注意,我们对区域的定义也扩展到了更小的区域,如室内、洞穴和艺术家可能需要定义的其他任何地方。对于每个区域,都暴露了最小和最大曲线来限制某些天气属性。这就是如何根据玩家在世界中移动的位置来改变天气的方式。

一个例子是,我们为室内区域设置了最大雾曲线为 0,以防止雾在室内出现。这种方法的限制是,如果你的室内区域有窗户,你会看到雾在你进入后立即消失。这就是为什么要仔细分配区域。天气管理器然后接收玩家周围所有可能重叠的区域,并相应地插值曲线,以输出调整后的天气。
天气管理器还需要允许进行覆盖,例如,用于游戏任务和预渲染的剪辑场景。游戏的开场任务让玩家在夜晚的雷暴中逃离城市街道。此外,游戏支持多人合作模式,所以天气需要为所有玩家复制。
这个流程图说明了最后的操作顺序。

总的来说,首先参考预报获取当前时间的天气预设。然后,根据玩家所在的位置,应用区域调整。这给我们提供了当前的天气状态,可以为游戏玩法进行覆盖。最后,使用这个天气状态随时间更新湿度、雨量和闪电,并输出最终参数。这些是像 WetnessFactor(湿度因子)、RainFactor(雨量因子)等变量,现在可以访问它们来驱动视觉、音频、游戏玩法等。
03 材质湿度
既然我们已经建立了天气管理器的内部工作机制,我们需要让材质做出响应。换句话说,是如何让资源变湿的呢?
湿度是天气的一个重要组成部分;当下雨时,我们需要看到世界的变化。这意味着每个资源都需要一个湿润状态,这样就可以在干燥和湿润之间进行混合。创建每个资源的湿润版本所带来的风险包括:
- 资源太多,所以需要额外的数据会增加很多的生产时间。
- 艺术团队都有自己的资源管道和着色器。可能会冒着缺乏整合的风险,这在我们项目的规模下很难控制。

我们的解决方案是与资源创建并行工作。为了做到这一点,它需要尽可能简单和统一。我们还决定,它主要应由技术艺术家驱动,并且应该能够 “开箱即用”。这意味着应该能够在世界中拖放资源,它们应该在雨中变湿。当然,有些材质会得到更多的细节和打磨,但对于最简单的道具或物件资源,湿度应该在不重新访问它们的情况下工作。那么,让我们深入了解湿度实现。
首先,我们通过设定两种湿度类型:静态和动态,将解决方案分为两部分。顾名思义,静态湿度适用于永远不会移动的对象,而动态湿度适用于会移动的对象。静态湿度主要应用于对象库资产,包括道具和结构。湿度级别是通过使用天气管理器提供的 WetnessFactor 参数来确定的。还使用湿度阴影图来在适当的地方遮蔽湿度。静态湿度在视觉上将是最简单的,并将在延迟照明阶段应用,这意味着它将在一个地方应用,静态湿度的优点是其简单性和所有事物都可以一次性调整。但缺点是没有灵活性。

动态湿度主要保留给武器、车辆和角色。

他们的湿度级别是通过射线投射来检测暴露在雨中的程度来计算的。他们的湿度还包括一个名为局部湿度的附加功能,用于处理在水中的浸没。对于动态湿度,视觉变化将在每个单独的着色器中应用;这意味着可以更多地定制外观。这样做的缺点是必须管理每一个可能用于动态资产的着色器。
首先,让我们看看静态湿度类型。主要需要的湿度计算是湿度阴影图,它遮蔽了例如在阳台下或室内的物体。这很重要,因为我们重复使用道具,所以无论它们放在哪里,它们的湿度都应该准确。由于静态湿度是在延迟照明阶段应用的,我们在延迟阴影阶段存储了湿度阴影。然后,将采样的阴影与天气管理器的 WetnessFactor 相乘,以得到最终的湿度。

让我们来看一个湿度阴影的例子。在这个场景中,混凝土地板和木制桌子上有较亮的干燥区域。

这是湿度阴影的可视化。如你所见,它工作得相当好,但在垂直表面上有一些精度限制。

如果你仔细看高亮区域,你会看到一些斑点细节。这是因为原始的截止线过于严格,我们使用抖动技术来使其变得柔和。
现在来看看我们是如何应用静态湿度的视觉效果的。首先参考了照片,以分析需要什么输入,以及它们会驱动什么变化。

我们认为必要的两个视觉变化是使反照率变暗和增加光滑度。例如,一些材质,如纸板,在湿润时会变暗,但其表面的光泽度不会显著增加。像瓷砖这样的材质在湿润时不会变暗,但其表面会变得更加光滑和有光泽。还有一些材质的变化则介于两者之间。所有这些变化的关键都在于材质的吸水性。
因此,我们选择的输入是孔隙度。孔隙度与反照率的变化最直接相关。

高孔隙度的材质,如土壤、布料和原木,会变得饱和并显得非常暗。低孔隙度的材质,如塑料、大理石和金属,水会在其表面积聚,而不是被吸收,这意味着它们的颜色应该保持不变。现在有了输入,可以只使用孔隙度图并继续前进。但是有一个问题 - 并非每种材质都能负担得起额外的纹理图;这将超出了预算,而且收益不大。所以,我们需要一种方法,在没有孔隙度纹理可用时,推导出孔隙度。
这就是我们引入孔隙度因子的地方。由于孔隙度和光滑度是概念上相连的属性,解决方案是以下基本公式,其中两个孔隙度因子值是可以按材质类型指定的浮点数。

这个公式基本上生成了一个孔隙度图,它具有光滑度图的所有细节,但其值位于我们精心策划的范围内。
幸运的是,我们的对象着色器已经有了一个带有硬编码 PBR 值的材质预设下拉菜单。

只需要添加孔隙度因子就可以了。这保持了艺术家的工作流程不变,并防止了任何设置错误。对于一个输入非常少的近似,要得到每一个材质的正确反应是不可能的。更重要的是要判断整体情况,而不是单个资产。这帮助我们避免了无休止的调整值的循环。


这是我们一些材质的干湿对比。

现在让我们看一下一整个场景的资产从干燥状态…

… 过渡到湿润状态。
现在,如果我们回想一下,动态湿润的好处是可以自定义湿润的应用方式。为了整理想法,我们再次回顾了真实世界的参考,比如在雨中凝视停在外面的汽车。

对于角色的衣物,我们采用了与道具相同的方法。使用了现有的下拉菜单,但也为最终的光滑度增加了上限,以便为湿布料提供更好的效果。角色的头发是我们做出的最简单的改变;只使用了一组毛孔因子。在早期的头脑风暴阶段,我们希望头发在湿润时能够凝聚在一起,但这对我们的工作范围来说过于雄心勃勃。

对于皮肤,我们希望雨水是可见的,但不是以一种滚动可能显得不自然或分散注意力的方式。我们采用了一种微妙的方法,使用了一种包含水滴法线贴图和湿度遮罩的纹理。应用了这些效果后,皮肤材质的湿度很快就确定下来了。

在我们的项目中,武器和车辆是密切相关的。它们都是游戏元素,并且从第一人称视角近距离看到。出于这个原因,给这些资产添加了动态雨效果,这些效果是通过每帧更新的动态纹理应用的。这些效果被分为两部分,条纹和水滴,分别应用在垂直和水平表面上。


对于条纹,我们的输入数据只有一张纹理,其中包含了条纹法线贴图、高度图和滚动遮罩。滚动遮罩驱动了条纹的垂直移动,使用局部空间 UV 应用它。确保只有当资产处于直立或倒置状态时才应用条纹;在后一种情况下,反转了滚动方向。

对于水滴,我们提供了另一种纹理,包含了法线贴图、高度图和 ID 图。为了驱动动画,对水滴进行了两次采样,以改变水滴淡入淡出的模式,这是通过 ID 图实现不同时间周期。我们还缺少的一件事是那些倾向于在硬表面上积累的微小静态水滴,所以我们后来设法将它们压缩到高度图纹理中。我们还确保在水滴纹理上使用手动 mips 来减少闪烁的伪影。
我们都知道,添加一个特性往往会导致意想不到的问题和边缘情况的出现。我们面临的一个问题是湿润效果出现在车辆内部。这是因为车辆有时会在外部和内部之间共享材质,以节省绘制调用。解决这个问题的方法就是简单地使用红色顶点绘制通道作为内部遮罩。

一旦这个被艺术家绘制,就能够移除内部的雨效果,当然,边缘情况除外,比如当敞篷车放下顶盖的时候。
当我们的游戏团队实现了移动的挡风玻璃刮水器,而我们的 3D 团队添加了雨滴效果时,又出现了一个问题,它们之间没有任何交互!

我们所做的是打包一个挡风玻璃渐变图,并设置着色器根据当前的渐变值来遮罩雨水。然后将这个值传递给游戏团队,以便将所有内容连接起来。这是一个我们最初从未计划的小功能,但是这样的不一致性可以很快地让玩家脱离体验,所以很高兴能够添加它。
对于植被,我们最初使用的是静态湿润度方法,但这导致了两个问题:
- 首先,丛林地面附近的树干和植物被雨水遮挡,这在自然界中看起来很奇怪。
- 其次,我们收到了一些反馈,说树木看起来像金属。这是因为平坦的叶片反射了灰色的天空,这种视觉效果非常强烈,以至于它有效地降低了树木的真实感。

为了解决这个问题,我们将植被转换为动态湿润度类型,这样就可以独立地在着色器中调整湿润度。当然,这意味着不再有任何阴影,但是 99% 的时间,树木都不是手动放置在室内的,所以我们进行了替换。在叶子上使用了水滴纹理,这最终使得它更接近现实,并修复了反射的错误。
地形是最后需要征服的领域。它不仅总是在玩家的视野中,而且如我们从照片参考中可以看到,它还需要更多的细节。

首先,让我们快速总结一下地形需要什么。主要使用的纹理包括反照率(albedo)、法线(normal)、光滑度(smoothness)以及为湿度新增的孔隙度(porosity)纹理。

这些属性都存储在我们用于整个世界的虚拟纹理图集中。要了解更多关于这个系统的信息,你可以参考过去关于地形系统的讲座。请注意,道路和地形贴花都烘焙到虚拟纹理图集中,所以真正需要应用湿度的是一个完整的系统。
为了使地形变湿,实际上使用了与静态物体相同的 ApplyWetness 函数。地形的关键区别实际上是从干燥到湿润的过渡。我们不想要线性淡出,因为这对于如此大的表面积来说有点过于人工。我们创建的新过渡是雨滴溅落效果;

为了做到这一点,我们在 GPU 上进行了一个高度场模拟,输入了一些参数,如溅落物的干燥和扩散速度。得到的动画遮罩被用作自定义湿度因子。还选择暂时增加地形的孔隙度,以通过更深的溅落物来强调效果。
现在地形已经应用了湿度,我们必须将注意力转向呈现湿度所需的最大的视觉特征之一:水坑。水坑也是艺术指导非常期待的特性,因为它们会提供反射,这最终使环境和照明更具吸引力。水坑的设置非常简单。水坑本身是一个地形贴花,但它只包含一个 alpha 渐变和一个只写入地形孔隙度的复选框。渐变作为一个符号距离场(SDF),允许水坑从中心积累。

我们只有几个水坑贴花,它们被现有的地形和道路分布系统散布,类似于裂缝和坑洞的散布方式。根据地形的孔隙度和天气管理器提供的 PuddleFactor 计算了水坑的湿度。一旦得到了水坑的湿度,再次相应地改变了地形的 gBuffer 值。稍微将反照率混合到一个泥泞的水坑颜色,并将光滑度混合到 1.0。对于法线,最初使用了一个平面法线,但这引导我们到了下一个实现…
我们的输入数据是一个单独的纹理,其中包含两个打包在内的法线贴图,一个用于雨滴涟漪,一个用于风涟漪。

输出是一个单独的动画法线纹理。对于雨滴涟漪,对法线纹理进行两次采样,以得到两个重叠的 flipbook 动画(序列帧动画)层。这些基于天气管理器的 RainEffectsFactor 进行淡入,这将确保这些涟漪只在下雨时出现。对于风涟漪,根据当前的风向和强度滚动纹理。因此,无论是否下雨,这种效果都可以存在,比如在风暴过后地面上还有水坑的时候。最后,我们将这些效果混合在一起,得到我们用于水坑法线的结果。

这就是我们为支持材质湿润度所需的着色器更改的收集过程。
04 渲染特性
前面我们展示了天气管理器的复杂性以及湿度实现的细节,现在来看一下我们需要的渲染特性如何来实现动态天气。
但首先,看一下我们所面临的技术限制。游戏在 9 个平台上发布,跨越了多个游戏主机代以及 PC。目标是在下一代主机上达到 4K 60fps,而在上一代主机上达到 30fps。这也是迄今为止 IP 的最大开放世界。我们有一个全天候的循环和一个包含室内和室外环境的开放世界。现在需要在所有这些条件下实现动态天气。
04.1 大气
让我们看一个示例场景,看看这些渲染特性是如何结合在一起来完成我们的天气状态描述的。首先,大气散射。

然后地平线上和头顶上的云。

接着,体积雾遮蔽区域并产生从云中逸出的光束。

接下来,立方体贴图和反射。注意到路面上反射的电线了吗。

最后,雨和闪电,这些元素完整地构建了一场激烈的雷暴场景。

在深入研究各种技术之前,先从光照模型开始介绍。我们使用基于物理的公式,并以能量守恒为目标。在《孤岛惊魂 6》中,有了更高质量的多次散射漫反射 BRDF,带有多次散射瓣的 GGX 镜面 BRDF,以及对区域光源的支持。

右边的图表显示了我们如何处理特定的材质,但值得注意半透明性,这在植被中使用。对于半透明性,我们包裹了用于次表面散射的漫反射光,并添加了第二个漫反射瓣来模拟光穿过表面。你可以参考 Steve McAuley 在 2019 年的 i3D 演讲,以了解更多关于这些改进的信息。
对于全局照明,我们使用一个光探针系统,艺术家们将探针放置在整个世界中,并每天烘焙以包含世界的变化。全局照明数据存储在体素中。我们打包了 13 帧的数据,其中 11 帧是一天中时间的增量,以覆盖天空、太阳和月亮。有一个关键帧用于局部光源,主要在夜晚使用。还有一个关键帧用于天空遮挡。

但是有一个问题,这些全局照明探针不包括云或任何天气的影响。解决方案是在云覆盖率高时淡出间接照明。如果局部光源在所有关键帧中都存在,这可能会产生问题,但由于它们仅限于一个,所以可以避免在云覆盖率高时淡出人造光源。此外,城市环境测试了这个系统的极限。利用数据的稀疏性和探针的可变大小,增加了在室内环境附近和内部的精度。
我们使用 Brune-ton 天空模型以及 Pree-tham 太阳模型,并进行了一些改进,如增加了臭氧层。这个设置涉及到离线计算的查找表(LUTs),以前这些是在运行时生成的,那时非常慢。艺术家们创建了四种天空:湿度 0 和 1,浑浊度 0 和 1,在运行时在这些之间进行混合,以获得变化。

你可以查看在 2015 年 GDC 上关于《孤岛惊魂 4》中的光照的演讲,看到这些概念的早期实现。
在这里,我们展示了湿度和浑浊度参数对光学深度查找表(LUT)和混合散射查找表的影响。

将四对数据混合在一起以得到结果。绿色箭头指向每帧的混合结果,红色箭头指向具有最大权重的查找表,这些权重是根据帧的湿度和浑浊度参数确定的。
这就是我们得到的令人惊叹的日出和日落,看到它们在云层的边缘投下美丽的光影。

04.2 体积云
这引导我们进入下一个主题,体积云。
那么,为什么我们需要实时的体积云呢?简单来说,天空盒子是不够的。它在运动中的效果差,且在不同的天气状态之间难以混合。天空盒子更像是一个背景,而我们需要的是能与世界互动的东西。此外,我们需要云能够响应天气,具有不同的云覆盖等级。最终的解决方案是基于之前的工作,如下所列。


云层照明中最重要的部分是消光。当光线穿过一个体积时,由于与构成云层的水分子的相互作用,它会失去能量。这种消光是由比尔 - 兰伯特定律决定的,我们将其称为透射率。消光包括吸收和散射两部分,其中云层的吸收实际上相当低。最终经过散射后到达眼睛的光线以辐射度来衡量。

单次散射是指光线进入云层,在朝向观察者的方向传播之前遇到一次散射事件。

多次散射是指光线在朝向观察者的方向传播之前,在云层内遇到近乎无限次的散射事件。

这里展示了单次散射对云层的影响。

接下来,将云层的多次散射效果进行了隔离。

最后,展示了单次散射和多次散射的综合效果。注意到云层有了更强烈的深度感。

所有这些散射事件都可以通过相位函数来建模,该函数预测光线在散射后将以多大的强度和何种方向传播。在这个图示中,用椭圆形状来表示在蓝点处发生散射事件后光线散射的相位函数近似值。


右边是我们在实现中使用的双瓣 Henyey-Greenstein 相位函数的图示。真实的云层相位函数会更复杂,而且实时计算的成本过高。你也可以在附加参考这个 Graph of Fog Phase Function 和其他的光照代码。
让我们来谈谈是如何创作云数据的,这些数据代表了云中粒子的密度,用于散射技术。我们创建了基础和详细纹理,包含了各种类型和频率的噪声,这些噪声被合并到一个通道中。然后,这些纹理被体积采样并混合在一起,以创建云的形状。接下来,生成了一个天气图,用于在世界中平铺,并按照天气管理器中的风向滚动。这构建了 XY 云形态,并使我们能够在云覆盖的各个层次之间平滑地插值。我们还有一个卷曲噪声,它包含了 3D 向量数据,在向基础和详细噪声发射光线时,使用它作为原点偏移。

最后,我们有卷云纹理,将其映射到天空的半球形上,以及卷云地平线纹理,将其映射到围绕相机的圆柱形上。

让我们把这些都结合起来。这里从一个晴朗的天空开始,只有大气散射。

然后,添加卷云半球和地平线云层。

接着,添加天气地图,你开始看到云层形成的 xy 形状。

然后,结合渐变纹理来定义云的形状。这个积云渐变最终成为我们需要的唯一渐变,尽管我们确实尝试过其他的。

然后,使用基础细节噪声体积纹理来获取独特的积云形状。

然后,使用细节噪声体积纹理来侵蚀更多的细节。

然后,添加卷曲噪声纹理以获取细腻的细节。

我们使用 Raymarching 的方法来高效地绘制云层,这涉及从观察者处发射一条光线,并在指定的步骤中进行评估。在每一步,从该点回溯到太阳(或月亮)的密度。然后,将返回的光学深度转换为透射率,这可以用来近似单次散射和多次散射。然后,沿着这个段积分透射率,并累积光线行进的单次和多次散射总量。还将消光应用到透射率上,以便影响后续的光线步骤。

因此,每个像素的一般处理过程如下:

计算像素的起点,并对密度、透射率、光学深度和散射(单次和多次散射)进行 Raymarching 计算。如果透射率非零,计算卷云。使用单次和多次散射值,结合相位函数计算辐射度。然后计算大气散射,并使用逆透射率作为遮罩,将前方的云层进行混合。
如果以全屏幕分辨率进行 Raymarching,成本会过高,所以以半屏幕分辨率进行。我们有一个辐射度和透射率纹理,以及历史辐射度和透射率纹理,这些纹理会从上一帧中进行时间重投影。

我们使用棋盘渲染过程来填充这些运行时纹理。对每个像素四边形进行一次 Raymarching,使用棋盘偏移和卷积噪声偏移来获取光线起点。Raymarching 的结果是单次散射、多次散射和透射率。将历史重投影到当前帧中,收集邻域并根据列出的启发式规则进行拒绝。然后使用邻域的双线性插值,并将 Raymarching 的结果分布在这个区域上,随着时间的推移淡化历史。结果被存储为将用于下一帧的辐射度和透射率历史。

别值得注意的是存储辐射度的过程。我们使用蓝噪声对其进行编码,以帮助掩盖由于在如此低的分辨率下进行 Raymarching 而产生的噪声伪影。注意右边,当我们对蓝噪声进行 2x 模糊时,它会收敛到灰色,而白噪声则不会。
我们尝试过从 gbuffer 中进行 Raymarching 阴影,但这太昂贵了。最终采用的解决方案是投影云阴影。这是一个简单的正交投影,没有什么花哨的东西。最后得到的阴影纹理将在后面用于体积雾,以从云中获取光线。



这里地面和水面上有了云的阴影。
04.3 体积雾
接下来体积雾,它被用于环境高度雾以及来自天气预设的局部雾。

我们的实现使用了在先前的工作中提出的与视锥体对齐的体素网格。根据天气有局部和全局的雾,以及用于不同生物群落的局部雾。有了体积雾,光轴是通过在体积的单元格内评估阴影来创建的。但这里存在一些问题!

这是一个关于体积雾处理过程的简要概述。显示的前三个步骤中准备场景深度信息,并将局部雾体积剔除到屏幕 Tile。

我们用辐照度填充单元格。使用雾和雾粒子的局部和全局密度,以及来自太阳、大气、间接照明和点光源和聚光灯的照明来实现这一点。请注意,在上一代上使用的是时间过滤,而不是即将出现的双边模糊。为此,我们需要一个历史缓冲区,该缓冲区被重新投影到当前帧并随着时间的推移逐渐消失。这需要更多的内存,不幸的是会有一些残影,但这是一个更便宜的解决方案。我们使用的相位函数与体积云相同。

在我们用辐照度填充单元格后,计算向体积后方的连续总和。现在,可以从眼睛到目的地,对任何体素进行全辐照度累积的采样。

最后,在下一代中,我们进行了一个两阶段的过程,即在 x 轴和 y 轴上进行双边模糊。在 X 轴上模糊会输出一个转置的结果,而在 Y 轴上模糊则会转置回原始的方向。这优化了纹理的读取和写入。

但是,这里有个问题!

在右边,你可以看到光束中的阶梯状效果,这是采样的一个副作用!我们希望这个过程能够平滑。双边滤波有所帮助,但并不能完全解决这个问题。我们尝试的下一个方法是模糊实际的阴影图。这种方法的一个好处是,结果可以在多个地方使用。

这里我们看到的是指数模糊阴影的过程:首先,转换并降采样阴影图中的三个近阴影切片或者说是太阳的阴影图的级联。然后,模糊这些切片并再次降采样。如你所见,结果比原始的要小得多,这对于体积采样时的质量和性能更为优化。
让我们看一下没有雾气的天气管理器中的一个样本场景。

在这里,可以看到雾效果本身,但是有一些不理想的人为痕迹。

这是在屏幕空间中模糊辐照度体积的结果。它已经好了一些,但仍然有一些人为痕迹暗示了体素的形状。

最后,启用了指数模糊阴影。

到目前为止,我们还没有提到是如何应用云层的。同时应用大气效果、云层和雾效,以节省在屏幕的可见区域写入的带宽。你也可以在这里看到,云层的阴影在雾中产生了美丽的光束。

我们 Composite Pass 的最后一个细节是,在最终的屏幕分辨率上应用更多的蓝噪声。当这个与时间抗锯齿后处理结合时,进一步消除了在云层中可见的 raymarch 痕迹。
04.4 反射
接下来,我们来谈谈反射。

反射对于完整地呈现湿润的视觉概念至关重要。正如之前提到的,使用天气系统的数据来判断哪些表面是湿的。然后,执行一个过程,其中光线使用 gbuffer 的位置和法线进行路径追踪,反射到屏幕空间的像素中。这主要完成了场景中的对象反射,例如在道路表面反射树木、天空和云层,以及在水面上显示的山脉。关于我们的 SSLR 以及混合光线追踪解决方案的更多信息,请参见我的同事 Stephanie 和 Ihor 的演讲:Performant Reflective Beauty(高效反射美学)。
但是,当没有屏幕空间反射的数据时,比如当我们从下图所示的角度观察水面时,会发生什么呢?

当没有数据驱动屏幕空间反射时,需要一个备选方案。备选方案是使用立方贴图,它需要显示一天中的时间变化、天气和云层,就像这辆车上的日落反射所示。立方贴图在构建时生成,位置由艺术家确定。然后,它们会与大气散射、云和雾一起重新照明。
立方贴图的数据以漫反射、法线和光滑度的形式进行烘焙和存储,以及一个低分辨率的深度。为了达到可接受的性能,每帧只重新照亮立方贴图的一个面,除了在特殊情况下,如电影摄像机的切换。这个过程从生成一个只包含大气散射、云和雾的天空立方贴图开始。这个只有天空的立方贴图用于海洋的备选反射。然后,使用烘焙的立方贴图数据重新照亮场景面。最后,将天空立方贴图合成到重新照亮的场景立方贴图中,得到右下角所示的结果。

逐步更新每帧的一个面,尝试在流入新区域时处理立方贴图。在重新照亮过程之后,立方贴图需要被过滤,以便在更粗糙的表面上产生模糊的反射。
我们遇到的一个大问题是阴天的照明和天气。有时候它就是不够暗,也可能显得平淡无味。

我们希望云层有对比度,有时还能看到蓝天穿透云层。这种情况的发生有几个原因。其中一个例子是我们创作的天空过于浑浊和灰暗。如果你想在自然光照中有蓝色,那就是从这里来的。但是要小心,否则大气散射在云层上过于蓝。我们还遇到了雾气淡化图像的问题。为了解决这个问题,根据云层覆盖度来淡化雾气。我们的天空照明过于明亮,因为云层永远不会到达地平线,而立方贴图在地平线处有一条蓝色的带子,这也增加了亮度。厚度均匀的云层也可能淡化图像,这就是为什么我们在暴风雨期间也将积云覆盖度限制在 0.8 以下。最后,我们的大气散射没有阴影,这本身也会引起问题。

04.5 粒子系统
让我们来看一下一些粒子效果。首先看的粒子效果是雨滴痕迹。我们尝试了折射、模糊和反射,但最终我们使用透明纹理来实现摄影中的雨景效果,这是我们的艺术方向。使用了两帧的变化和一个法线纹理来帮助照明。我们在玩家周围的一个圆柱体内循环使用雨滴痕迹,以确保无论摄像机移动得多快,雨粒子的数量都保持一致。为了在粒子的整个生命周期中增加持续的变化,使用了一个映射到世界空间的 3D 噪声纹理作为湍流。最后,雨的发射率和方向由天气管理器提供的雨和风值决定。

由于我们有许多室内和室外环境,需要在不合理的地方遮挡雨水。为了做到这一点,我们创建了一个雨影阴影图,可以参考它来确定雨滴痕迹或溅水是否被遮挡。这是一个方向性的阴影设置,用于覆盖摄像机附近的可玩区域,与雨的方向平行。然后将这个信息存储在常规的太阳阴影图集中,如右图所示。左边的彩色图像是查找表,用于将 UV 空间转换为图集纹理。

这个雨影阴影图只包含静态场景元素,并且每帧更新几个部分。
我们的下一个粒子效果是雨水击中表面时出现的溅水。最初,这只是由一个 GPU 粒子事件系统驱动的,当一个父粒子击中深度缓冲区、地形或水面时,它会发射新的粒子。这意味着在任何不透明的表面上都可以得到溅水粒子,包括车辆、角色和武器。然而,并不一定希望这些溅水出现在建筑物的侧面,所以我们加入了一个斜度因子。另一个问题是,没有得到足够的溅水,因为并不是检查碰撞的每一个粒子都会产生溅水。所以,我们在摄像机周围的一个体积内产生了更多的粒子,并将它们对齐到深度缓冲区。

对于雨滴痕迹和溅水,我们都希望有像素照明,特别是在夜晚。然而,这太昂贵了,而顶点照明又不够好。我们所做的是为每个粒子的顶点生成一个球形谐波光探针的系统。每个探针都计算 3 阶 SH 系数,并结合太阳、点光源和聚光灯。这给我们提供了比顶点照明更好但比像素照明便宜得多的效果。此外,可以采样粒子的法线贴图,以获得更多的方向性照明和更好的高光效果。这里我们看到了一个例子,右边是一个只有漫反射和法线纹理的粒子球体。

我们需要的下一个粒子效果是雷暴中的闪电。为了这个效果,创建了一个带状(或轨迹)系统。首先自动创建由单个粒子留下的轨迹,然后通过生成粒子并在每个粒子之间进行镶嵌来扩展这个系统。我们称这些为链接发射器。使用链接系统,在从顶部到底部移动的圆柱体中发射粒子,增加了体积中心的湍流,并在每个端部逐渐消失。但是,仅仅这样还不够真实 - 我们需要让闪电影响周围的世界。

首先,我们生成了一个全向光源并将其放置在场景中,以便闪电照亮环境。然后,必须处理闪电周围的云。在上采样过程中将闪电应用到云中,这样闪电就不会最终出现在历史缓冲区中并导致鬼影。为了确定在云中的哪个位置添加光线,我们使用了在云层 raymarch 通道的绿色通道中保存的太阳散射因子。实现基本上就像一个柱状光源。

以下是我们粒子系统的简要概述。总的来说,使用计算着色器在 GPU 上进行粒子的发射、模拟、排序和渲染,与 CPU 的交互最少。



04.6 海洋

海洋主要支持热带主题
- 但海洋受到天气的两种影响
- 贝福特等级
- 风向
- 屏幕空间细分受到限制
- 海岸线波浪存在问题
- 远处的平铺模式较差
- 我们保留这个用于淡水(河流,湖泊,溪流)
- 更多信息请参见 [Grujic18]
- 新的细分(SUBD)
我们为海洋选择的细分方案是基于 iSubD 细分方案。

基本算法将海洋网格从一个单独的两个三角形的四边形细分到你在相机附近看到的细分三角形。这是一个渐进式细化算法,当三角形靠近相机时,它会在每一帧中细分成更小的三角形,当它们远离相机时,它会将三角形合并成更大的三角形。

每一帧可以执行的操作数量有一个性能限制,所以我们用键填充了一个缓冲区来执行这些操作,但这导致了一个有很多空白空间的列表。这是我们发现 Subd 算法中尚未解决的一个区域,能够在不同类型的数据集上使用来自 GPU 粒子过滤的相同前缀和算法。在我们的世界中,有淡水与海洋相接,因此需要将之前的屏幕空间细分与 subd 细分进行混合。为了做到这一点,在这些细分类型的交叉点使用淡水的位移和法线作为输入,并在该范围内进行混合。然后使用简单的模板测试来避免水网格的冗余像素着色。
贝福特风力等级是一个经验系统,它将风速与其对水体的影响联系起来。在这里,我们将展示来自天气管理器的贝福特等级如何影响水的外观。这是贝福特等级 0。

贝福特等级本身是通过风速、振幅、规模和波浪度等数据进行调整的。这里贝福特等级 1…

这些值被用来驱动我们稍后将展示的波浪模拟。这里贝福特等级 2。

贝福特等级还包括海岸线波浪的设置,如振幅、频率、速度、陡度和波浪数量。这里是贝福特等级 3。

贝福特等级包括泡沫的设置,你可以看到随着波浪大小的增加,泡沫也在积累。这就是贝福特等级 4。


我们有每帧生成的海洋波浪缓冲区,用于创建波浪的运动。这些缓冲区可以映射回 CPU 进行物理计算和海平面的读取,但由于性能限制,我们禁用了这个功能。我们创建了世界空间的分形布朗运动(fbm)以在相机附近获得尖锐的波浪。使用世界空间缓冲区使能够直接生成法线,而不是像之前那样从屏幕空间生成。这使在近处和远处都能通过 mip 映射获得更好的细节。接下来,使用了快速傅里叶变换(FFT)模拟远处的波浪。我们调整了原来的实现,以响应贝福特等级和风参数。还在纹理中添加了一个累积通道,以在波浪上创建持久的白帽。为了防止远处的平铺,我们使用 FFT 缓冲区作为不同规模的级联,以及一个 Perlin 噪声波纹理。
这里只有开始时的 FFT 缓冲区。注意到远处的平铺效果了吗?

然后 FFT 级联,消除了一些平铺效果。

最后,基于 Perlin 噪声的滚动纹理,消除了剩余的平铺效果。这在模拟远处大片水面上风的效果方面做得很好。

与新的海洋技术相关的是海岸线波浪的引入,因为我们需要它们来构建热带海滩。我们有太多的海岸线需要覆盖,所以需要一种程序化的方法,这导致我们选择了 Gerstner 波浪公式。为了正确地放置和移动这些波浪,需要从海洋到海岸生成一个有符号的距离场来推导方向。

我们使用了振幅、速度和泡沫等参数来控制视觉效果。还为波浪添加了一个噪声参数,帮助打破波浪的平行性,否则将会有完美的圆形波浪接近岛屿。我们的系统支持多个波浪,但在最终的游戏中只使用了一个。
04.7 树木弯曲
我们想要讨论的最后一个渲染特性是树木弯曲,它与天气管理器提供的风向和风力一起工作。通过添加噪声和弯曲振幅来改进树木弯曲的设置。结合风值,这给我们提供了反映风暴天气条件的树木的夸张运动。这些设置由艺术家在设置树干骨架时控制。然而,运动有时会超过树的边界框,这是有问题的,因为叶子可能会突然被剔除并消失。为了解决这个问题,我们还包括了扩大边界框的设置。最终,这个特性仍然相当有限,因为:
- 边界框的扩大影响了性能,每帧都有更多的树保持可见。
- 远处树木的代理体不能弯曲,所以近距离的运动仍然需要允许一个可接受的过渡。
05 总结
最后,我想从技术艺术和编程的角度分享一些想法。
对我来说,真正推动我们成功的因素是限制复杂性,研究参考,尽可能减少生产依赖,并确定最大的收益,这样我们就可以专注于最能 “卖出” 天气的东西。对于未来的工作,我们可以进一步简化数据库,设置一个更容易调试和审查天气的方法,而不是让覆盖设置混淆我们,更多地利用湿度效果,推动预设更加极端,并研究程序化天气模式,而不是硬编码的预测。

对于渲染方面的收获,你们看到了我们需要维护的技术有很多。有许多相互关联的部分,需要分别和一起进行打磨。并且要尽早完成这些事情!这意味着你的数据格式、数据和流程。你不希望这些在生产过程中发生太大的变化。对于未来的工作,虽然列出了几个事项,但我认为最有趣的可能是同时使用相同的细分处理(tessellation)来处理水和地形,并使用相同的图集和虚拟纹理。

06 参考资料
- [Grujic2018] Water Rendering in Far Cry 5, Branislav Grujic & Cristian Cutocheras, GDC 2018
- [Moore2018] Terrain Rendering in Far Cry 5, Jeremy Moore, GDC 2018
- [Preetham1999] A Practical Analytic Model for Daylight, A. J. Preetham et. al., SIGGRAPH 1999
- [Wronski14] Wronski, B., Volumetric Fog: Unified Compute Shader-based Solution to Atmospheric Scattering, SIGGRAPH 2014, Advances in Real-Time Rendering
- [Hillaire15] Hillaire, S., Towards Unified and Physically-Based Volumetric Lighting in Frostbite, SIGGRAPH 2015, Advances in Real-Time Rendering
- [Bruneton08] Bruneton, E., Neyret, F., Precomputed Atmospheric Scattering, Eurographics Symposium on Rendering 2008
- [Schneider15] Schneider, A., The Real-time Volumetric Cloudscapes of Horizon: Zero Dawn, SIGGRAPH 2015, Advances in Real-Time Rendering
- [Bauer19] Bauer, F., Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution, SIGGRAPH 2019, Advances in Real-Time Rendering
- [McAuley19] McAuley, S., Advances in Rendering, Graphics Research and Video Game Production, i3D 2019, ACM SIGGRAPH Symposium on Interactive 3D Graphics and Games
- [Eggers10] Eggers, J. et. al., Drop Dynamics After Impact on a Solid Wall: Theory and Simulations, Physics of Fluids 22, 062101 (2010)

> 本文由简悦 SimpRead 转码