Unity 技术开放日 | 太空科幻SLG手游的渲染与优化(下篇)
Unity官方 / 2021年6月17日
在 Unity 技术开放日-北京站活动中,我们有幸邀请到壳木游戏为大家介绍 SLG 手游的优化策略。壳木 CTO 邝圣凯和壳木技术平台主管陈石以《Infinite Galaxy》为例,详细介绍了太空 SLG 游戏的优化思路和细节。
本文节选了部分精彩内容,完整录播已上传至 B 站。
陈石:我直接接着邝圣凯刚才介绍的部分直接进入一些细节上的讨论。
我们先看一下刚才介绍到的恒星系在 IG 游戏里面大概是什么样的。
这个是一个 30 级玩家的城堡,拉远的时候看到 LOD 切换过程当中的变化,图中的聚集程度还是比较常见的。在恒星系里面还有很多的环境因素,会给负载带来压力的主要是陨石块,其数量是非常可观的。
我们先介绍一下光照。
我们的游戏场景在外太空,所以我们把主光源抽象成为一个点光源,但实际操作时,直接用点光源会得到这张图的样子,它导致受光面积比较小,背面特别黑。
然后我们尝试用了一些比较常规的办法,比如说用天光盒作为环境光输入,提升环境光的亮度。但是这样再提升环境光亮度的话,细节就会被吞掉。
还有一个问题是光面的问题,其实我们整个恒星环境,主光源的体积非常大,我们不是把它当成一个单纯的点光源,而是当成球光源。我们最后引入了类似于基于图片的这种光照思想,把 Matcap 引进来作为环境光的输入,对光照方程也做了修改,最后得到下面三个结果。
这三个结果基本就是在不同星系里面玩家建筑的表现,大家可以看到在背光的情况下,其实这些光照的细节还是能够得到保留的。
有了光之后,就是处理阴影。
先看一下这个图,这个图里面是没有阴影的,没有阴影会导致玩家是没有方位感的,他们不知道现在这个建筑跟我的主光源的位置关系是什么样的,所以阴影对于我们来说也很重要。
IG 项目带给我们的挑战在于它是点光源,和传统 SLG 不一样。传统的 SLG 大部分是方向光,只要方向不变,烘培的结果是可以支持建筑在一定程度上移动,改变位置的。但点光源不行,点光源一旦烘焙,只要建筑的位置有移动,这个阴影就失效了。同时,如果打开实时阴影,产生的开销实在太大。IG 项目中唯一的好处是,在太空环境中没有地表,所以我们其实只要考虑自阴影就行了。
对于阴影我们是这样处理的,烘了 16 个阴影贴图。大家可以看到下面这张白色的图,我们是根据模型的正方向,将其切成 16 个等分,得到 16 张阴影贴图。在整个使用过程当中,当模型的正方向和光源的正方向发生改变的时候,我们就会把阴影贴图切换到最接近的那一张去。
但这个最初的方案会导致阴影变化的时候会有一个跳变,极不自然。
所以我们再一次引入了 Job system,这样我们不光能减少性能上的压力,最重要的是对 16 张光照贴图进行压缩。在 Job system 进行实时解压,同时解压两张——解压与当前的角度最接近的两张,用这两张作差值,这样它的阴影就不会跳变,而是渐变的过程,类似实时阴影,同时开销又比实时阴影小非常多。
建筑方面基本上就是这样,我们接下来聊一下我们的小行星带。
这条小行星带是在缓慢转动的,我们的每个恒星系里的陨石个数大致是 4K 到 1W 之间,再加上每个对象都是实时的,每一帧都需要转动,所以这个开销还是蛮大的。
我们首先想到的用 Draw Instance 的方式来做,但是这个方式的上限是 1024。除此之外,它其实是我们提交多少它就绘制多少,最后到了三角面剔除的时候才会剔除,这个部分的带宽及各方面的开销是很大的。
最终的方案是,既然要分块,我们就分得细一点,分块结束之后用四叉树组织起来,这样 CPU 计算 clipping 的时候也会快一些。我们实际上是在 CPU 计算 clipping 之后,把需要显示的陨石分块提交到 GPU,也会带上 LOD,在材质里面直接把自旋转的问题解决了,最终优化下来这块开销就基本可以忽略不计了。
下面我们介绍一下玩家城堡的优化,IG 这个项目特殊的地方在于太空里面有大量的能量效果。我们的镜头支持拉近,拉近时候有大量飞行轨迹——模拟小飞行器巡逻的状态。但我们最开始拿到的美术设计,美术给我们的资源是一个六切面的模型,一整套 PPR 贴图还有 220 个 Particle system,最终造成在一个屏幕当中显示 50 个左右玩家城堡的时候,仅 Particle system 就能达到 1W 多个。
我们首先分析了一下 Particle system,发现其实最终使用 Particle system 的部分只有 8 个,是非常有限的,其他的部分可以通过把发生的片来作为合并,然后分布到 8 个动画材质上去,就可以达到最终的效果。剩下的 LOD 过程就是普通操作。
接下来介绍一下动态的部分,主要就是 IG 里面的战斗。
这其实是一个常见的战斗过程,一个成熟的联盟大概有五十个玩家,在特定时段联盟里的 50 个玩家会派出自己最好的舰队一起攻打其他联盟或者某一个关卡,我们现在看到的就是每个玩家最强的舰队,派出来是 20 到 50 艘船的规模。实际上这样一场战斗至少是 1000 个舰船模型的规模,而且舰船还都不一样。进入到战场之后,压力也不会减轻。
这种情况下,我们会在 UI 的元素上面做一些简化,但模型,包括光影这些细节还是一直保持一致的。大家可以看到刚才邝圣凯介绍的战斗过程当中光影的变化,这些变化其实主要是在一些对于玩家来说比较关键的节点,比如自己的旗舰损毁了这样一些场景的反馈。
针对这样的游戏体验需求,我们做的第一件事情就是对我们的舰船做更精简的优化,因为其实它的数量和玩家的城堡比起来是更高的一个数量级。这个情况下除了对模型和特效材质做优化我们还加了更简化一级的 LOD,参照 Impostor 概念是 2D 的片,我们还是保留了法线和高光,实际上它跟模型使用的是同一个材质,这么做的好处是我们在镜头快速拉远的时候不会看到大面积的像素跳动,光感还是一样的。
这过程中我们遇到一个坑,就是 Trail Renderer,我们用它来表现舰船的尾焰。Trail Renderer 的特性是在全生命周期,都有 Mesh 更新存在,即便隐藏掉也会存在。所以在隐藏时必须手动关闭 emitting,如果不关闭的话,CPU 的负载就都一直在。
下面看一下 IG 舰队移动的细节。
我们刚才也聊到,舰队本身船体是没有动画的,这会带来一些便利,但实际上这个舰队的整个形态在整个运动过程当中是需要保障的。
我们也试过很多方案,比如类似鸟群、鱼群模拟,甚至 LVO,最后综合开销下来,我们还是以舰队的固定形态加上曲线差值的方式来实现最终舰队的移动。
大家也看到即便是这样,即便我们在设计上和算法上进行很多优化,但其实一旦到了刚才 1000 艘船的量级,它也是会卡顿的。针对这个情况,我们是把整个运动和曲线拟合的算法全部移动到 Job system 中,这样的表现效果还是比较让人满意的。
战斗的这块除了可见的优化之外还有一些优化是不可见的,我们看到这个视频里面是一个比较常见的 IG 项目里面会发生的本地战斗。
本地战斗里面会出现类似于 100 个舰船打另外 100 个舰船的场景,这种场景带来的 AI 方面最大的开销就是会突发地需要寻找目标,在对象一旦出现的时候,比如 100 寻找目标时间的复杂度就是 O(n 平方),无论是什么样的机型都会有瞬间的帧率波动。
针对这个情况,我们首先想到用 KDTree 来简化目标查询,大家知道它是二分差,实现复杂度降低很多,但是 KDTree 构建也是蛮大的开销。我们把 KDTree 的构建放到了不同帧中,因为这是主线程逻辑,不可能放到另外一个线程,所以我们放到不同帧里面去,每帧只算一部分,算好之后再切换,马上开始计算下一个 KDTree 的结果,这种方式最终实现帧率的稳定,这个会对战斗结果有一定影响,但目前我们的需求里面看到的就是几帧的延迟是完全可以满足设计需求的。
陈石老师还分享了世界地图、体积光、面部捕捉等方面的精彩内容,欢迎大家前往 B 站观看完整版进行学习。