如何使用 Unity 和 Arm 分析工具解决移动端游戏性能问题(下篇)
上篇分享到,通过 Performance Advisor 的报告,我们发现着色器成本过高,因此可以通过降低它们的精度进行优化。在 Streamline 中,我们可以使用 Mali 使用变化图表来查看使用 32 位(高精度)或 16 位(中等精度)插值时的循环数。我们可以看到大多数循环使用了 32 位插值。16 位变量的插值速度是 32 位变量的两倍,存储插值结果所使用的着色器寄存器空间也仅为后者的一半,因此我们建议尽可能在片段着色器中使用 mediump(16 位)变化输入。
Mali Offline Compiler 着色器分析
03 使用 Mali Offline Compiler 进行着色器分析
为了解着色器的情况,我们可以使用 ARM Mobile Studio 的静态离线编译工具来快速分析着色器程序。
首先,你需要从 Unity 提供的编译文件中获取着色代码,然后对该文件运行 Mali Offline Compile:
在 Unity 中选择要分析的着色器,可以直接从资产文件夹中选定,或选定素材,单击齿轮图标并选择 Select shader。
在检查器中选择 Compile and show code。编译的着色器代码将在默认代码编辑器中打开。此文件包含多个着色器代码变量。
将顶点或片段着色器变量从此文件中复制到一个新文件中,增加扩展名 .vert 或 .frag。顶点着色器从 #ifdef VERTEX 开始,片段着色器从 #ifdef FRAGMENT 开始。它们以各自的 #endif 结尾。(在新文件中不要包括 #ifdef 和 #endif 语句)。
在命令行终端中,对该文件运行 Mali Offline Compiler,指定要测试的 GPU。例如:malioc –c Mali-G72 myshader.frag 更多说明,敬请参阅“Mali Offline Compiler 入门”。
Mali Offline Compiler 入门:
https://developer.arm.com/documentation/102468/latest
我们选择分析在敌方 NPC 死亡时产生溶解效果的片段着色器。以下是 Mali Offline Compiler 报告,其中突出显示了一些值得关注的部分:
我们可以看到只有 2% 的算术运算是在 16 位精度下运行的。所以如果我们将精度从 highp 降低到 mediump,着色器运行效率将更高。这样一来不仅降低了能量消耗和寄存器压力,并且使性能加倍。有些情况下总是需要用到 highp,例如位置和深度计算;然而在许多情况下,即便精度降低到 mediump,画面上也几乎看不到明显的差异。
该报告大致分析了 Mali 着色器核心中主要功能单元的循环成本。在报告中我们发现运算单元是使用最频繁的。
在着色器属性部分,我们看到该着色器包含只依赖文字常量或统一值的统一计算。这对绘制调用或计算调度中的每个线程产生相同的结果。理想情况下,这种统一计算应该转移到 CPU 上的应用程序逻辑中。
我们还看到着色器能够修改片段覆盖遮蔽,它通过运用 discard 语句将片段降低到 alpha 阈值以下来确定每个像素中的哪些样本点被片段覆盖。具备可修改覆盖功能的着色器必须使用后期 ZS 更新,这不仅会降低前期 ZS 测试效率,还会降低同一坐标下后续片段调度的效率。因此,我们应尽可能减少在片段着色器中使用 discard 语句和 Alpha-To-Coverage。关于使用 discard 语句的建议,敬请参阅 Arm Mali 最佳实践指南中关于使用 discard 语句的建议。
关于使用 discard 语句的建议:
https://developer.arm.com/documentation/101897/v2-2/Shader-code/Discards
04 使用 Graphics Analyzer 进行图形 API 调用分析
在 Arm Mobile Studio 的 Graphics Analyzer 中能够看到应用程序的所有图形 API 调用,并且可以逐步调用看到整个场景是如何一步步生成的。这大有帮助,可以用来识别那些实际显示尺寸很小,或者距离相机很远,却过于复杂的物体。以下是我们在此款游戏中发现的一些示例:
场景远处角落里的砖块是由几何体建造的,共使用 2064 个顶点。砖块细节在最终呈现的画面中并不十分显眼,因此对其的处理完全是浪费资源。
不仅如此,地砖也存在同样的问题- 每块地砖有 1170 个顶点,然而即使对象靠近相机,这样复杂的物体也并没有给整体画面带来多少提升。在这里舍弃三角形,转而使用法线贴图来呈现凹凸和边角会更加有好处。此外,我们还能够看到这些对象是使用独立绘制调用绘制而成的。通过将对象批处理或使用对象实例化来减少绘制调用的数量能够提高性能。
我们再来看看另一个例子:场景后面的雕像,每个雕像包含 6966 个顶点。可以看到网格相当复杂,当玩家靠近雕像时视觉效果非常出色,但仅从当前相机位置来看,这些雕像却毫不起眼。当这些对象距离相机如此遥远时,使用网格 LOD 来呈现就能节省大量的算力。
为大量类似的对象降低复杂性可以大大节省几何处理负担,从而减少所需的片段着色量。这样一来不仅会降低片段工作量,增加每秒帧数,还可以减少安装游戏所占用的空间。
游戏优化
我们从不同方面找到了若干个对游戏进行更改从而提高性能的办法。下面是我们选择实施的内容及其具体做法。
固定时间步长
固定时间步长间隔与帧速率无关,用于控制何时执行物理计算和 FixedUpdate() 事件。在默认情况下,固定时间步长设置为以每秒 50 帧的帧率运行。尽管 50 帧,甚至 60 帧的帧率在高端移动设备上也能够实现,但更多主流设备的帧数为每秒 30 帧,本文即针对这一数值进行探讨。
进入 Edit> Project Settings,然后选择 Time 类别,并将 Fixed Timestep(固定时间步长)设置为 0.04。如此一来,物理计算、FixedUpdate() 事件和更新都将确保同步运行。
在 Unity 中对固定时间步长进行调整后,主游戏循环的固定更新部分每帧仅调用一次,平均为 1.5 毫秒。与之前的 12 毫秒相比,这不仅仅是一个巨大改进,也是针对常见性能缺陷的简单解决方案。
资源 (Resources) 文件夹
在应用程序启动时,内置场景或资源文件夹中引用的所有对象数据都会加载到实例 ID 缓存中。这些资产被视为一个大型资产包,因此总是有元数据和索引信息被加载到内存中。一旦使用了这个包中的资产,就永远不会从内存中卸载。
如果需要降低内存消耗,建议使用可寻址资源系统来处理资产和资源,这样我们就可以有效地从内存中卸载无用的内容。
可寻址资源系统:
https://learn.unity.com/project/getting-started-with-addressables
GPU 实例化
在我们的环境中有诸多对象屡次出现。墙壁、地砖和其他环境道具都需要重复使用,从而构建场景。我们可以为对象材质启用 GPU 实例化来保存绘制调用。GPU 实例化使用少量绘制调用渲染相同的网格,并允许每个实例具备不同的参数,例如颜色或比例。这种修改可以提高 CPU 性能。在下图中能够看到启用 GPU 实例化之前的 Performance Advisor 数据。
GPU 实例化:
https://docs.unity.cn/cn/current/Manual/GPUInstancing.html
启用 GPU 实例化之前的数据
接着可以看到在应用程序的同一部分启用了 GPU 实例化的结果,这一增益看似微小却大有用处,离我们 30 帧的目标更近一步。
渲染纹理
渲染纹理是一种向 UI 以及许多其他用例添加 3D 元素的方式。如果有用于渲染纹理的相机,那么当该相机不用于显现画面时请确保将其禁用。我们无需渲染用户看不到的内容。使用 Graphics Analyzer 或 Unity 的 Frame Debugger 确保让这些纹理只有显示在画面中时才会更新。
渲染纹理:
https://docs.unity.cn/cn/current/Manual/class-RenderTexture.html
建立对象池
为了避免一而再地重复创建和销毁相同的对象,给 CPU 增加额外负担,我们可以尝试建议对象池。建立对象池是一种设计模式,你可以预先创建需要的对象,预先完成 CPU 的工作。然后并不销毁它们,而是重新放回池中,以便下次又需要同类对象时再次使用。这是一种解放 CPU 算力的极好方法,能让 CPU 转而处理游戏中更为重要的任务。
建议对象池:
https://learn.unity.com/tutorial/introduction-to-object-pooling
建立对象池后,在 Unity Profiler 捕捉到的画面中,在屏幕上当敌人一波又一波袭来时并没有出现可识别的峰值,对帧速率也没有产生明显的影响。
网格细节级别(LOD)
当网格出现在屏幕上时,GPU 需要花时间渲染网格中大大小小的所有三角形。在镜头和资产可以移动的手游中,这通常会占用大量的 GPU 资源去渲染小到无法在画面中看到的三角形网格。
我们可以使用网格细节级别来解决这个问题。让你的游戏在相机远离资产时利用更简单的网格,通过降低网格复杂性减少 GPU 的渲染负担,同时减少每帧的顶点数,让更大的三角形进行像素处理。这样做不仅提高了效率,也不会有损场景的美观程度。
网格细节级别:
https://docs.unity.cn/cn/current/Manual/LevelOfDetail.html
网格细节级别
有关其他资产优化技巧,敬请查看 Arm 的游戏美工指南 。
游戏美工指南:
https://developer.arm.com/Gaming%20Graphics%20and%20VR/#aq=%40navigationhierarchiescategories%3D%3D%22Gaming%2C%20Graphics%20and%20VR%22&numberOfResults=48&f[navigationhierarchiescontenttype]=Guides
纹理图集
如果你知道将会在同一场景中使用某些具有相同材质属性的资产,那么就可以对它们进行批量处理。将它们的纹理数据整合到一个纹理图谱中,对它们进行一次性绘制来节省绘制调用,与多个单独的文件相比,这样压缩后可以节省占用的空间。
着色器的浮点数精度与半精度
在编写自定义着色器或使用 Shader Graph 时,你可以选择浮点数精度或是半精度。要尽可能选择半精度,这可令着色器的性能更高,然而你可能需要使用浮点数精度来处理世界空间的位置或深度计算,敬请注意。
Shader Graph:
https://docs.unity3d.com/Packages/com.unity.shadergraph@7.5/manual/index.html
在 Shader Graph 中选择半精度
Integrated 和 Post Processing V2 功能集
当着手为项目规划后期处理效果时,有两个选项可供选择:旧版 Integrated 功能集或新的 Post Processing v2 功能集。以下是使用 Integrated 功能集处理的游戏。
Post Processing v2 功能集:
https://docs.unity3d.com/Packages/com.unity.postprocessing@3.0/manual/index.html
未使用 Post Processing V2 的 GPU 分析器
每 3 到 4 帧,我们就会看到垂直同步出现一个峰值,此时系统正在等待帧渲染。相比之下,使用相同的游戏效果,采用 Post Processing v2 功能集可得出如下分析器数据。
使用 Post Processing V2 后的 GPU 分析器
这次分析器数据显示出的结果要好得多,因为 Post Processing v2 针对移动设备经过了优化。在你的项目中应用 Post Processing v2 将会获得最好的后期处理性能。
后处理效果
但在追求效果的同时确保性能也同样重要。毕竟这些效果可能成本高昂。在游戏中添加后期处理效果能为项目锦上添花,改善视效。在大众化设备上关闭后期处理效果不仅省电效果显著,还能防止玩家手中的设备过热。
进行其他优化后,我们仍可以看到某些区域存在峰值。通过使用二进制搜索,打开和关闭各项元素,我们最终发现了两点:一是项目在使用后处理效果。这对总时间有所帮助,然而只要我们关闭反锯齿,帧率最终就会趋于稳定。如此一来,即使是在我们用来测试的最低规格设备上也可以保留一部分后期处理。
GPU Profiler 优化后的示例
在优化游戏后,我们再次在 Arm Mobile Studio 上运行来观察有什么改变。此时 Performance Advisor 报告显示我们的平均帧率已经达到了每秒 28.9 帧(之前为 17),并降低了整体片段消耗。游戏中某些部分的片段活动仍然很高,所以我们还要继续优化。有了优质的数据进行指导,相信我们通过分析能够优化这些部分,进一步提高性能。
Arm Mobile Studio - 捕捉摘要
现在每帧的顶点数远远低于我们的 500,000 顶点预算,可以看到敌方 NPC 被摧毁时顶点数量的规律性下降。
Arm Mobile Studio - 每帧的顶点数
现在,几何体的使用和剔除效率更高,可见图元数量在输入图元数量中所占的百分比要合理得多。如预期中一样,正/背面测试剔除了约 50% 的图元,样本测试剔除的图元低于 10%,也就表明我们减少了极小型三角形的数量。
Mali Geometry Usage(几何形内存占用)和 Culling Rate(几何形剔除率)
总结
我们在 Unity Profiler、Frame Debugger 及 Arm Mobile Studio 的帮助下找出了几种提高性能、降低 CPU 和 GPU 负荷的方法,这里得出的最佳实践完全可以避免未来游戏中出现同样的问题。
当然,优化不能牺牲屏幕图像的质量。下方将在优化前与优化后的游戏外观间做一个比对。
预防性性能测试
性能测试通常在开发周期的后期进行。找到进一步优化的契机固然是好,但如果在发布截止日期前没有时间解决问题该怎么办?从一开始就合理设计内容,显然是更加好的办法。
围绕网格复杂性、着色器复杂性和纹理压缩来制定内容预算大有裨益,这能够充分促使你的团队针对移动设备进行高效设计。下面这些内容可能对你的团队有用:
适用于 Unity 开发者的 Arm 指南 ↓
https://developer.arm.com/Gaming%20Graphics%20and%20VR/#aq=%40navigationhierarchiescategories%3D%3D%22Gaming%2C%20Graphics%20and%20VR%22&numberOfResults=48&f[navigationhierarchiestopics]=Arm%20Guides%20for%20Unity%20Developers
Mali 最佳实践开发指南 ↓
https://developer.arm.com/Gaming%20Graphics%20and%20VR/#aq=%40navigationhierarchiescategories%3D%3D%22Gaming%2C%20Graphics%20and%20VR%22&numberOfResults=48&f[navigationhierarchiescontenttype]=Guides
只要让大多数游戏和资产都遵循一套最佳实践,就可以在整个开发周期中定期进行性能测试,及早发现问题、解决问题。
使用持续集成系统的团队可以利用 Arm Mobile Studio 专业版提供的自动化性能测试。此版本可以在设备群中的多个设备上运行,省去手动分析的麻烦。报告的数据甚至可以输入至任何与 JSON 兼容的数据库,方便构建可视化仪表盘以及设置警报来监控性能随时间的变化,从而更快地发现问题。