夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

Unityで実行時のFPSを落とさずにシーン内のオブジェクトの動きをアニメーションとして記録する

本日はUnity調査枠です。

Unityではシーン内の物理挙動やプレイヤーの動きをアニメーションとして録画する仕組みとしてUnityRecorderがあります。

redhologerbera.hatenablog.com

UnityRecorderを使用することで簡単に画面録画やアニメーション録画ができますが、筆者の体感ですが若干FPSが低下するような印象を受けました。

今回はGithubで別のアニメーションレコーダーを見つけましたのでこちらも触っていきます。

〇Unity-Runtime-Animation-Recorder

Unity-Runtime-Animation-RecorderはMITライセンスで公開されているUnityのレコーダーシステムです。

github.com

特徴としてMayaのアニメーションエクスポートにも対応しているようです。

またBlenderなどにも対応するようにアニメーションをfbxとして書き出すこともできるようです

〇導入・使い方

①リポジトリからプロジェクトを入手します。筆者の場合今回はZipで入手しました。

Unity Runtime Recorderを自身のパッケージにドラッグ&ドロップします。

③記録したいオブジェクトの親オブジェクトにUnityAnimationRecorderコンポーネントをアタッチします。

Set Save Pathを選択し録画したアニメーションが保存されるディレクトリを指定します。

以上で準備は完了しました。

⑤実行中にQキーを押すことでレコードが始まり、Wキーを押すことで記録が終了します。

この際にConsolウィンドウにStart RecorderEnd Recordのログがそれぞれ出力されます。

Set Save Pathで指定したディレクトリにアニメーションクリップとして出力されます。

〇FPSの調整

Unity-Runtime-Animation-Recorderは非常に便利なパッケージですが、問題点としてFPSを変更することができず、常に実行環境でのFPSで記録が行われ者によっては非常に重たいファイルとなります。

こちらは22年6月9日記事公開現在PRとしてリポジトリに提出されていますが、5年ほど放置されている状態でしたので今回任意のFPSに変更できる仕組みを作ってみました。

github.com

今回は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