本日は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