天天看點

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

一、簡介

  在最近的優化工作中,馬三發現項目中的動畫檔案記憶體占比實在是太大了,峰值竟然有200多mb,很明顯需要進行優化。經過一番網上查閱資料并結合自己實際操作以後,得到一些需心得體會,在這裡馬三記錄一下并且分享給大家,希望對大家能有一些幫助。

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

二、動畫壓縮的注意事項

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檔案大小:

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

  anim動畫運作時占用記憶體:

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

  fbx動畫assetbundle檔案大小:

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

  fbx動畫占用運作時記憶體:

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

  可以看到無論是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 優化壓縮,官方會選擇最優的壓縮方式來進行壓縮,建議選擇這個,我們項目也是選擇的這個。
【Unity遊戲開發】初探Unity動畫優化

7.動畫精度壓縮與曲線剔除代碼

【Unity遊戲開發】初探Unity動畫優化
【Unity遊戲開發】初探Unity動畫優化
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++編寫的動畫壓縮庫,至于它的原理和如何使用,馬三在這裡先買個關子,我會在後面的部落格中進行講解,敬請期待!

如果覺得本篇部落格對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支援微信和支付寶喲!

 

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

作者:馬三小夥兒

出處:https://www.cnblogs.com/msxh/p/14090805.html

請尊重别人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!

繼續閱讀