Unity动画文件优化探究

Unity动画文件优化探究

这是侑虎科技第246篇原创文章,感谢作者舒航供稿,欢迎转发分享,未经作者授权请勿转载。当然,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者博客:http://www.codershu.com. 同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入 U Sparkle开发者计划,这个舞台有你更精彩!


前不久笔者在UWA群里和一些朋友们讨论动画文件精度优化的问题参考了UWA问答上王亮的代码后,我也尝试优化了动画文件,发现如下的一个很奇怪的现象。下图中Inspector中显示的大小并没有任何变化,但是文件大小和Profiler中的内存大小确实是减小了。那么Inspector中显示的Size是什么含义呢?

下图中的动画文件不包含Scale曲线,所以这次优化中只压缩了浮点数精度。
请输入图片描述

我分别对比了动画文件优化前和优化后的大小。

FileSize

  1. FileInfo.Length取得的文件大小
  2. 可以在操作系统的文件系统中看到

MemorySize

  1. Profiler.GetRuntimeMemorySize取得的内存大小
  2. 可以在Profiler中通过采样看到
  3. 分别在真机和Editor下进行了采样

BlobSize

  1. 反射取得的AnimationClipStats.size二进制大小
  2. 显示在AnimationClip的Inspector的面板上

请输入图片描述
请输入图片描述

红色框内即是BlobSize,在我的理解,FileSize是指文件在硬盘中占的大小,BlobSize是从文件反序列化出来的对象的二进制大小。Editor下的MemorySize不仅有序列化后的内存大小,还维护了一份原文件的内存。就像我们在Editor下加载一张Texture内存是双份一样,而真机下就约等于BlobSize。真机下的MemorySize和Inspector里的BlobSize非常接近,BlobSize可以认为是真机上的内存大小,这个大小更有参考意义

同时,我也对去除Scale曲线的方法进行了实验。下图这个动画文件原来Inspector中Scale的值为4,即有Scale曲线,原始文件BlobSize为10.2KB,去除Scale曲线后,Blob Size变为7.4KB,所以BlobSize减小了27%。
请输入图片描述
请输入图片描述


Curve减少导致内存减小

从上面的实验可以看出来,只裁剪动画文件的压缩精度,没有引起Curve减少。BlobSize是不会有任何变化的,因为每个浮点数固定占32bit。而文件大小、AB大小、Editor下的内存大小,压缩精度后不管有没有引起Curve的变化,都会变小。

裁剪动画文件的精度,意味着点的位置发生了变化,所以Constant Curve和Dense Curve的数量也有可能发生变化。由于是裁剪精度所以动画的点更稀疏了,而连续相同的点更多了。所以Dense Curve是减少了,Constant Curve是增多了,总的内存是减小了。

Constant Curve只需要最左边的点就可以描述一个曲线段。
请输入图片描述


只裁剪精度使BlobSize减小的实例

裁剪精度前,大小为2.2kb,ScaleCurve为0, ConstantCurve为4(57.1%),Stream(使用Optimal模式这部分数据将储存为Dense)为3(42.9%)。

请输入图片描述

裁剪精度后,大小为2.1kb,ConstantCurve为7(100%),Stream为0(0%)。裁剪完精度后导致ConstantCurve增加了3,Stream(Optimal模式下即为Dense)减少了3,BlobSize减小了0.1kb。
请输入图片描述

因此,可以看出,通过精度优化降低内存的方式,其实质是将曲线上过于接近的数值(例如相差数值出现在小数点4位以后)直接变为一致,从而使部分曲线变为constant曲线来降低内存消耗。


总结

隔壁项目组对他们项目中所有的动画文件都进行了优化。其中文件大小从820MB->225MB, ab大小从72MB->64MB, 内存大小从50MB->40MB。总的来说动画文件的scale越多优化越明显。


取BlobSize代码

AnimationClip aniClip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path);
var fileInfo = new System.IO.FileInfo(path);
Debug.Log(fileInfo.Length);//FileSize
Debug.Log(Profiler.GetRuntimeMemorySize (aniClip));//MemorySize

Assembly asm = Assembly.GetAssembly(typeof(Editor));
MethodInfo getAnimationClipStats = typeof(AnimationUtility).GetMethod("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic);
Type aniclipstats = asm.GetType("UnityEditor.AnimationClipStats");
FieldInfo sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance);

var stats = getAnimationClipStats.Invoke(null, new object[]{aniClip});
Debug.Log(EditorUtility.FormatBytes((int)sizeInfo.GetValue(stats)));//BlobSize

工具代码

最后附上工具的代码和简要使用说明,选中想要优化的文件夹或文件,右键Animation->裁剪浮点数去除Scale。
请输入图片描述

//****************************************************************************
//
//  File:      OptimizeAnimationClipTool.cs
//
//  Copyright (c) SuiJiaBin
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
//****************************************************************************
using System;
using System.Collections.Generic;
using UnityEngine;
using System.Reflection;
using UnityEditor;
using System.IO;

namespace EditorTool
{
    class AnimationOpt
    {
        static Dictionary<uint,string> _FLOAT_FORMAT;
        static MethodInfo getAnimationClipStats;
        static FieldInfo sizeInfo;
        static object[] _param = new object[1];

        static AnimationOpt ()
        {
            _FLOAT_FORMAT = new Dictionary<uint, string> ();
            for (uint i = 1; i < 6; i++) {
                _FLOAT_FORMAT.Add (i, "f" + i.ToString ());
            }
            Assembly asm = Assembly.GetAssembly (typeof(Editor));
            getAnimationClipStats = typeof(AnimationUtility).GetMethod ("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic);
            Type aniclipstats = asm.GetType ("UnityEditor.AnimationClipStats");
            sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance);
        }

        AnimationClip _clip;
        string _path;

        public string path { get{ return _path;} }

        public long originFileSize { get; private set; }

        public int originMemorySize { get; private set; }

        public int originInspectorSize { get; private set; }

        public long optFileSize { get; private set; }

        public int optMemorySize { get; private set; }

        public int optInspectorSize { get; private set; }

        public AnimationOpt (string path, AnimationClip clip)
        {
            _path = path;
            _clip = clip;
            _GetOriginSize ();
        }

        void _GetOriginSize ()
        {
            originFileSize = _GetFileZie ();
            originMemorySize = _GetMemSize ();
            originInspectorSize = _GetInspectorSize ();
        }

        void _GetOptSize ()
        {
            optFileSize = _GetFileZie ();
            optMemorySize = _GetMemSize ();
            optInspectorSize = _GetInspectorSize ();
        }

        long _GetFileZie ()
        {
            FileInfo fi = new FileInfo (_path);
            return fi.Length;
        }

        int _GetMemSize ()
        {
            return Profiler.GetRuntimeMemorySize (_clip);
        }

        int _GetInspectorSize ()
        {
            _param [0] = _clip;
            var stats = getAnimationClipStats.Invoke (null, _param);
            return (int)sizeInfo.GetValue (stats);
        }

        void _OptmizeAnimationScaleCurve ()
        {
            if (_clip != null) {
                //去除scale曲线
                foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(_clip)) {
                    string name = theCurveBinding.propertyName.ToLower ();
                    if (name.Contains ("scale")) {
                        AnimationUtility.SetEditorCurve (_clip, theCurveBinding, null);
                        Debug.LogFormat ("关闭{0}的scale curve", _clip.name);
                    }
                } 
            }
        }

        void _OptmizeAnimationFloat_X (uint x)
        {
            if (_clip != null && x > 0) {
                //浮点数精度压缩到f3
                AnimationClipCurveData[] curves = null;
                curves = AnimationUtility.GetAllCurves (_clip);
                Keyframe key;
                Keyframe[] keyFrames;
                string floatFormat;
                if (_FLOAT_FORMAT.TryGetValue (x, out floatFormat)) {
                    if (curves != null && curves.Length > 0) {
                        for (int ii = 0; ii < curves.Length; ++ii) {
                            AnimationClipCurveData curveDate = curves [ii];
                            if (curveDate.curve == null || curveDate.curve.keys == null) {
                                //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don't have curve; Animation name {1} ", curveDate, animationPath));
                                continue;
                            }
                            keyFrames = curveDate.curve.keys;
                            for (int i = 0; i < keyFrames.Length; i++) {
                                key = keyFrames [i];
                                key.value = float.Parse (key.value.ToString (floatFormat));
                                key.inTangent = float.Parse (key.inTangent.ToString (floatFormat));
                                key.outTangent = float.Parse (key.outTangent.ToString (floatFormat));
                                keyFrames [i] = key;
                            }
                            curveDate.curve.keys = keyFrames;
                            _clip.SetCurve (curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);
                        }
                    }
                } else {
                    Debug.LogErrorFormat ("目前不支持{0}位浮点", x);
                }
            }
        }

        public void Optimize (bool scaleOpt, uint floatSize)
        {
            if (scaleOpt) {
                _OptmizeAnimationScaleCurve ();
            }
            _OptmizeAnimationFloat_X (floatSize);
            _GetOptSize ();
        }

        public void Optimize_Scale_Float3 ()
        {
            Optimize (true, 3);
        }

        public void LogOrigin ()
        {
            _logSize (originFileSize, originMemorySize, originInspectorSize);
        }

        public void LogOpt ()
        {
            _logSize (optFileSize, optMemorySize, optInspectorSize);
        }

        public void LogDelta ()
        {

        }

        void _logSize (long fileSize, int memSize, int inspectorSize)
        {
            Debug.LogFormat ("{0} \nSize=[ {1} ]", _path, string.Format ("FSize={0} ; Mem->{1} ; inspector->{2}",
                EditorUtility.FormatBytes (fileSize), EditorUtility.FormatBytes (memSize), EditorUtility.FormatBytes (inspectorSize)));
        }
    }

    public class OptimizeAnimationClipTool
    {
        static List<AnimationOpt> _AnimOptList = new List<AnimationOpt> ();
        static List<string> _Errors = new List<string>();
        static int _Index = 0;

        [MenuItem("Assets/Animation/裁剪浮点数去除Scale")]
        public static void Optimize()
        {
            _AnimOptList = FindAnims ();
            if (_AnimOptList.Count > 0)
            {
                _Index = 0;
                _Errors.Clear ();
                EditorApplication.update = ScanAnimationClip;
            }
        }

        private static void ScanAnimationClip()
        {
            AnimationOpt _AnimOpt = _AnimOptList[_Index];
            bool isCancel = EditorUtility.DisplayCancelableProgressBar("优化AnimationClip", _AnimOpt.path, (float)_Index / (float)_AnimOptList.Count);
            _AnimOpt.Optimize_Scale_Float3();
            _Index++;
            if (isCancel || _Index >= _AnimOptList.Count)
            {
                EditorUtility.ClearProgressBar();
                Debug.Log(string.Format("--优化完成--    错误数量: {0}    总数量: {1}/{2}    错误信息↓:\n{3}\n----------输出完毕----------", _Errors.Count, _Index, _AnimOptList.Count, string.Join(string.Empty, _Errors.ToArray())));
                Resources.UnloadUnusedAssets();
                GC.Collect();
                AssetDatabase.SaveAssets();
                EditorApplication.update = null;
                _AnimOptList.Clear();
                _cachedOpts.Clear ();
                _Index = 0;
            }
        }

        static Dictionary<string,AnimationOpt> _cachedOpts = new Dictionary<string, AnimationOpt> ();

        static AnimationOpt _GetNewAOpt (string path)
        {
            AnimationOpt opt = null;
            if (!_cachedOpts.ContainsKey(path)) {
                AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path);
                if (clip != null) {
                    opt = new AnimationOpt (path, clip);
                    _cachedOpts [path] = opt;
                }
            }
            return opt;
        }

        static List<AnimationOpt> FindAnims()
        {
            string[] guids = null;
            List<string> path = new List<string>();
            List<AnimationOpt> assets = new List<AnimationOpt> ();
            UnityEngine.Object[] objs = Selection.GetFiltered(typeof(object), SelectionMode.Assets);
            if (objs.Length > 0)
            {
                for(int i = 0; i < objs.Length; i++)
                {
                    if (objs [i].GetType () == typeof(AnimationClip))
                    {
                        string p = AssetDatabase.GetAssetPath (objs [i]);
                        AnimationOpt animopt = _GetNewAOpt (p);
                        if (animopt != null)
                            assets.Add (animopt);
                    }
                    else
                        path.Add(AssetDatabase.GetAssetPath (objs [i]));
                }
                if(path.Count > 0)
                    guids = AssetDatabase.FindAssets (string.Format ("t:{0}", typeof(AnimationClip).ToString().Replace("UnityEngine.", "")), path.ToArray());
                else
                    guids = new string[]{};
            }
            for(int i = 0; i < guids.Length; i++)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath (guids [i]);
                AnimationOpt animopt = _GetNewAOpt (assetPath);
                if (animopt != null)
                    assets.Add (animopt);
            }
            return assets;
        }
    }
}

感谢舒航的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)。
也欢迎大家来积极参与U Sparkle开发者计划,简称"US",代表你和我,代表UWA和开发者在一起!

  • Techniques for Game Development 发表在 1月29日 回复

    [...]There are two articles which is related to animation sampling in UWA Blog, GPU Skinning and Animation File Reduction Accuracy, the code related to the sampling part in these two articles can be reffer[...]