【Unity游戲開發】初探Unity動畫優化


一、簡介

  在最近的優化工作中,馬三發現項目中的動畫文件內存占比實在是太大了,峰值竟然有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
請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM