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

Blog:https://blog.jp.uwa4d.com/2019/11/05/unity動画ファイル最適化について/

 

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

 

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

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

 

FileSize

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

 

MemorySize

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

 

BlobSize

  1. 反射で取得したAnimationClip.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の減少になりません。浮動小数点数はすべて32bitを固定で占められているから、BlobSizeは何の変化もありません。しかしファイルサイズ、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;
		}
	}
}