【Unity游戏开发】初探Unity动画优化

时间:2024-03-06 06:59:36

一、简介

  在最近的优化工作中,马三发现项目中的动画文件内存占比实在是太大了,峰值竟然有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.动画精度压缩与曲线剔除代码

  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 }
View Code

此工具也已经集成进了ColaFramework:https://github.com/XINCGer/ColaFrameWork/blob/master/Assets/Editor/OptimizeToolkits/AnimtionClipOptimizeToolKit.cs

8.剔除冗余的关键帧信息

后来发现一些3D Max或者Maya等工具导出的fbx动画中,包含了大量前一帧与后一帧值相同或者精度小于某一阈值范围的关键帧。其实剔除掉这些关键帧信息并不会影响到最终的美术效果,但是可以大幅减少动画内存的占用。因此很有必要针对这些冗余的关键帧信息进行剔除和优化。需要注意的是,对于Rotation信息,我们一般是需要保留不剔除的,因为Rotation被剔除以后可能造成动画抖动。我们只需要剔除Position中的冗余信息就可以了,scale曲线之前在上文中整条都被删除了。另外,在Position的曲线中,尽量保持对应一帧的x,y,z信息都在,如果缺少了其中的某一个或者某两个,又会造成内存的过多占用。

三、总结

  在本篇博客中,马三跟大家一起分享了一下在优化项目动画文件内存占用中的一些注意事项,希望可以对大家起到一些帮助。同时这里也有一些非常不错的关于动画内存优化的博客和uwa的问答,马三在这里贴给大家,可以自己阅读一下,加深理解。

  1. Anim动画压缩优化探究

  2. Unity动画文件Animation的压缩和优化总结

  最后的最后,还不得不提一下 ACL 这个非常牛逼的C++编写的动画压缩库,至于它的原理和如何使用,马三在这里先买个关子,我会在后面的博客中进行讲解,敬请期待!

 

 

 

 

如果觉得本篇博客对您有帮助,可以扫码小小地鼓励下马三,马三会写出更多的好文章,支持微信和支付宝哟!

       

 

作者:马三小伙儿
出处:https://www.cnblogs.com/msxh/p/14090805.html
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!