LoginSignup
2
2

More than 3 years have passed since last update.

Unity動画ファイル最適化について

Posted at

動画ファイルの最適化しようとした際に、以下の不思議な現象が発生しました。下図のInspectorに表示されるサイズ情報の変化はありませんが、ファイルサイズとProfile内のメモリサイズは確かに減少しました。ではInspectorに表示されているサイズはどういう意味でしょうか?

下図の動画ファイルにはScale曲線は含まれていません、今回の最適化処理は浮動小数点の精度を圧縮だけになりました。
01.jpg

動画ファイルの最適化前後のサイズを比較してみました。

FileSize
FileInfo.Lengthで取得したファイルサイズ
OSのファイルシステムで確認できるファイルサイズ

MemorySize
Profiler.GetRuntimeMemorySizeで取得したメモリサイズ
Profilerでサンプリングして取得しました
それぞれ実機およびEditorでサンプリングしました

BlobSize
反射で取得したAnimationClip.sizeのバイナリーサイズ
AnimationClipのInspectorのパネル上に表示されるサイズ

02.jpg
03.jpg
赤枠内は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%を減少しました。
04.jpg
05.jpg

Curveの減少がメモリサイズの減少に繋がります

上述の実験で分かるように、動画ファイルの圧縮精度をカットするだけで、Curveの減少になりません。浮動小数点数はすべて32bitを固定で占められているから、BlobSizeは何の変化もありません。しかしファイルサイズ、ABサイズ、Editor内のメモリサイズは、精度を圧縮後、Curveの変化有無にかかわらず、すべて小さくなります。

動画ファイルの精度をカットすれば、サンプルの位置も変わるということで、Constant CurveとDense Curveの数量も変わる可能性があります。精度をカットしたことにより動画のサンプルは薄くなりますが、連続の同じサンプルが増えました。だからDense Curveが減少し、Constant Curveが増え、合計のメモリサイズが減少になりました。

Constant Curveは一番左側のサンプルだけで一つの曲線ブロックを表現できる。
06.jpg

精度をカットのみでBlobSize減少させる実例

精度カット前、サイズは2.2kb、ScaleCurveは0、 ConstantCurveは4(57.1%)、Stream(Optimalモード使用したデータはDenseとして保存される)は3(42.9%)。
07.jpg
精度カット後、サイズは2.1kb、ConstantCurveは7(100%)、Streamは0(0%)。カット後、ConstantCurveを3増加させたが、Stream(Optimalモード下ではDense)は3が減少しました、BlobSizeは0.1kb減少になりました。
08.jpg
ここでわかるように、精度を通じての最適化方法は、その本質は曲線上あまり近い数値(例、相違数値が浮動小数点4桁以降に現れた場合)を直接同じ数値に変えることによって、一部の曲線をconstant曲線に変更し、メモリサイズを減少させることです。

結果

プロジェクトチームからのフィードバックによると、全ての動画ファイルに対して最適化を行いました。それでファイルサイズは820MB->225MB, ABサイズは72MB->64MB,メモリサイズは50MB->40MBになりました。全体的に言えば動画ファイルのscaleが多ければ、最適化を行う効果を得られやすいとのことです。

BlobSizeコード

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を取り除きます。
Blog_Optimization_Animation_4.gif

//**************************************************************************** 
// 
//  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;
        }
    }
}

UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析最適化ソリューション及びコンサルティングサービスを提供している会社でございます。

UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2