見出し画像

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

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

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

画像1

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

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

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

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

画像2

画像3

赤枠内は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%を減少しました。

画像4

画像5

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

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

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

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

画像6

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

精度カット前、サイズは2.2kb、ScaleCurveは0、 ConstantCurveは4(57.1%)、Stream(Optimalモード使用したデータはDenseとして保存される)は3(42.9%)。

画像7

精度カット後、サイズは2.1kb、ConstantCurveは7(100%)、Streamは0(0%)。カット後、ConstantCurveを3増加させたが、Stream(Optimalモード下ではDense)は3が減少しました、BlobSizeは0.1kb減少になりました。

画像8

ここでわかるように、精度を通じての最適化方法は、その本質は曲線上あまり近い数値(例、相違数値が浮動小数点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を取り除きます。

画像9

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

この記事が気に入ったらサポートをしてみませんか?