Unity性能优化之资源优化

Unity的性能优化是一个永恒的话题,性能优化主要是围着CPU、GPU和内存三大方面进行。先从资源优化开始吧,资源优化涉及到资源加载(CPU、内存),资源渲染(GPU、显存)等方面,也是最容易着手优化的点。主要涵盖了Audio资源、Model资源、Texture资源、Animation资源等,现在逐个来看看优化点以及优化方法,以及最后的对 SUNTAIL - Stylized Fantasy Village 工程的优化前后对比。

前期准备

为了能够显著看出优化前后的对比,以 AssetStore 的 SUNTAIL - Stylized Fantasy Village 场景优化过程为例,先添加Universal插件,将普通3D项目转换为URP项目,再导入URP的Unity Packages,通过Unity编辑器 Edit > Project Setiings > Graphics > Scriptable Render Pipeline Settings选择SuntailUniversalRenderPipelineAsset.asset为默认管线设置。

打开 Assets/Suntail Village/Demo/Suntail Village.unity 场景文件,显示正常即可,另外对于Suntail 这个Demo,还需要设置 Tags And Layers属性、Quality属性等设置。

可以先通过资源分类功能大致查看一下各类资源的分布情况:

需要关注场景中的摄像机数量,摄像机的数量会影响渲染流程复杂度、灯光数量直接影响了场景的光照复杂度和阴影复杂度。如果项目中灯光和摄像机太多,则以后的优化重点肯定包括了灯光优化和摄像机优化。

渲染基本流程

了解 Unity 的渲染优化时必须先搞清楚每一帧的渲染 CPU 和 GPU 都做了些什么:

1、CPU 检查场景中每个对象,决定他们是否应该被渲染,只有满足一定条件的对象才会被渲染;

2、CPU 收集即将被渲染的对象信息,并把这些信息分类为渲染指令,也就是 Draw calls,Draw Call 实际上就是一个命令。一个 draw call包含网格数据以及网格如何被渲染。在某些场景,共享设置的一些对象可能会被合并为一个draw call。合并不同对象的数据到同一个draw call 被称作 batching;

3、CPU 给每个 draw call 创建一个数据包,称为 batch 。每一个batch必须包含一个draw call;

4、CPU 会发出指令,使GPU改变一些渲染状态。这个指令被称为 SetPass call。SetPass call 通知 GPU 如何去渲染下一个网格。只有在渲染下一个网格时,其渲染状态相对于渲染上一个网格发生了变化时,才会调用 SetPass call;

5、CPU把draw call发送给GPU。draw call通知GPU使用最近的SetPass call去渲染指定的网格;

6、有时,batch可能需要不止一个的 pass。pass 是 shader 代码的一部分,而新的pass需要改变渲染状态。对于batch中的每个pass,CPU必须发送一个新的SetPass call然后必须要再次发送draw call;

7、GPU按照CPU发送的指令顺序处理这些指令;

8、如果当前任务是SetPass call,那么GPU更新渲染状态;

9、如果当前任务是draw call,那么GPU渲染网格。渲染网格发生在很多阶段,不同阶段的shader代码可以定义渲染。其中:顶点着色器vertex shader告诉GPU怎么处理网格的顶点。片元着色器fragment shader告诉GPU怎么绘制单独的像素;

10、以上过程会重复执行,直到所有CPU发送的任务都被GPU完成

渲染优化的主要目的就是减少渲染的工作量,控制渲染的工作量是保证效率的根本。

Unity Profiler 初体验

关于性能分析器,可以看官方的详细文档: https://docs.unity3d.com/Manual/profiler-profiling-applications.html

通过真机运行,并且勾选Autoconnec Profiler,可以自动运行时打开Unity性能分析窗口,可以看到仅仅只是Audio就占用了76M的内存

通过底部的详细内存分析,可以看到主要是纹理,音效,Shader等占用内存较多;通过底部的详细数据,也能看到主线程和渲染线程执行各个函数的耗时,方便分析到底是哪一部分代码引起的耗时过长:

查看渲染部分的性能分析,能看出顶点数量(Vectices)和面片数量(Triangle)过多,GPU渲染耗时也会变得更长:

Assetchecker 检测工具

以静态资源优化——Audio导入设置检查与优化,关于Unity音乐音效资源的优化为例,使用Unity UPR AssetChecker工具,下载地址: https://upr.unity.cn/download ,操作手册地址: https://upr.unity.cn/instructions/assetchecker ,通过 AssetChecker 本地资源检测,可以帮助我们尽早发现资源文件中存在的问题,具体操作很简单,检查完成后访问检查报告的地址即可:

音频导入设置检查与优化

  • 根据平台选择合理的音频设置,原始音频资源尽量采用未压缩WAV格式
  • 移动平台对音乐音效统一采用单通道设置(Force to Mono),并将音乐采样频率设置为22050Hz
  • 移动平台大多数声音尽量采用Vorbis压缩设置,IOS平台或不打算循环的声音可以选择MP3格式,对于简短、常用的音效,可以采用解码速度快的ADPCM格式(PCM为未压缩格式)
  • 音频片段加载类型说明
    • 简短音效导入后小于200kb,采用Decompress on Load模式
    • 对于复杂音效,大小大于200kb,长度超过5秒的音效采用Compressed In Memory模式
    • 对于长度较长的音效或背景音乐则采用Streaming模式,虽然会有CPU额外开销,但节省内存并且加载不卡顿
  • 当实现静音功能时,不要简单的将音量设置为0,应销毁音频(AudioSource)组件,将音频从内存中卸载

优化结果:内存从76M左右降低到8M,几乎是降低了10倍。APK包体积降低30M,CPU开销由2.5%左右上升到5%左右,这是由于部分音频资源才用了Streaming模式加载,不过相对CPU消耗的微微提升,带来的内存优化效果是非常显著且值得的:

模型导入设置检查与优化

DCC中模型导出的原则

1、格式统一使用FBX

DCC中模型导出时,Unity 支持多种标准和专有模型文件格式(DCC)。Unity 内部使用 .fbx 文件格式作为其导入链。最佳做法尽可能使用 .fbx 文件格式,并且不应在生产中使用专有文件格式。

2、优化原始导入模型文件,删除不需要的数据

  • 统一单位,避免DCC工具中的单位和Unity中的单位出现不一致的情况
  • 导出的网格必须是多边形拓扑网格,不能是贝塞尔曲线、样条曲线、NURBS、NURMS、细分曲面等
  • 烘培Deformers,在导出之前,确保变形体被烘培到网格模型上,如骨骼形变烘培到蒙皮权重上
  • 不建议模型使用到的纹理随模型导出,纹理应该单独给出
  • 如果需要导入blend shape normals,必须要指定光滑组smooth groups,当然也不是全部都需要指定
  • DCC导出面板设置, 不建议携带场景信息导出,如不建议导出摄像机、灯光、材质等信息,因为这些的信息与Unity内默认都不同。除非你自己为某DCC做过自定义导出插件。

Unity模型导入流程

原始模型文件对性能的影响点

  • 最小化面数,不要使用微三角形(三角形内只有个位数像素即微三角形),分布尽量均匀,尽量使用LOD
  • 合理的网络拓扑和平滑组,尽可能是闭包,避免后期Unity烘焙错误,避免产生额外的三角形顶点和边
  • 尽量少的使用材质个数,材质数量的增多会引起Shader和贴图的暴涨
  • 尽可能少的使用蒙皮网格,同一个模型尽量尽量使用同一个蒙皮网格
  • 尽可能少的骨骼数量,过多骨骼数量会引起蒙皮动画的CPU、GPU双方的性能瓶颈
  • 原始模型中的FK与IK节点分离,Unity中不支持导入的IK骨骼,导出时需要删除IK节点

上面这一部分主要是美术需要关心的性能优化点,下面看看其他的优化点

  • 尽可能的将网格合并到一起
  • 尽可能使用共享材质,单个模型尽量使用单个材质
  • 不要使用网格碰撞体
  • 不必要不要开启网格读写,否则会存在内存和显存中两份模型资源
  • 使用合理的LOD级别,可以参考 https://docs.unity3d.com/cn/2020.3/Manual/LevelOfDetail.html
  • Skin Weights受骨骼影响个过多
  • 合理压缩网格,减化网格,避免出现全部是精细模型,比如一个石头就有上万的顶点数量肯定是不合理的
  • 不需要rigs和BlendShapes尽量关闭
  • 如果可能,禁用法线或切线
  • 多套模型

资源检查报告——FBX部分问题解读

其中两项建议与模型动画有关,而测试项目中所有模型资源都不涉及动画,可以将Rig标签下的Animation Type设置为None,并关闭Animation标签下的Import Animations选项,设置Materials标签中的Material Creation Mode为None。

开启Project Settings ——> Player ——> Optimization下的Vertex Compression与Optimize Mesh Data选项。

资源导入工作流的优化

接下来看看Unity资源导入工作流的优化与工程目录的Asset目录优化的优化。资源导入工作流目前有三种方案:1、手动编写工具 2、利用Preset 3、AssetGraph工具;下面分别来看看各种方案的优缺点和使用场景。

手动编写工具

优点:根据项目特点自定义安排导入工作流,并且可以和后续资源制作与大包工作流结合

缺点:存在开发和维护成本,会让编辑器菜单界面变得复杂,对新人理解工程不友好

适合类型:大型商业游戏团队

将如下代码放入Asset->Editor文件夹下即可,在处理资源时会自动执行对应方法:

 1using UnityEngine;
 2using System.Collections;
 3using UnityEditor;
 4public class MyEditor : AssetPostprocessor {
 5 
 6    // 模型导入之前调用
 7    public void OnPreprocessModel()
 8    {
 9        Debug.Log ("OnPreprocessModel="+this.assetPath);
10    }
11 
12    // 模型导入之后调用
13    public void OnPostprocessModel(GameObject go)
14    {
15        Debug.Log ("OnPostprocessModel="+go.name);
16    }
17 
18    // Texture导入之前调用,针对Texture进行设置
19    public void OnPreprocessTexture()
20    {
21        Debug.Log ("OnPreProcessTexture="+this.assetPath);
22        TextureImporter impor = this.assetImporter as TextureImporter;
23        impor.textureType = TextureImporterType.Sprite; // 将Texture设置为Sprite
24    }
25 
26 	// Texture导入之后调用,针对Texture进行设置
27    public void OnPostprocessTexture(Texture2D tex)
28    {
29        Debug.Log ("OnPostProcessTexture=" + this.assetPath);
30    }
31 
32 	// 导入Audio后操作
33    public void OnPostprocessAudio(AudioClip clip)
34    {
35    
36    }
37 	
38 	// 导入Audio前操作
39    public void OnPreprocessAudio()
40    {
41        AudioImporter audio = this.assetImporter as AudioImporter;
42        audio.format = AudioImporterFormat.Compressed;
43    }
44 
45    // 所有的资源的导入,删除,移动,都会调用此方法,注意,这个方法是static的
46    public static void OnPostprocessAllAssets(string[] importedAsset, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
47    {
48        Debug.Log ("OnPostprocessAllAssets");
49        foreach (string str in importedAsset) {
50            Debug.Log("importedAsset = "+str);
51        }
52        foreach (string str in deletedAssets) {
53            Debug.Log("deletedAssets = "+str);
54        }
55        foreach (string str in movedAssets) {
56            Debug.Log("movedAssets = "+str);
57        }
58        foreach (string str in movedFromAssetPaths) {
59            Debug.Log("movedFromAssetPaths = "+str);
60        }
61    }
62}

具体功能用法可以参考官方文档:https://docs.unity3d.com/ScriptReference/AssetPostprocessor.html

这种方式的主要问题就是如何使用一套代码对全部的资源进行统一处理呢?其实很难做到统一处理全部的资源,如果实在是需要分开处理,那么肯定需要对资源路径做统一管理,根据通配符啥的需要有一套规则来完成这个事情。

这个方式只针对导入时生效,如果后续由于操作不不小心发生了更改,那么很多人针对这个这个问题是把导入工作流和打包发布合并在一起处理了,就是在打包发布前重新对资源进行检查,但是这样做的后果就是某些性能问题只有在真包上才有办法发现。为了处理这个问题,可以使用AssetsModifiedProcessor接口,资源被添加、删除、修改、移动时回调该对象的OnAssetsModified接口,可以参考: AssetsModifiedProcessor.cs

利用Presets功能(重点)

优点:使用简单方便,只需要Assets目录结构合理规范即可

缺点:无法和后续工作流整合,只适合做资源导入设置。

适合类型:小型团队或中小规模项目

此功能自从Unity2018的版本推出,但是国内开发者很多都没有使用过Presets。Presets是将相同属性设置跨多个组件、资源或项目设置保存和应用的资源,该资源运行时没有效果,仅能在Unity编辑器下使用。

通过Presets预设可以直接将之前的参数保存起来:

然后保存到系统预设,这样下次使用创建其他场景的时候也可以直接使用此预设:

Presets Manager的过滤器支持的通配符参数如下:

但是使用Presets功能依旧是无法解决导入后的人为修改,或者是误操作引发的修改。此时可以使用DefaultPresetsByFolder来解决这个问题,即默认预设,可以从根本上保证资源不会被人为修改,使用文档如下(参考其中的代码即可): https://docs.unity3d.com/cn/2021.1/Manual/DefaultPresetsByFolder.html

利用AssetGraph工具

优点:功能全,覆盖Unity资源工作流全流程,节点化编辑,直观

缺点:有一定上手成本,一些自定义生成节点也需要开发,不是Unity标准包,Unity新功能支持较慢。

适合类型:任何规模项目和中大型团队

AssetGraph项目是 Unity 日本的工作室开发的,项目地址: https://github.com/Unity-Technologies/AssetGraph ,下载后通过本地安装的方式进行安装,安装后在Unity的Windows -> AssetGraph中打开,具体的使用教程参考官方文档

工程目录与Asset目录优化

Unity工程目录结构及用途

Asset文件夹:用来存储和重用的项目资产

Library文件夹:用来存储项目内部资产数据信息的目录

Packages文件夹:用来存储项目的包文件信息

Project Settings文件夹:用来存储项目设置的信息

UserSettings文件夹:用来存储用户设置信息

Temp文件夹:用来存储使用Unity编辑器打开项目时的临时数据,一旦关闭Unity编辑器也会被删除

Logs文件夹:用来存储项目的日志信息(不包含编辑器日志信息)

Assets目录中特殊文件夹及用途

Editor文件夹(可以多个)

Editor Default Resources文件夹(根目录唯一)

Gizmos文件夹(根目录唯一)

Plugins文件夹(2019后已无,但仍可使用,仍能保障其中代码编译的优先顺序)

Resources文件夹(可以多个,强烈建议正式项目中一定不要有此文件夹)

Standard Assets文件夹(根目录唯一,其中代码编译优先)

StreamingAssets文件夹(根目录唯一)

忽略导入的文件夹

  • 隐藏的文件夹

  • .开头的文件和文件夹

  • ~结尾的文件和文件夹

  • 扩展名为cvs的文件和文件夹

  • 扩展名为.tmp的文件夹

Assets目录结构设计

一级目录设计原则

  • 目录尽可能少

  • 区分编辑模式与运行模式

  • 区分工程大版本

  • 访问场景文件、全局配置文件便捷

  • 不在一级目录做资源类别区分,只有Video类视频建议直接放到StreamAssets下

二级目录设计原则

  • 只区分资源类型

  • 资源类型大类划分要齐全

  • 不做子类型区分

  • 不做功能区分

  • 不做生命周期区分

三级目录设计原则

  • Audio/Texture/Models三级目录做子类型区分

  • 其他类型资源可按功能模块/生命周期区分

四级目录设计原则

  • 只有Audio/Texture/Models做四级目录,可按工程按模块/生命周期划分

参考资料

主要内容来自Bilibili的Metaverse大衍神君的 2022新年计划《Unity性能优化》 ,其中的大部分笔记摘抄自其PPT作为以后参考。