一、簡介
在最近的優化工作中,馬三發現項目中的動畫文件內存佔比實在是太大了,峯值竟然有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 }
此工具也已經集成進了ColaFramework:https://github.com/XINCGer/ColaFrameWork/blob/master/Assets/Editor/OptimizeToolkits/AnimtionClipOptimizeToolKit.cs
三、總結
在本篇博客中,馬三跟大家一起分享了一下在優化項目動畫文件內存佔用中的一些注意事項,希望可以對大家起到一些幫助。同時這裏也有一些非常不錯的關於動畫內存優化的博客和uwa的問答,馬三在這裏貼給大家,可以自己閱讀一下,加深理解。
最後的最後,還不得不提一下 ACL 這個非常牛逼的C++編寫的動畫壓縮庫,至於它的原理和如何使用,馬三在這裏先買個關子,我會在後面的博客中進行講解,敬請期待!
如果覺得本篇博客對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支持微信和支付寶喲!
作者:馬三小夥兒
出處:https://www.cnblogs.com/msxh/p/14090805.html
請尊重別人的勞動成果,讓分享成爲一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!