本页内容简介:MetalPop Games 软件工程师 Michelle Martin 介绍如何为一系列移动设备优化游戏,从而覆盖尽可能多的潜在玩家。
在开发移动策略游戏《Galactic Colonies》时,MetalPop Games 面临的挑战是:如何让玩家在低端设备上建造巨大的城市,而不出现帧率下降或设备过热的情况。了解他们如何在精美的视觉效果与稳定的性能之间找到平衡。
尽管现在的移动设备功能强大,但要以稳定的帧率运行美观的大型游戏环境仍然十分困难。在较旧的移动设备上,运行大型 3D 环境并且达到稳定的 60fps 可能挑战极大。
作为开发者,我们可以瞄准高端手机,并假设大多数玩家都拥有足够好的硬件来顺畅地运行我们的游戏。但是这会将大量潜在玩家排除在外,因为很多人仍在使用旧款设备。如果可以避免,您并不想放弃所有这些潜在客户。
在我们的游戏《Galactic Colonies》中,玩家可以殖民外星球,并通过大量的单个建筑建造巨大的殖民地。虽然较小的殖民地可能只有十几座建筑,但较大的殖民地往往拥有数百座建筑。
以下是我们开始构建管线时的目标清单:
- 我们希望创建涵盖大量建筑的地图
- 我们希望能在较便宜和/或较旧的移动设备上快速运行游戏
- 我们希望创建精美的灯光和阴影效果
- 我们希望构建简单且可维护的生产管线
游戏中光照良好对于 3D 模型的显示效果非常关键。在 Unity 中非常简单:设置好关卡,然后布置好动态光照就行了。如果您需要关注性能,只需烘焙所有光照,并且通过后期处理栈添加一些 SSAO 及其他吸睛元素。就这样了,出货!
在移动游戏中设置光照通常需要很多技巧和解决方法。例如,除非您是面向高端设备,否则最好不要使用任何后期处理效果。同样,充满动态光照的大型场景也会大幅降低帧率。
实时光照的成本非常高,哪怕是台式机也是如此。而移动设备的资源更加有限,根本无法承载您想要的所有出色功能。
因此,您不希望场景中太多不必要的花哨光照耗尽用户的手机电量。
如果总是挑战硬件的极限,手机会过热,为了保护自己,它会自动减速。为了避免这种情况,您可以烘焙每个不需要投射实时阴影的光源。
光照烘焙过程将预先计算(静态)场景的高光和阴影区域,并将信息存储在光照贴图中。如此,渲染器就知道让模型的哪些地方变亮或变暗,从而创建光照的视觉效果。
这种方式的渲染速度很快,因为所有成本高昂而缓慢的光照计算已在线下完成,在运行时,渲染器(着色器)只需在纹理中查询结果。
这里的权衡是您必须提供一些能够增大构建大小的额外光照贴图纹理,并且在运行时需要一些额外的纹理内存。您还将失去一些空间,因为网格将需要光照贴图 UV,这会导致其变大一些。但整体而言,您的速度会大幅提升。
但我们的游戏无法使用这种方法,因为我们的游戏世界是由玩家实时构建的。事实上,新的区域不断被发现,更多的建筑在建立,现有建筑在升级,阻止了任何类型的高效光照烘焙。如果是玩家能够不断改变的动态世界,只点击烘焙 按钮是没有用的。
因此,在为高度模块化的场景烘焙光照时,会面临许多问题。
Unity 中的光照烘焙数据会存储并直接与场景数据关联。如果您有个别关卡和预构建的场景,并且只有少量动态对象,那么这不是问题。您可以预先烘焙光照。
这在动态创建关卡时显然不行。在城市建设游戏中,世界不是预建的,基本上是玩家即时动态装配的,在哪里建什么完全由玩家决定。这通常是在玩家决定建设的地方通过实例化预制件来完成。
此问题唯一的解决方案是将所有相关的光源烘焙数据存储在预制内中,而非场景中。遗憾的是,没有简单的方法将要使用的光照贴图数据及其坐标和比例复制到预制件。
获取可靠管线处理光照烘焙预制件的最佳方法是在不同的单独场景(实际上是多个场景)中创建预制件,然后在需要时将它们加载到主游戏中。每个模块化部分进行光源烘焙,然后在需要时加载到游戏中。
进一步分析光照烘焙在 Unity 中的运行过程会发现,渲染光照烘焙的网格实际上只是向其应用另一个纹理,并且使该网格变亮或变暗(有时是彩色化)一点。您只需要光照贴图纹理和 UV 坐标 - 两者由 Unity 在光照烘焙过程中创建。
在光照烘焙过程中,Unity 会为个别网格创建一组新的 UV 坐标(指向光照贴图纹理)以及偏移和缩放。每次重新烘焙光照都会改变这些坐标。
要开发此问题的解决方案,了解 UV 通道如何运行以及如何最佳利用它们很有帮助。
每个网格都可有多个 UV 坐标集(在 Unity 中称为 UV 通道)。在大多数情况下,一组 UV 就够了,因为不同的纹理(漫射、反射、凹凸等)都会将信息存储在图像的相同位置。
但当对象共享某个纹理(如光照贴图)并且需要在一个大型纹理中查找特定位置的信息时,通常没有办法添加另一组用于此共享纹理的 UV。
使用多个 UV 坐标的弊端在于它们会占用额外的内存。如果使用的 UV 是两组而不是一组,则每一个网格的顶点都有两倍的 UV 坐标量。对于每个顶点,将会额外存储两个浮点数,在渲染时也会将它们上传到 GPU。
Unity 使用常规光照烘焙功能生成坐标和光照贴图。引擎会将光照贴图的 UV 坐标写入模型的第二个 UV 通道。请务必注意,此时无法使用主要 UV 坐标集,因为模型需要展开。
想象一个盒子的每一面都使用相同的纹理:盒子的各个侧面都具有相同的 UV 坐标,因为它们重复使用相同的纹理。但这对光照贴图的对象行不通,因为方框的每边都会分别被光线和阴影命中。每边在包含其个别光照数据的光照贴图都需要自己的空间。因此需要一组新的 UV。
要设置新的光照烘焙预制件,只需要存储纹理及其坐标,以免它们丢失,然后将其复制到预制件。
在光源烘焙完成后,我们运行经过场景中所有网格的脚本,将应用了偏移和缩放值的UV坐标写入网格的实际UV2通道。
修改网格的代码非常简单(见下面的示例)。
更具体:这是对网格的副本而不是原始网格进行的,因为我们将在烘焙过程中对网格进行进一步的优化。
副本会自动生成,保存到预制件,并为其分配自定义着色器的新材质和新建的光照贴图。原始网格保持不动,光照烘焙的预制件可立即使用。
因此工作流程非常简单。要更新图形的样式和外观,只需打开适当的场景,修改到满意为止,然后开始自动烘焙和复制过程。当此过程完成时,游戏将开始使用更新的预制件和更新了光照的网格。
实际光照贴图纹理由自定义着色器添加,它在渲染期间将光照贴图作为第二个光源纹理应用到模型。着色器非常简短,除了应用色彩和光照贴图之外,还会计算简单的伪反射/光泽效果。
下面是着色器代码;上图是使用该着色器的材质设置。
在本例中,有四个全部设置了预制件的不同场景。我们的游戏具有不同的生物群区,如热带、冰川、沙漠等,我们对场景进行了相应的分离。
要用于指定场景的所有预制件共享一个光照贴图。这意味着除了只共享一种材质的预制件之外,还有一种额外的纹理。因此,我们能够将所有模型渲染成静态,只需要一个绘制调用便可批量渲染几乎整个世界。
其中设置了所有瓦片/建筑的光照烘焙场景具有额外的光源,可用于创建局部高光区域。您可以按需在设置场景中布置多种光照,因为它们无论如何都会进行烘焙。
烘焙过程在自定义 UI 对话中处理,所有必要的步骤都要完成。它将确保:
- 将合适的材质分配至所有网格
- 隐藏整个过程中所有无需烘焙的对象
- 合并/烘焙网格
- 复制 UV 并创建预制件
- 正确命名所有内容,并从版本控制系统中签出所需文件。
从网格创建正确命名的预制件,使游戏代码可以加载并直接使用。在此过程中也会更改元数据文件,从而避免丢失对预制件网格的引用。
此工作流程可让我们按照需要调整建筑,以我们喜欢的方式照亮它们,然后让脚本处理一切。
当我们切换回主场景并运行游戏时,调整就会生效 - 无需手动参与或其他更新。
100% 光照预烘焙的场景有一个明显的缺点:很难有任何动态的对象或运动。投射阴影的任何对象都需要实时光照和阴影计算,当然,我们都想避免。
但如果没有任何移动的对象,3D 环境就像静态的,没有活力。
我们愿意承受一些限制,因为我们的首要任务是实现良好的视觉效果和快速渲染。要创建充满活力、动态的殖民空间或城市,不需要实际移动全部对象。大多数对象并不一定需要阴影,或至少不会注意到没有阴影的情况。
我们先将所有城市构建块拆分为两个单独的预制件:一个静态部分,其中包含大多数顶点、网格的所有复杂位;一个动态部分,其中包含最少的顶点。
预制件的动态部分是放在静态部分上面的动画位。它们未进行光照烘焙,我们使用非常快速、简单的光照着色器来创建亮度,以动态点亮对象。
对象也没有阴影,或者我们创建伪阴影作为动态位的一部分。大多数表面是平面,因此在我们的示例中没有大的障碍。
动态位没有阴影,但很少被注意到,除非您特意去看。动态预制件的光照也是完全伪造的 – 根本没有实时光照。
我们采用的第一条简便捷径是将光源(太阳)位置硬编码到伪光照着色器中。它是着色器确定需要查找并从世界动态填充的阴影。
使用常量肯定比使用动态值要快。这让我们获得网格的基本光照、光亮面和阴暗面。
为了让事物更加闪亮,我们为动态和静态对象的着色器添加了伪镜面/光泽计算。镜面反射有助于创建金属外观,还有助于传递曲面的曲率。
由于镜面高光是一种反射,因此需要互为相关的摄像机角度与光源才可正确计算。当摄像机移动或旋转时,镜面会变动。任何着色器计算都需要访问摄像机位置和场景中的每个光源。
然而,在我们的游戏中,我们只有一个用于镜面反射的光源:太阳。在我们的例子中,太阳从不移动,可以被认为是定向光。只使用一个光源时,只需考虑固定位置和其入射角,因此可大幅简化着色器。
更有利的是,我们在《Galactic Colonies》中的摄像机像大多数城市建设游戏一样,以顶视角透视场景。摄像机可以倾斜一点、放大和缩小,但不能围绕上方向轴旋转。
整体而言,视角总是从上俯视环境。为伪造简单的镜面效果,我们假定摄像机完全固定,并且摄像机与光源之间的角度始终不变。
这样我们可以再次将常量值硬编码到着色器中,获得简单的镜面/光泽效果。
对镜面使用固定角在技术上当然不正确,但实际上,只要摄像机角度不改变很多,是不可能真正识别出差异的。
在玩家看来,场景仍然是正确的,这就是实时光照的重点。
在实时视频游戏中,环境光照始终是为了视觉上看起来正确,而不是物理上正确模拟。
由于几乎所有网格都共享一种材质,因此我们在镜面纹理贴图中添加了来自光照贴图和顶点的大量细节,以指示着色器何时何地应用镜面值以及应用强度。使用主要 UV 通道访问纹理,因此不需要其他坐标集。还因为其中没有很多细节,所以分辨率很低,几乎不占用什么空间。
对于一些顶点较少的小动态位,我们甚至可以利用 Unity 的自动动态批处理,进一步加快渲染速度。
所有烘焙阴影有时可能产生新问题,特别是在使用高度模块化的建筑时。在一个玩家可以建仓库的游戏中,会在实际建筑上显示仓库中存放的货物类型。
这会造成问题,因为我们在光照烘焙对象上还有光照烘焙对象。光源烘焙-感知!
我们使用另一个简单的技巧解决了问题:
- 必须在平坦的表面添加额外对象,并使用与基础建筑相匹配的特定灰色
- 作为交换,我们可以在一个更小的平面上烘培对象,再将其放置在一个有轻微偏移的区域的顶部
- 灯光、高光、彩色光和阴影全部烘焙成瓦片
以这种方式构建和烘焙预制件可以获得包含数百幢建筑的巨大地图,同时保持超低绘制调用数。我们的整个游戏世界或多或少只用一种材质渲染,我们面对的状态是:UI 使用的绘制调用多于游戏世界。Unity 必须渲染的不同材质越少,游戏的性能就越佳。
这让我们有很大的空间在游戏世界中添加更多事物,如粒子、天气影响及其他吸睛的元素。
这样,即便是使用旧设备的玩家,也能够在保持稳定60 fps的同时建造拥有数百栋建筑的大城市。