本日はUnity調査枠です。
Unityではシーン内の物理挙動やプレイヤーの動きをアニメーションとして録画する仕組みとしてUnityRecorderがあります。
UnityRecorderを使用することで簡単に画面録画やアニメーション録画ができますが、筆者の体感ですが若干FPSが低下するような印象を受けました。
今回はGithubで別のアニメーションレコーダーを見つけましたのでこちらも触っていきます。
〇Unity-Runtime-Animation-Recorder
Unity-Runtime-Animation-RecorderはMITライセンスで公開されているUnityのレコーダーシステムです。
特徴としてMayaのアニメーションエクスポートにも対応しているようです。
またBlenderなどにも対応するようにアニメーションをfbxとして書き出すこともできるようです
〇導入・使い方
①リポジトリからプロジェクトを入手します。筆者の場合今回はZipで入手しました。
②Unity Runtime Recorderを自身のパッケージにドラッグ&ドロップします。
③記録したいオブジェクトの親オブジェクトにUnityAnimationRecorderコンポーネントをアタッチします。
④Set Save Pathを選択し録画したアニメーションが保存されるディレクトリを指定します。
以上で準備は完了しました。
⑤実行中にQキーを押すことでレコードが始まり、Wキーを押すことで記録が終了します。
この際にConsolウィンドウにStart Recorder、End Recordのログがそれぞれ出力されます。
Set Save Pathで指定したディレクトリにアニメーションクリップとして出力されます。
〇FPSの調整
Unity-Runtime-Animation-Recorderは非常に便利なパッケージですが、問題点としてFPSを変更することができず、常に実行環境でのFPSで記録が行われ者によっては非常に重たいファイルとなります。
こちらは22年6月9日記事公開現在PRとしてリポジトリに提出されていますが、5年ほど放置されている状態でしたので今回任意のFPSに変更できる仕組みを作ってみました。
今回はUnityAnimationRecorderを次のように改造しました。
_fpsの値をデフォルトで60にしていますが、任意の値に変更することで任意のFPSでレコードが行えるようになっています。
#if UNITY_EDITOR using UnityEngine; using UnityEditor; using System.Collections; using System.Collections.Generic; using Cysharp.Threading.Tasks; using UnityEngine.UI; public class UnityAnimationRecorder : MonoBehaviour { // save file path public string savePath; public string fileName; // use it when save multiple files int fileIndex = 0; public KeyCode startRecordKey = KeyCode.Q; public KeyCode stopRecordKey = KeyCode.W; // options public bool showLogGUI = false; string logMessage = ""; public bool recordLimitedFrames = false; public int recordFrames = 1000; int frameIndex = 0; public bool changeTimeScale = false; public float timeScaleOnStart = 0.0f; public float timeScaleOnRecord = 1.0f; public bool recordBlendShape = false; Transform[] recordObjs; SkinnedMeshRenderer[] blendShapeObjs; UnityObjectAnimation[] objRecorders; List<UnityBlendShapeAnimation> blendShapeRecorders; bool isStart = false; float nowTime = 0.0f; bool isRecording; // Use this for initialization void Start() { SetupRecorders(); } void SetupRecorders() { recordObjs = gameObject.GetComponentsInChildren<Transform>(); objRecorders = new UnityObjectAnimation[recordObjs.Length]; blendShapeRecorders = new List<UnityBlendShapeAnimation>(); frameIndex = 0; nowTime = 0.0f; for (int i = 0; i < recordObjs.Length; i++) { string path = AnimationRecorderHelper.GetTransformPathName(transform, recordObjs[i]); objRecorders[i] = new UnityObjectAnimation(path, recordObjs[i]); // check if theres blendShape if (recordBlendShape) { if (recordObjs[i].GetComponent<SkinnedMeshRenderer>()) { SkinnedMeshRenderer tempSkinMeshRenderer = recordObjs[i].GetComponent<SkinnedMeshRenderer>(); // there is blendShape exist if (tempSkinMeshRenderer.sharedMesh.blendShapeCount > 0) { blendShapeRecorders.Add(new UnityBlendShapeAnimation(path, tempSkinMeshRenderer)); } } } } if (changeTimeScale) Time.timeScale = timeScaleOnStart; } // Update is called once per frame void Update() { if (Input.GetKeyDown(startRecordKey)) { StartRecording(); } if (Input.GetKeyDown(stopRecordKey)) { StopRecording(); } if (isStart) { nowTime += Time.deltaTime; /* for (int i = 0; i < objRecorders.Length; i++) { objRecorders [i].AddFrame (nowTime); } if (recordBlendShape) { for (int i = 0; i < blendShapeRecorders.Count; i++) { blendShapeRecorders [i].AddFrame (nowTime); } } */ } } int _fps=60; async void Record() { var token = this.GetCancellationTokenOnDestroy(); while (isRecording) { await UniTask.Delay(1000/_fps, cancellationToken: token); for (int i = 0; i < objRecorders.Length; i++) { objRecorders[i].AddFrame(nowTime); } /* if (recordBlendShape) { for (int i = 0; i < blendShapeRecorders.Count; i++) { blendShapeRecorders [i].AddFrame (nowTime); } } */ } } public void StartRecording() { CustomDebug("Start Recorder"); isStart = true; isRecording = true; Time.timeScale = timeScaleOnRecord; Record(); } public void StopRecording() { CustomDebug("End Record, generating .anim file"); isStart = false; isRecording = false; ExportAnimationClip(); ResetRecorder(); } void ResetRecorder() { SetupRecorders(); } void FixedUpdate() { if (isStart) { if (recordLimitedFrames) { Debug.Log("FU"); if (frameIndex < recordFrames) { for (int i = 0; i < objRecorders.Length; i++) { objRecorders[i].AddFrame(nowTime); } Debug.Log("F3"); ++frameIndex; } else { Debug.Log("Fa"); isStart = false; ExportAnimationClip(); CustomDebug("Recording Finish, generating .anim file"); } } } } void OnGUI() { if (showLogGUI) GUILayout.Label(logMessage); } void ExportAnimationClip() { string exportFilePath = savePath + fileName; // if record multiple files when run if (fileIndex != 0) exportFilePath += "-" + fileIndex + ".anim"; else exportFilePath += ".anim"; AnimationClip clip = new AnimationClip(); clip.name = fileName; for (int i = 0; i < objRecorders.Length; i++) { UnityCurveContainer[] curves = objRecorders[i].curves; for (int x = 0; x < curves.Length; x++) { clip.SetCurve(objRecorders[i].pathName, typeof(Transform), curves[x].propertyName, curves[x].animCurve); } } if (recordBlendShape) { for (int i = 0; i < blendShapeRecorders.Count; i++) { UnityCurveContainer[] curves = blendShapeRecorders[i].curves; for (int x = 0; x < curves.Length; x++) { clip.SetCurve(blendShapeRecorders[i].pathName, typeof(SkinnedMeshRenderer), curves[x].propertyName, curves[x].animCurve); } } } clip.EnsureQuaternionContinuity(); AssetDatabase.CreateAsset(clip, exportFilePath); CustomDebug(".anim file generated to " + exportFilePath); fileIndex++; } void CustomDebug(string message) { if (showLogGUI) logMessage = message; else Debug.Log(message); } } #endif