腾讯魔方引擎专家GDC分享:我们在手机上做出了主机级别的天气系统

文/ 秋秋 2022-04-21 11:53:51

4月初,腾讯魔方工作室群自研的硬核FPS手游《暗区突围》又一次开放了测试,截至目前,该游戏在好游快爆已经拥有了超155万的预约量,每次测试都能登上其新游期待榜TOP 2;在TapTap也有超过72万的预约量和8.5分的评分。

对于魔方来说,《暗区突围》应该是他们研发过的、美术品质最高的写实题材游戏。此前魔方总裁张晗劲也曾告诉葡萄君,在射击这条赛道上,他们的资源投入很大,头也够铁。

1650599633212903.png

那么这些投入究竟效果如何?在最近的GDC大会上,魔方工作室群引擎中心的引擎专家陈家铭和引擎程序员应若晨,分享了他们如何做出《暗区突围》中主机级别的实时演算天气系统。

葡萄君将其分享内容整理了出来:

各位好,我们是腾讯游戏魔方工作室群引擎中心的陈家铭和应若晨。今天,我们想分享关于魔方工作室即将推出的手机游戏《暗区突围》中的天气系统。

QQ截图20220422121027.png

首先,我们会讲解动态大气散射和体积云的相关细节,以及将它们带到移动设备上的优化。之后我们将分享一些相关的天气效果,和天气系统的设计思路。


01 低开销的动态天空渲染方案

天空的大气散射是一个复杂的过程,涉及到很多物理计算,因此我会试着用简单的方式来解释。

1650599634841823.png

我们的大气由不同大小的粒子或分子所组成,天空的颜色是由这些粒子对太阳光的各种散射决定。

为了渲染出大气散射效果,我们一般需要在视点以光线行进计算每个采样点所接受的散射,然后计算它们的积。然而,这方法的计算量在游戏和实时应用上很难实现。

UE的大气散射优化

先让我们来看看Unreal Engine中的 SkyAtmosphere 方案,它使用了一系列查找表(Look up Table,LUT)来减少计算量。具体而言,UE 在每一帧中使用计算着色器生成4个基本的LUT,分别为透光度LUT、多重散射LUT、天空视图LUT和大气透视的3D LUT。

它不但基于物理,对美术也比较友好,同时在中高端移动设备上也能有良好的性能。但是老的设备带宽非常有限,负担不起每帧更新这么多的LUT。所以我们对UE的 SkyAtmosphere 做了一些优化:

首先,我们舍弃了大气透视数据,使用高度雾来代替它。舍弃掉这个数据后,所有剩余的LUT都是2D的,因此我们现在可以使用像素着色器来更新它们——这非常重要,因为许多移动设备仍然不支持计算着色器。

因为《暗区突围》局内的时间变化比较慢,所以我们分帧计算每个LUT,每帧只计算很少量的像素,在低端设备上也能承受。

为了进一步优化,我们将天空视图LUT 以半八面体参数化,并丢弃地平线以下的内容。这不仅节省了50%的光线行进计算量,也省了查找天空视图LUT时调用昂贵的平方根指令。

以下是原始版本和优化版本在一天中3个不同时段的比较。可以看到太阳周围有一些偏差,但这在游戏中不太容易看到。

1650599634152121.png

而且如下图所示,优化后的天空在渲染性能上有显著提升。从原始的1.35毫秒到优化后0.77毫秒,半八面体投影节省了将近40%的GPU时间。

1650599635382094.png

天气亮度与大气散射

美术有时需要将天空变暗,以获得更戏剧性的效果,很有可能他们会直接调整阳光亮度,但这不仅在物理上不正确,而且对我们的优化也不友好,会导致画面闪屏——天空颜色就像在一帧帧卡顿地变化。

我们的建议是先以固定的太阳亮度算出 SkyViewLUT,然后新增一个单独的美术乘数来调整最终亮度。如此操作,变暗的卡顿感消失了,我们也因此得到了更合理的散射结果。


02 体积云的实现

在游戏中,体积云的实现,可以归纳为这样几个问题:

  • 如何对云建模。我们需要一些方法,来定义云层各处的云的密度。

  • 如何计算光在其中的传播。特别是对于云这样的白色散射介质,光线可以在其中多次反弹,比普通的表面物体更难计算。

  • 这种Raymarching方法一般很昂贵,我们如何让它在手机上运行。

云建模

首先是如何对云建模:

我们使用Worley噪声制作3D噪声纹理,将它平铺在天空中,就可以定义基本的云密度。

1650599635589653.png

我们会使用基础和细节2层噪声,最终的噪声是通过基础噪声减去细节噪声得到的。细节噪声会平铺更多次,这样我们不需要特别高的分辨率,就可以获得足够的细节。

但是仅仅这样,它们不足以创造覆盖整个天空的云景。

于是我们引入了一张2D的Weather Map。在我们的系统中,Weather Map是由Cloud Mask组成的。每个Cloud Mask都有一个材质来指定绘制内容。

例如在下图中,你可以看到我正在拖动一个Mask。这个Mask的材质输出一个白色的Sphere Mask——因为白色意味着更高的覆盖率,因此那里的云变得更密集。我们也可以输出黑色,则该位置的云将被擦除。

0 (6).gif

为了方便控制不同天气下的云层,我们有两个全局参数:全局云覆盖率和全局云类型。这两个值将作为材质参数传递给Cloud Mask,并更改绘制内容。

除了Weather Map外,我们还用到一个称为Cloud Profile的纹理。

在现实生活中,不同类型的云在不同的高度有不同的形状。因为Weather Map只描述了XY平面的信息,所以我们需要使用Cloud Profile描述每种云在不同高度的形状。

1650599636850394.png

我们使用了UE4中内置的曲线工具,借此可以在编辑器中轻松地创建Cloud Profile贴图。上图是这个工具的样子,我们能看到每个参数都是一条曲线,其中x是归一化高度,Y是它的值。

云光照

云的光照有5个部分:单次散射、多重散射、全局光、大气透视、自发光。

因为性能限制,我们没有计算大气透视,游戏中使用的其实是一个简化的计算,因此不会在这里讨论。此外自发光也比较简单,这里不作讨论。

  • 单次散射

对于单次散射,我们通过太阳光、阴影和相位函数相乘来计算能量。

在阴影采样时,我们也不考虑细节噪声,并且在每一步上使用更高LOD。对于相位函数,我们用一个经典的方法,通过混合两个HG函数作为最终相位函数。

1650599636168126.png

天空环境光比较简单,只是根据采样点高度计算颜色。云越高,天空环境光越亮。这其实是UE4中的方法,我们发现它很好用。

下面是开关天空环境光的效果对比。我们发现,如果没有环境光,云体有很大部分阴影的时候,很难分辨形状;而开启环境光后,云在阴影部分添加了漂亮的蓝色色调,造型就清晰多了。


1650599637852525.png

1650599637903808.png

上:无天空环境光 下:开启天空环境光

我们还有一个地面环境光,用于来模拟地面反射的光线。这是以同样的方式计算的,但是把高度参数反转,这样越低的云层受光越强。

地面环境光的颜色是通过将地面视为纯色的Lambert表面,计算主光源的反射得到的。

这里我们可以看到没有地面环境光时,云的底部比较暗。而打开地面环境后云的底部变亮,更接近我们在日常生活中的感觉。

1650599638658989.png

1650599638931612.png

上:无地面环境光 下:开启地面环境光

  • 多重散射

多重散射指的是光子在云中弹射了不止一次。模拟多重散射对于正确的外观非常重要——云看起来这么白,就是因为当光束击中云粒子时,大部分会被弹开并继续进入云内,而不是被吸收。

1650599639612963.png

特别是在云层深处,多重散射对外观的贡献会变得更加明显。这让云有一些反直觉的效果,比如这张照片中,云的内部比外部更亮,就是因为其内部的多重散射更加明显。

为了模拟这种效果,首先,我们参考了在寒霜引擎中的方法来计算一个大致的多重散射的强度。这个方法可以以较少的开销完成一个近似的多重散射的计算。

1650599639718100.png

这是没有多重散射的效果,因为光线无法到达云层深处,云看起来更像烟。

1650599639624321.png

这是在有多重散射的情况下,云层的整体亮度更准确,更像现实生活中的云。

接下来我们参考地平线的方案,添加了暗边的效果。注意下图中圆圈的部分,在没有添加暗边效果之前,我们很难辨别这些部分的形状;

1650599640265098.png

但是加上了效果后,这部分就有了更多的细节。

1650599640895927.png

体积云性能优化

基于Raymarching的体积云,在它的每一步都涉及大量的计算。我们一般需要至少每条光线64步,每一步都涉及许多纹理读取和ALU指令。在手机上,这样的计算量难以取得较好性能,以满足我们渲染天空的预算要求。

我们的方案是,使用半八面体映射,把Raymarching的结果缓存在一个512 x 512的2D纹理中。由于天气以及太阳方向在我们的游戏中变化较慢,因此我们可以将缓存分多帧更新;

另外,我们需要两张RT,一张用于渲染天空,一张用于分帧的Raymarching,并且在Raymarching完全结束时交换两张RT。

1650599641470420.png

这么做的优点是什么?

  • 我们可以把缓存在任何动态反射效果中重复使用。例如《暗区突围》使用了平面反射,天空在水中的倒影不需要再做额外的Raymarching。

  • 云的运动在八面体空间中相对较慢,可以与重投影技术很好地结合。

同时,为了有效地限制计算量,以避免移动端的GPU过热。我们采用了以下策略:

1.棋盘渲染技术。这个技术有助于节省每帧需要计算的 50% 的光线。

1650599641857017.png

下面的图片展示了棋盘格式更新的效果。

0 (1).gif

2.切片。我们通过将RT分成4-16个切片,我们每帧仅更新一个切片,来实现分帧更新。

1650599644428984.png

然而,切片也会导致云在移动时看起来较为卡顿。为了解决这个问题,我们可以插值缓存的采样方向。

0 (4)~1.gif



例如,云分4帧从A移动到B点,假设我们在当前帧中查看B点,我们现在比上次更新周期过了1帧。由于云的移动量是已知的,因此我们可以向后追溯并找到点C,从视点到C的方向将是重新投影的方向。

下面的图片展示了插值后的效果。

0 (5)~1.gif

3.时序升采样。如前所述,不加优化的话,我们在Raymarching时可能需要64步才能有较好的效果。所以我们想稍微优化一下,节省GPU 预算给其他效果。因此我们采用类似于 TAA 的技术,在每一帧中,我们对每条光线的起始点应用一个全局偏移。并且将结果和之前帧的结果混合,来得到最终结果。

1650599644935306.png

我们还使用了重投影,来减少云在快速移动时产生的“鬼影”问题。

1650599645619056.png

0 (3)~1.gif

无重投影,可以看到云层移动时重叠的“鬼影”

0 (2)~1.gif


重投影开启,“鬼影”情况大大减少

4.压缩最小化内存存储和带宽。我们还使用了一些方法,来压缩体积云的存储大小。

通常我们使用半浮点型来存储射线行进结果,单张512 x 512的RT需要2MB,这对于主流设备来说是可以接受的,但是在旧设备上还是有优化空间,因此我们想尝试支持使用RGBA8的格式。

1650599647740507.png

最终,我们将散射的结果除以相位函数,并且预计算一个曝光值来降低整体亮度,以及Gamma压缩等方法,成功将贴图压缩进了RGBA8的格式里。

5.可扩展性和性能。下面是在iPhone 11上测试的不同设置的一些性能。

1650599647117599.png

可以看到在优化前,iPhone11需要大概10ms来完成一帧的更新。而在开启切片、时序升采样等优化后,我们可以将开销控制在1ms以内。


03 暗区突围中的动态天气效果

下面让我们来看看《暗区突围》项目中的一些动态天气效果。

云影

因为我们将体积云的结果保存在RT中,所以我们可以通过一些简单的计算,把结果投射在地面上,做出云影效果。

1650599647348314.png

它包含雨滴粒子和湿润效果,雨本身是用粒子渲染的。

湿润效果通过调整材料参数来达成,参考了Waylon的方法。遮挡贴图则使用了CSM Scrolling技术,来支持快速更新遮挡贴图。

然而这些细节仍然需要持续的打磨和优化。比如遮挡采样开销过高,没有太阳光和AO的情况下,场景显得太平等等,还需要更多的优化。

QQ截图20220422122545.png

闪电

接下来是闪电,我想分享一些在实时渲染中制作真实闪电的关键点。

下面的动画来自YouTube上一个名为the slomo guys的频道,它清楚地显示了一次完整的打雷的过程。

微信图片_20220422122530.gif

现实生活中的闪电,有三个阶段:

  • Lightning Leader。这是一条像蜘蛛网一样的路径,从云端延伸到地面。实际上肉眼是几乎看不见这个现象的,因为它太快了。但这很酷,所以我们决定实现它。

  • Return Stroke。当Lightning Leader到达地面时,就产生了我们所说的“闪电通道”。巨大的电流从中穿过,把空气加热到非常高的温度,出现闪电和雷声。

  • Re-Strike。在Return Stroke之后,可能会有若干次Re-Strike。Re-Strike沿着同样的通道发生,通常比Return Stroke小,平均发生3到4次,产生闪烁的效果。

为了创造闪电,我们需要对它建模。这里我们使用分形算法,来创建闪电通道。

算法本身很简单,就是不断地分裂线段,每次分裂时偏移中间的节点。

在分裂一段节点时,随机地创建当前节点到末端的新线段,并且把它旋转一定角度,即可得到分支的效果。

1650599648290715.png

然后,我们将结果转换为四边形组成的网格,同时用顶点颜色存储一些信息,来模拟从云层到地面的生长动画。

1650599649830195.png

这是它最终动起来的效果。我减慢了Lightning Leader的速度,这样我们就能看得更清楚了。

微信图片_20220422122535.gif

这是它在游戏中的样子。

0.gif

场景是通过直接增强主光源亮度来模拟闪电的光照的,强度则基于相机和闪电之间的距离计算。灯光方向不会变化,因此物体的影子并不正确。但是闪电发生得很快,所以也不是特别明显。

为了照亮云层,我们计算云和闪电的距离。云的位置是通过和云层求交来计算的,因此结果不是百分之百准确,但对于移动端来说已经足够了。


04 创作工具

接下来的部分,我想分享一些在我们的系统中关于用户体验和软件工程的东西。

天气创作

首先是我们如何在我们的系统中定义一个天气。

一般的想法是,我们可以有一个天气参数的结构体,里面包含了太阳强度、云量、雾密度等。这样不同的天气就是一组不同的值。

但是在我们的实现中,我们希望用户可以拥有最大的灵活度,所以我们避免了这样的硬编码。

在我们的系统中,用户可以使用UE的Level Sequence工具,直接将任何属性添加到系统中。系统将基于一天中的时间或太阳角度,来采样该Sequence,这样我们就可以让天气参数跟着时间变化。

1650599650662735.png

并且,我们允许多个Sequence同时生效,多个Sequence之间有叠加顺序,较高Sequence中的值将覆盖较低Sequence中的值。同时每一层都有一个不透明度,通过控制不透明度值,可以控制Sequence的强度,就像Photoshop中的图层一样。

这样一来,用户可以将不同的天气元素参数,组织在不同的Sequence中。并且通过不同的不透明度组合出最终的天气。

1650599650774172.png

举例来说,一个晴朗的天气的预设,它保存的不透明度可能是,基础层是1,云是0,雾是0.2等等。

我们再做一个叫做多云的预设,它的云、雾两个Sequence的不透明度较高;再做一个叫做雨天的预设,它的雨层会有一定不透明度。

Gameplay逻辑最终看到的就是这些预设,如果Gameplay要求改变天气,系统就只是在这些不透明度值之间过渡。

天体系统

接下来是天体系统,它帮助我们定位太阳、月亮或天空中任何东西。

这个系统允许你为场景定义时间和地理属性,包括一天中的时间、一年中的时间、时区、纬度和经度等。有了这些设定后,该系统可以根据天体属性,自动将它定位到正确的位置。

图像参考捕获

这个功能允许你把摄像机放在关卡的任何地方,只需轻轻一点,系统就会遍历时间和天气,渲染成图像并进行保存。同时它还支持立方体贴图和HDR格式。这样,美术可以更方便地迭代天气效果。

1650599651537132.png

建议

最后是一些实现类似的天气系统插件的建议。

因为每个项目都有自己非常具体的需求。例如,并不是每个项目都需要体积云或者基于物理的大气。而且有些项目可能对材质有一些特殊的要求等等。

因此最好不要尝试做一个开箱即用的解决方案,否则类似的需求使得你必须为每个项目创建分支。在虚幻引擎中,材质和蓝图很难管理,因为它们是二进制的,维护多个分支会很麻烦。

我们最终使用了一个方案,尽可能的去解耦所有的功能,包括把所有的功能特性都分成Actor、Component或Material Function。用户可以在蓝图编辑器或材质编辑器中组装这些功能。

这样做,虽然让用户在早期带来了多一点的设置工作,但将来会少很多麻烦。

我们分享了在我们的天气系统中,如何渲染真实的天空,以及一些基本的天气效果,但这不是终点。我们也在致力于打造动漫风格的天气系统。并研究如何将把更极端的天气条件,如暴风雪,沙尘暴移植到移动端实现。

最后,我们要感谢魔方和魔方引擎中心为我们提供的支持与帮助。

消息来源:
Alex Matveev
2022-06-06 16:27:13
不合规
审核中
@苏某某: 她在音乐方面的喜好,以及对天文的兴趣,也源于这部动画的影响。一直很喜欢爵士乐的她突然开始想
乐方面的喜好,以及对天文的兴趣,也源于这部动画的影响。一直很喜欢爵士乐的她突然开始想,没有系统了解过此类音乐的她怎么会喜欢上 呢?后来听完《美少女战士》原声带后才发现,“原来我在那么小的时候
评论全部加载完了~