一、简介
在最近的优化工作中,马三发现项目中的动画文件内存占比实在是太大了,峰值竟然有200多mb,很明显需要进行优化。经过一番网上查阅资料并结合自己实际操作以后,得到一些需心得体会,在这里马三记录一下并且分享给大家,希望对大家能有一些帮助。
二、动画压缩的注意事项
1.fbx中的动画无法压缩精度,即降低动画文件的浮点数精度
fbx中的动画无法压缩精度,压缩完重启Unity会发现又恢复为原来的样子,并且在版本控制中看不出差别。原因是fbx在Unity中被识别为只读文件,精简动画这个修改的结果实际上是保存在Library/metadata。也就是说这个修改是本地化的操作,无法放入版本管理。导入fbx中的animation是read-only的(考虑到re-import,可编辑的意义其实不大),要编辑需要将动画文件复制出来。可以选中fbx中的动画文件,ctrl D复制一份出来。复制出的文件是可以编辑的,运行脚本也无问题。然后项目中去使用这个复制的动画文件。
2.Ctrl D复制出来Anim以后会发现,复制出来的这个anim的文件体积会比原来的fbx动画体积还要大
Ctrl D复制出来Anim以后会发现,复制出来的这个anim的文件体积会比原来的fbx动画体积还要大这个也是正常的。项目中fbx一般是二进制方式存储的,复制出来的anim如果是用text存储的话,体积会比原来大很多。这个没有太大的影响,最后还要看打出来的ab的大小,实测证明anim打出来的ab要比fbx的体积小很多。
下面列举了两幅图,对比说明了anim动画和fbx动画打出的bundle文件大小对比和运行时内存占用的对比情况:
anim动画assetbundle文件大小:
anim动画运行时占用内存:
fbx动画assetbundle文件大小:
fbx动画占用运行时内存:
可以看到无论是AssetBundle的体积还是运行时内存占用,使用抽离出来的anim动画都比使用fbx中的动画要节省。
3.去除动画文件的scale信息
对于一般的人形动画需求,不会有模型骨骼scale变化的情况。因此我们可以把动画信息的scale部分去除,可以节约一部分大小。
4.为什么压缩动画的float精度、剔除Scale曲线,可以达到减少运行时内存占用
Mecanim的动画系统的压缩确实不是靠改变float类型来达到的,而是通过降低数值位数后,将曲线上过于接近的数值(例如相差数值出现在小数点4位以后)直接变为一致,可以产生更多的const曲线,从而让引擎达到更高效存储的效果,进而达到所谓的“压缩”结果。缩短float类型的精度,导致动画文件内点的位置发生了变化,引起Constant Curve和Dense Curve的数量也有可能发生变化,最终可能导致动画的点更稀疏,而连续相同的点更多了。所以Dense Curve是减少了,Constant Curve是增多了,总的内存是减小了。
5.尽量使用从fbx中复制出来的anim动画,而不是直接引用fbx中的动画文件
很多项目在开发初期阶段,为了快速迭代,并没有使用后处理工具将导入的带有动画的fbx文件进行动画抽离,而是直接是用fbx中的动画文件。实际上这种做法也会造成内存占用较多。因为fbx文件有可能依赖了一些贴图、材质,而且如果项目处理的不够好的话,还会导致交叉引用的出现。比如有一个主角的fbx动画文件,由于美术同学的一些操作,将它引用了怪物的一些材质,然后这个材质又会引用一些纹理。我明明只想加载简简单单的一个主角待机动画,结果就像从泥土里面拎花生一样,带出了一连串的其实不必要加载的文件,白白占用了大块的内存空间,很有可能就因为这一些内存空间被占用就导致了游戏的闪退和崩溃,这个问题是在我们项目中真实遇见过的情况,很值得注意一下。
6.动画文件压缩方式(Anim.Compression)
一般项目都会对这个进行设置,所以就放在最后讲了。对于包含有anim动画的fbx文件,Unity提供了下面的这个设置面板。在Animation选项卡中,我们可以通过设置Anim.Compression来调整动画的文件的压缩方式:
- Off 关闭压缩
- Keyframe Reduction 减少没有必要的关键帧
- Optimal 优化压缩,官方会选择最优的压缩方式来进行压缩,建议选择这个,我们项目也是选择的这个。
7.动画精度压缩与曲线剔除代码
代码语言:javascript复制 1 //----------------------------------------------
2 // ColaFramework
3 // Copyright © 2018-2049 ColaFramework 马三小伙儿
4 //----------------------------------------------
5
6 using System;
7 using System.IO;
8 using System.Reflection;
9 using Sirenix.OdinInspector;
10 using Sirenix.OdinInspector.Editor;
11 using UnityEditor;
12 using UnityEngine;
13 using UnityEngine.Profiling;
14
15 /// <summary>
16 /// 动画优化,存储占用/内存占用/加载时间
17 /// 通过降低float精度,去除无用的scale曲线
18 /// 从而降低动画的存储占用、内存占用和加载时间.
19 /// 使用方法
20 /// 通过菜单ColaFramework/OptimiseToolKits/优化动画打开窗口,
21 /// 在Assets目录下选择要优化的动画,点击Optimize按钮,等待一段时间即可
22 /// </summary>
23 public class AnimtionClipOptimizeToolKit : OdinEditorWindow
24 {
25 [ShowInInspector]
26 [InfoBox("剔除Scale曲线")]
27 private bool m_excludeScale;
28
29 private static AnimtionClipOptimizeToolKit _window;
30
31 [MenuItem("ColaFramework/Optimise/AnimtionClipOptimize")]
32 [MenuItem("Assets/Optimise/AnimtionClipOptimize")]
33 protected static void Open()
34 {
35 _window = GetWindow<AnimtionClipOptimizeToolKit>("动画优化压缩工具");
36 _window.Init();
37 _window.Show();
38 }
39
40 private Vector2 m_scoll;
41 private bool m_ing;
42 private int m_index;
43
44 private string animclipPath;
45 private AnimationClip animClip;
46 private static MethodInfo getAnimationClipStats;
47 private static FieldInfo sizeInfo;
48
49 private void Init()
50 {
51 Assembly asm = Assembly.GetAssembly(typeof(Editor));
52 getAnimationClipStats =
53 typeof(AnimationUtility).GetMethod("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic);
54 Type aniclipstats = asm.GetType("UnityEditor.AnimationClipStats");
55 sizeInfo = aniclipstats.GetField("size", BindingFlags.Public | BindingFlags.Instance);
56 }
57
58 protected override void OnGUI()
59 {
60 var selects = Selection.objects;
61
62 using (var svs = new EditorGUILayout.ScrollViewScope(m_scoll))
63 {
64 m_scoll = svs.scrollPosition;
65 foreach (var obj in selects)
66 {
67 var clip = obj as AnimationClip;
68 if (clip == null)
69 continue;
70 EditorGUILayout.ObjectField(clip, typeof(AnimationClip), false);
71 }
72 }
73
74
75 using (new EditorGUILayout.HorizontalScope())
76 {
77 m_excludeScale = EditorGUILayout.ToggleLeft("Exclude Scale", m_excludeScale);
78
79 if (GUILayout.Button("Optimize"))
80 {
81 m_ing = true;
82 }
83 }
84
85 if (m_ing)
86 {
87 if (m_index >= selects.Length)
88 {
89 m_ing = false;
90 m_index = 0;
91 EditorUtility.ClearProgressBar();
92 return;
93 }
94
95 var info = string.Format("Process {0}/{1}", m_index, selects.Length);
96 EditorUtility.DisplayProgressBar("Optimize Clip", info, (m_index 1f) / selects.Length);
97
98 var obj = selects[m_index];
99 m_index ;
100 var clip = obj as AnimationClip;
101 if (clip == null)
102 return;
103 animClip = clip;
104 animclipPath = AssetDatabase.GetAssetPath(clip);
105 Log("优化前---->");
106 FixFloatAtClip(clip, m_excludeScale);
107 Log("优化后---->");
108 }
109 }
110
111 private static void FixFloatAtClip(AnimationClip clip, bool excludeScale)
112 {
113 try
114 {
115 if (excludeScale)
116 {
117 foreach (var theCurveBinding in AnimationUtility.GetCurveBindings(clip))
118 {
119 var name = theCurveBinding.propertyName.ToLower();
120 if (name.Contains("scale"))
121 {
122 AnimationUtility.SetEditorCurve(clip, theCurveBinding, null);
123 }
124 }
125 }
126
127 var curves = AnimationUtility.GetCurveBindings(clip);
128 foreach (var curveDate in curves)
129 {
130 var curve = AnimationUtility.GetEditorCurve(clip, curveDate);
131 if (curve == null || curve.keys == null)
132 {
133 continue;
134 }
135
136 var keyFrames = curve.keys;
137 for (var i = 0; i < keyFrames.Length; i )
138 {
139 var key = keyFrames[i];
140 key.value = float.Parse(key.value.ToString("f3"));
141 key.inTangent = float.Parse(key.inTangent.ToString("f3"));
142 key.outTangent = float.Parse(key.outTangent.ToString("f3"));
143 keyFrames[i] = key;
144 }
145
146 curve.keys = keyFrames;
147 clip.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curve);
148 }
149 }
150 catch (System.Exception e)
151 {
152 Debug.LogError(string.Format("CompressAnimationClip Failed !!! animationPath : {0} error: {1}", clip.name,
153 e));
154 }
155 }
156
157 #region LogInfo
158
159 private void Log(string title)
160 {
161 Debug.LogFormat("{0} FileSize:{1},MemorySize:{2},InspectorSize:{3}", title, GetFileSize(), GetMemorySize(),
162 GetInspectorSize());
163 }
164
165 private long GetFileSize()
166 {
167 var fileInfo = new FileInfo(animclipPath);
168 return fileInfo.Length;
169 }
170
171 private long GetMemorySize()
172 {
173 return Profiler.GetRuntimeMemorySizeLong(animClip);
174 }
175
176
177 private int GetInspectorSize()
178 {
179 var stats = getAnimationClipStats.Invoke(null, new object[] {animClip});
180 return (int) sizeInfo.GetValue(stats);
181 }
182
183 #endregion
184 }
此工具也已经集成进了ColaFramework:https://github.com/XINCGer/ColaFrameWork/blob/master/Assets/Editor/OptimizeToolkits/AnimtionClipOptimizeToolKit.cs
三、总结
在本篇博客中,马三跟大家一起分享了一下在优化项目动画文件内存占用中的一些注意事项,希望可以对大家起到一些帮助。同时这里也有一些非常不错的关于动画内存优化的博客和uwa的问答,马三在这里贴给大家,可以自己阅读一下,加深理解。
- Anim动画压缩优化探究
- Unity动画文件Animation的压缩和优化总结
最后的最后,还不得不提一下 ACL 这个非常牛逼的C 编写的动画压缩库,至于它的原理和如何使用,马三在这里先买个关子,我会在后面的博客中进行讲解,敬请期待!