unity 动画优化
小木婭
编辑于 2024年02月20日 00:29

1.优化思路

(1).降低帧信息的精度

unityEditor存储的动画文件是text文件,所以缩短浮点数精度,会导致文件表面上有所减少,裁剪动画文件的精度,会让动画的点变的更加稀疏(会将一些曲线上相近的数值,变为一致),Dense Curve是减少了,Constant Curve是增多了,总的内存是减小了。

(2). 去除没有变化的帧动画

去除没有变化的Position,Rotation序列帧,只留头尾两帧。

(3). 去除冗余Scale曲线数据

fbx中很少用到Scale曲线做动画,所以在跟美术的小伙伴确定之后,这部分是可以去除的。PS(如果有需要的话可以在用到Scale变化的骨骼节点加上关键字区分)

2.代码

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEditor;

using System.IO;

using System.Linq;

namespace EngineTeam.ToolTeam.Animation

{

    public class AnimationClipExtract : EditorWindow

    {

        private const float AnimationPositionError = 0.2f;

        private const float AnimationRotationError = 0.1f;

        //ModelImporterAnimationCompression--动画压缩选项

        //Optimal--执行减少关键帧的操作,并在运行时选择最佳动画曲线表示以减少内存占用(默认)。

        private const ModelImporterAnimationCompression Compression = ModelImporterAnimationCompression.Optimal;

        private const int DecimalAccuracy = 10000;

        [MenuItem("Tools/AnimationClip提取&#​34;)]

        static void RightBtn()

        {

            ProcessFbx(true);

        }

        public static void ProcessFbx(bool reProcess = false)

        {

            mAnimationClipList.Clear();

            //获取工具路径

            List<string> assets = ToolUtility.GetAssetPath();

            int index = 0;

            int count = assets.Count;

            foreach (var ass in assets)

            {

                //显示或更新含有 Cancel 按钮的进度条

                bool isCancel = EditorUtility.DisplayCancelableProgressBar("提取AnimationClip&#​34;, string.Format("正在提取:{0}/{1}&#​34;, ++index, count), (float)index / count);

                if (isCancel)

                {

                    EditorUtility.ClearProgressBar();

                    return;

                }

                //Path.GetExtension    返回指定路径字符串的扩展名(包括句点“.”)

                if (Path.GetExtension(ass).ToLower().Contains(".fbx&#​34;))

                {

                    ExtractAnimationClip(ass, reProcess);

                }

            }

            //清理进度条

            EditorUtility.ClearProgressBar();

        }

        public static List<string> mAnimationClipList = new List<string>();

        //提取动画片段

        private static string ExtractAnimationClip(string fbxPath, bool reProcess = false)

        {

            //表示空字符串,此字段为只读

            string folderPath = string.Empty;

            //返回一个值,该值指示指定的字符是否出现在此字符串中。

            if (fbxPath.Contains("@"))

            {

                //AssetDatabase-可用于访问项目中包含的资源

                //LoadAllAssetsAtPath在加载的时候, 从脚本加载和访问资源

                Object[] objs = AssetDatabase.LoadAllAssetsAtPath(fbxPath);

                int index = 0;

                foreach (var obj in objs)

                {

                    //AnimationClip-保存基于关键帧的动画

                    if (obj is AnimationClip)

                    {

                        //预览和第一帧就跳出

                        if (obj.name == "__preview__Take 001&#​34;)

                            continue;

                        if (obj.name.Contains("__preview__&#​34;))

                            continue;

                        if (index++ == 0)

                        {

                            //GetDirectoryName--返回指定路径的目录信息

                            folderPath = Path.GetDirectoryName(fbxPath) + "/Clips&#​34;;

                            //Directory.Exists-确定给定路径是否引用磁盘上的现有目录。

                            if (!Directory.Exists(folderPath))

                            {

                                //没有就创建

                                Directory.CreateDirectory(folderPath);

                            }

                        }

                        AnimationClip clip = (AnimationClip)obj;

                        string _filePath = folderPath + "/&#​34; + clip.name + ".anim&#​34;;

                        if (reProcess)

                        {

                            if (File.Exists(_filePath))

                            {

                                mAnimationClipList.Add(_filePath);

                                continue;

                            }

                        }

                        if (!OptimizeAnimationCurveData(clip, folderPath))

                        {

                            Debug.LogError("动画文件异常,导出动画失败:&#​34; + fbxPath + "  &#​34; + clip.name);

                        }

                        mAnimationClipList.Add(_filePath);

                    }

                }

            }

            return folderPath;

        }

        /// <summary>

        /// 优化动画片段

        /// 1.删除不需要的序列帧

        /// 2.降低帧信息的精度(缩短动画曲线数据产生的浮点数精度)

        /// </summary>

        /// <param name="clip&#​34;></param>

        /// <param name="folderPath&#​34;></param>

        /// <returns></returns>

        public static bool OptimizeAnimationCurveData(AnimationClip clip, string folderPath)

        {

            if (clip == null)

            {

                return false;

            }

            //AnimationUtility.GetAllCurves从特定动画剪辑中检索所有曲线

            var curveDatas = AnimationUtility.GetAllCurves(clip, true);

            if (curveDatas == null || curveDatas.Length == 0)

            {

                return false;

            }

            AnimationClip newClip = new AnimationClip();

            //复制 Unity Object 的所有设置

            EditorUtility.CopySerialized(clip, newClip);

            newClip.name = clip.name;

            //ClearCurves 清除该剪辑中的所有曲线

            newClip.ClearCurves();

            foreach (var dt in curveDatas)

            {

                var nodeName = dt.path.ToLower().Split('/&#​39;).Last();

                //缩短动画曲线数据产生的浮点数精度

                var keys = dt.curve.keys;

                for (var i = 0; i < keys.Length; i++)

                {

                    //如果数字结尾是 .5,从而使它处于两个整数正中间(其中一个是偶数,另一个是奇数),则返回偶数

                    keys[i].time = Mathf.Round(keys[i].time * DecimalAccuracy) / DecimalAccuracy;

                    keys[i].value = Mathf.Round(keys[i].value * DecimalAccuracy) / DecimalAccuracy;

                    //曲线正切值

                    keys[i].outTangent = Mathf.Round(keys[i].outTangent * DecimalAccuracy) / DecimalAccuracy;

                    keys[i].inTangent = Mathf.Round(keys[i].inTangent * DecimalAccuracy) / DecimalAccuracy;

                }

                //过滤位移值没有变化的帧动画

                //因为帧信息有初始位置,所有要保留头尾两帧,如果全部删除会出现初始位置为默认值的问题

                if (IsFilterApproximateKeyFrame(ref keys))

                {

                    //创建关键帧

                    var newKeys = new Keyframe[2];

                    newKeys[0] = keys[0];

                    newKeys[1] = keys[keys.Length - 1];

                    keys = newKeys;

                }

                dt.curve.keys = keys;

                //设置新数据

                newClip.SetCurve(dt.path, dt.type, dt.propertyName, dt.curve);

            }

            //在此路径下创建一个新资源。

            AssetDatabase.CreateAsset(newClip, folderPath + @"/&#​34; + newClip.name + ".anim&#​34;);

            //导入所有更改的资源。

            AssetDatabase.Refresh();

            return true;

        }

        /// <summary>

        /// 动画默认不导出Scale序列帧,除非该节点包含scale关键词(加scale关键词表示该节点需要进行scale变换)

        /// </summary>

        /// <param name="dt&#​34;></param>

        /// <returns></returns>

        private static bool IsFilterCurveData(AnimationClipCurveData dt, string nodeName)

        {

            if (dt.propertyName.ToLower().Contains("scale&#​34;) && !nodeName.Contains("scale&#​34;))

                return true;

            return false;

        }

        /// <summary>

        /// 过滤值一样的序列帧

        /// </summary>

        /// <param name="keys&#​34;></param>

        /// <returns></returns>

        private static bool IsFilterApproximateKeyFrame(ref Keyframe[] keys)

        {

            for (var i = 0; i < keys.Length - 1; i++)

            {

                //Mathf.Abs 返回 f 的绝对值

                if (Mathf.Abs(keys[i].value - keys[i + 1].value) > 0 ||

                    Mathf.Abs(keys[i].outTangent - keys[i + 1].outTangent) > 0

                    || Mathf.Abs(keys[i].inTangent - keys[i + 1].inTangent) > 0)

                {

                    return false;

                }

            }

            return true;

        }

        private static string GetNewClipDir(string clipPath)

        {

            int index = clipPath.LastIndexOf("/&#​34;);

            if (index < 0)

            {

                index = clipPath.LastIndexOf("\\&#​34;);

            }

            string ret = clipPath.Substring(0, index + 1);

            string newFolder = string.Format("{0}\\Clips&#​34;, ret);

            if (!Directory.Exists(newFolder))

            {

                Directory.CreateDirectory(newFolder);

            }

            return newFolder;

        }

    }

}