📷

VRChat でアバターの動きを取得し、再生をする

に公開

この記事は team411 Advent Calendar 2025 の9日目の記事です

昨日の記事は mimifuwacc さんによる dotfilesをやってみよう でした。私は今まで開発用PCが1台しかなかったのですが、これから初期化や移行する機会が多くなると思うので、是非導入したいと思いました

明日の記事は 黄泉比良坂46(むつみん) さんによる null です。楽しみですね

はじめに

こんにちは、team411 に所属するとらです。本日は、team411で IVRC メタバース部門 に参加した作品「3d camera」のうち、私が担当した部分の紹介をします

私が担当した部分は、記事タイトルにある通り VRChat 内で自分の動きを記録し、その動きを他のアバターがトレースするギミックです。本記事ではそのギミックの原案である 3Dカメラ(ワールド名ではない)について説明します

3Dカメラとは、普通のカメラが2次元に切り抜いて保存するように、空間を3次元に切り抜いて保存し後から見ることができるものと(ここでは)定義します。カメラなのに動画です。本ギミックでは、切り抜く対象を1体のアバターのみに制限をして作成を行いました。具体的な要件は以下のとおりです

  • 録画ボタンを押した人の動きを記録する
  • 録画ボタンを押すと録画が開始する
  • 再生ボタンを押すと予め設定したアバターが録画した動きを再生する
  • 撮影中のカメラの向きとアバターの位置が保存される
  • 動きを再生する時、カメラの向きを変えると再生アバターも同じ方向に移動する

要はカメラを右回転させたら視界では左側に移動する・再生場所はカメラを動かせばすきな位置に移動できるということです

設計

毎フレームごとに、カメラからみたアバターの位置・アバターの回転・それぞれのボーンの回転を記録し、クラスが持つ配列に保存します
取得するボーン情報は Unity の HumanBodyBones から取れるボーンだけを記録します

記録を行う配列は、recordFramesという変数を使い、リングバッファとして使います。これにより配列長以上のフレーム数の録画を行っても、最後 n秒の録画を使うことが出来ます。録画・再生の状態管理はisAcvitveisPlayを使っています

以上の方針で、まずはカメラの録画・再生以外の部分を作成します。以下のメソッドを public なメソッドとして作成します

  • Activate(): 録画の開始
  • Deactivate(): 録画の停止
  • Play(): 録画の再生
  • SetTargetPlayer(VRCPlayerApi targetPlayer): 録画する対象を指定
録画・再生以外の部分のコード
public class ModelController : UdonSharpBehaviour
{
    // 動きを反映するもとのプレイヤー
    private VRCPlayerApi sourcePlayer;

    // ワールドに設置したアバターのボーンをインスペクターから設定
    public GameObject targetAvatar;

    public int recordFrames = 3000;
    // 今回は使わない
    public bool recordMode = true;

    // すべてのHumanBodyBonesの変数定義
    HumanBodyBones hipsBone = HumanBodyBones.Hips;                      // 尻のボーン
    HumanBodyBones leftUpperLegBone = HumanBodyBones.LeftUpperLeg;      // 左太もものボーン
    HumanBodyBones rightUpperLegBone = HumanBodyBones.RightUpperLeg;    // 右太もものボーン
    HumanBodyBones leftLowerLegBone = HumanBodyBones.LeftLowerLeg;      // 左ひざのボーン
    HumanBodyBones rightLowerLegBone = HumanBodyBones.RightLowerLeg;    // 右ひざのボーン
    HumanBodyBones leftFootBone = HumanBodyBones.LeftFoot;              // 左足首のボーン
    HumanBodyBones rightFootBone = HumanBodyBones.RightFoot;            // 右足首のボーン
    HumanBodyBones spineBone = HumanBodyBones.Spine;                    // 背骨の第一ボーン
    HumanBodyBones chestBone = HumanBodyBones.Chest;                    // 胸のボーン
    HumanBodyBones upperChestBone = HumanBodyBones.UpperChest;          // 上胸のボーン
    HumanBodyBones neckBone = HumanBodyBones.Neck;                      // 首のボーン
    HumanBodyBones headBone = HumanBodyBones.Head;                      // 頭のボーン
    HumanBodyBones leftShoulderBone = HumanBodyBones.LeftShoulder;      // 左肩のボーン
    HumanBodyBones rightShoulderBone = HumanBodyBones.RightShoulder;    // 右肩のボーン
    HumanBodyBones leftUpperArmBone = HumanBodyBones.LeftUpperArm;      // 左上腕のボーン
    HumanBodyBones rightUpperArmBone = HumanBodyBones.RightUpperArm;    // 右上腕のボーン
    HumanBodyBones leftLowerArmBone = HumanBodyBones.LeftLowerArm;      // 左ひじのボーン
    HumanBodyBones rightLowerArmBone = HumanBodyBones.RightLowerArm;    // 右ひじのボーン
    HumanBodyBones leftHandBone = HumanBodyBones.LeftHand;              // 左手首のボーン
    HumanBodyBones rightHandBone = HumanBodyBones.RightHand;            // 右手首のボーン
    HumanBodyBones leftToesBone = HumanBodyBones.LeftToes;              // 左つま先のボーン
    HumanBodyBones rightToesBone = HumanBodyBones.RightToes;            // 右つま先のボーン
    HumanBodyBones leftEyeBone = HumanBodyBones.LeftEye;                // 左目のボーン
    HumanBodyBones rightEyeBone = HumanBodyBones.RightEye;              // 右目のボーン
    HumanBodyBones jawBone = HumanBodyBones.Jaw;                        // 顎のボーン

    // 左手の指のボーン
    HumanBodyBones leftThumbProximalBone = HumanBodyBones.LeftThumbProximal;            // 左親指第一指骨のボーン
    HumanBodyBones leftThumbIntermediateBone = HumanBodyBones.LeftThumbIntermediate;    // 左親指第二指骨のボーン
    HumanBodyBones leftThumbDistalBone = HumanBodyBones.LeftThumbDistal;                // 左親指第三指骨のボーン
    HumanBodyBones leftIndexProximalBone = HumanBodyBones.LeftIndexProximal;            // 左人差し指第一指骨のボーン
    HumanBodyBones leftIndexIntermediateBone = HumanBodyBones.LeftIndexIntermediate;    // 左人差し指第二指骨のボーン
    HumanBodyBones leftIndexDistalBone = HumanBodyBones.LeftIndexDistal;                // 左人差し指第三指骨のボーン
    HumanBodyBones leftMiddleProximalBone = HumanBodyBones.LeftMiddleProximal;          // 左中指第一指骨のボーン
    HumanBodyBones leftMiddleIntermediateBone = HumanBodyBones.LeftMiddleIntermediate;  // 左中指第二指骨のボーン
    HumanBodyBones leftMiddleDistalBone = HumanBodyBones.LeftMiddleDistal;              // 左中指第三指骨のボーン
    HumanBodyBones leftRingProximalBone = HumanBodyBones.LeftRingProximal;              // 左薬指第一指骨のボーン
    HumanBodyBones leftRingIntermediateBone = HumanBodyBones.LeftRingIntermediate;      // 左薬指第二指骨のボーン
    HumanBodyBones leftRingDistalBone = HumanBodyBones.LeftRingDistal;                  // 左薬指第三指骨のボーン
    HumanBodyBones leftLittleProximalBone = HumanBodyBones.LeftLittleProximal;          // 左小指第一指骨のボーン
    HumanBodyBones leftLittleIntermediateBone = HumanBodyBones.LeftLittleIntermediate;  // 左小指第二指骨のボーン
    HumanBodyBones leftLittleDistalBone = HumanBodyBones.LeftLittleDistal;              // 左小指第三指骨のボーン

    // 右手の指のボーン
    HumanBodyBones rightThumbProximalBone = HumanBodyBones.RightThumbProximal;          // 右親指第一指骨のボーン
    HumanBodyBones rightThumbIntermediateBone = HumanBodyBones.RightThumbIntermediate;  // 右親指第二指骨のボーン
    HumanBodyBones rightThumbDistalBone = HumanBodyBones.RightThumbDistal;              // 右親指第三指骨のボーン
    HumanBodyBones rightIndexProximalBone = HumanBodyBones.RightIndexProximal;          // 右人差し指第一指骨のボーン
    HumanBodyBones rightIndexIntermediateBone = HumanBodyBones.RightIndexIntermediate;  // 右人差し指第二指骨のボーン
    HumanBodyBones rightIndexDistalBone = HumanBodyBones.RightIndexDistal;              // 右人差し指第三指骨のボーン
    HumanBodyBones rightMiddleProximalBone = HumanBodyBones.RightMiddleProximal;        // 右中指第一指骨のボーン
    HumanBodyBones rightMiddleIntermediateBone = HumanBodyBones.RightMiddleIntermediate; // 右中指第二指骨のボーン
    HumanBodyBones rightMiddleDistalBone = HumanBodyBones.RightMiddleDistal;            // 右中指第三指骨のボーン
    HumanBodyBones rightRingProximalBone = HumanBodyBones.RightRingProximal;            // 右薬指第一指骨のボーン
    HumanBodyBones rightRingIntermediateBone = HumanBodyBones.RightRingIntermediate;    // 右薬指第二指骨のボーン
    HumanBodyBones rightRingDistalBone = HumanBodyBones.RightRingDistal;                // 右薬指第三指骨のボーン
    HumanBodyBones rightLittleProximalBone = HumanBodyBones.RightLittleProximal;        // 右小指第一指骨のボーン
    HumanBodyBones rightLittleIntermediateBone = HumanBodyBones.RightLittleIntermediate; // 右小指第二指骨のボーン
    HumanBodyBones rightLittleDistalBone = HumanBodyBones.RightLittleDistal;            // 右小指第三指骨のボーン

    // 位置を記録する配列
    private Vector3[] recordPosition;
    // ボーンの回転を記録する配列
    private Quaternion[][] recordBones;
    // カメラ相対のアバター回転を記録する配列
    private Quaternion[] recordRelativeRotations;
    // 録画時のカメラ回転を記録する配列
    private Quaternion[] recordCameraRotations;

    // ボーンに対応するTransformの配列
    private Transform[] avatarBones;
    private bool isActive = false;
    private bool isPlay = false;

    private int currentFrame = 0;
    private int startFrame = 0;
    private int endFrame = 0;
    private bool isLoop = false; // リングバッファが1周したか


    public void setTargetPlayer(VRCPlayerApi targetPlayer)
    {
        sourcePlayer = targetPlayer;
    }

    public void Activate()
    {
        Debug.Log("ModelController is now active.");
        startFrame = currentFrame;
        isActive = true;
    }

    public void Deactivate()
    {
        Debug.Log("ModelController is now inactive.");
        // startFrame ~ endFrame は閉区間
        isActive = false;
        endFrame = (currentFrame - 1 + recordFrames) % recordFrames;
        if (isLoop)
        {
            startFrame = currentFrame;
        }
        currentFrame = startFrame;
    }

    public void Play()
    {
        if (isActive)
        {
            Debug.LogWarning("Cannot play while recording is active.");
        }
        else if (isPlay)
        {
            isPlay = false;
            Debug.Log("Playback stopped.");
        }
        else if (!recordMode)
        {
            Debug.LogWarning("Cannot play while not in record mode.");
        }
        else
        {
            isPlay = true;
            Debug.Log("Playback started from frame " + startFrame + " to frame " + endFrame);
        }
    }

    void Start()
    {
        if (sourcePlayer == null)
        {
            sourcePlayer = Networking.LocalPlayer;
        }

        // アバターのボーン配列を初期化
        if (targetAvatar != null)
        {
            Animator avatarAnimator = targetAvatar.GetComponent<Animator>();
            if (avatarAnimator != null)
            {
                // ボーンの総数分の配列を作成
                avatarBones = new Transform[55]; // HumanBodyBonesの総数

                // 各ボーンのTransformを取得
                avatarBones[0] = avatarAnimator.GetBoneTransform(hipsBone);
                avatarBones[1] = avatarAnimator.GetBoneTransform(leftUpperLegBone);
                avatarBones[2] = avatarAnimator.GetBoneTransform(rightUpperLegBone);
                avatarBones[3] = avatarAnimator.GetBoneTransform(leftLowerLegBone);
                avatarBones[4] = avatarAnimator.GetBoneTransform(rightLowerLegBone);
                avatarBones[5] = avatarAnimator.GetBoneTransform(leftFootBone);
                avatarBones[6] = avatarAnimator.GetBoneTransform(rightFootBone);
                avatarBones[7] = avatarAnimator.GetBoneTransform(spineBone);
                avatarBones[8] = avatarAnimator.GetBoneTransform(chestBone);
                avatarBones[9] = avatarAnimator.GetBoneTransform(upperChestBone);
                avatarBones[10] = avatarAnimator.GetBoneTransform(neckBone);
                avatarBones[11] = avatarAnimator.GetBoneTransform(headBone);
                avatarBones[12] = avatarAnimator.GetBoneTransform(leftShoulderBone);
                avatarBones[13] = avatarAnimator.GetBoneTransform(rightShoulderBone);
                avatarBones[14] = avatarAnimator.GetBoneTransform(leftUpperArmBone);
                avatarBones[15] = avatarAnimator.GetBoneTransform(rightUpperArmBone);
                avatarBones[16] = avatarAnimator.GetBoneTransform(leftLowerArmBone);
                avatarBones[17] = avatarAnimator.GetBoneTransform(rightLowerArmBone);
                avatarBones[18] = avatarAnimator.GetBoneTransform(leftHandBone);
                avatarBones[19] = avatarAnimator.GetBoneTransform(rightHandBone);
                avatarBones[20] = avatarAnimator.GetBoneTransform(leftToesBone);
                avatarBones[21] = avatarAnimator.GetBoneTransform(rightToesBone);
                avatarBones[22] = avatarAnimator.GetBoneTransform(leftEyeBone);
                avatarBones[23] = avatarAnimator.GetBoneTransform(rightEyeBone);
                avatarBones[24] = avatarAnimator.GetBoneTransform(jawBone);

                // 左手の指
                avatarBones[25] = avatarAnimator.GetBoneTransform(leftThumbProximalBone);
                avatarBones[26] = avatarAnimator.GetBoneTransform(leftThumbIntermediateBone);
                avatarBones[27] = avatarAnimator.GetBoneTransform(leftThumbDistalBone);
                avatarBones[28] = avatarAnimator.GetBoneTransform(leftIndexProximalBone);
                avatarBones[29] = avatarAnimator.GetBoneTransform(leftIndexIntermediateBone);
                avatarBones[30] = avatarAnimator.GetBoneTransform(leftIndexDistalBone);
                avatarBones[31] = avatarAnimator.GetBoneTransform(leftMiddleProximalBone);
                avatarBones[32] = avatarAnimator.GetBoneTransform(leftMiddleIntermediateBone);
                avatarBones[33] = avatarAnimator.GetBoneTransform(leftMiddleDistalBone);
                avatarBones[34] = avatarAnimator.GetBoneTransform(leftRingProximalBone);
                avatarBones[35] = avatarAnimator.GetBoneTransform(leftRingIntermediateBone);
                avatarBones[36] = avatarAnimator.GetBoneTransform(leftRingDistalBone);
                avatarBones[37] = avatarAnimator.GetBoneTransform(leftLittleProximalBone);
                avatarBones[38] = avatarAnimator.GetBoneTransform(leftLittleIntermediateBone);
                avatarBones[39] = avatarAnimator.GetBoneTransform(leftLittleDistalBone);

                // 右手の指
                avatarBones[40] = avatarAnimator.GetBoneTransform(rightThumbProximalBone);
                avatarBones[41] = avatarAnimator.GetBoneTransform(rightThumbIntermediateBone);
                avatarBones[42] = avatarAnimator.GetBoneTransform(rightThumbDistalBone);
                avatarBones[43] = avatarAnimator.GetBoneTransform(rightIndexProximalBone);
                avatarBones[44] = avatarAnimator.GetBoneTransform(rightIndexIntermediateBone);
                avatarBones[45] = avatarAnimator.GetBoneTransform(rightIndexDistalBone);
                avatarBones[46] = avatarAnimator.GetBoneTransform(rightMiddleProximalBone);
                avatarBones[47] = avatarAnimator.GetBoneTransform(rightMiddleIntermediateBone);
                avatarBones[48] = avatarAnimator.GetBoneTransform(rightMiddleDistalBone);
                avatarBones[49] = avatarAnimator.GetBoneTransform(rightRingProximalBone);
                avatarBones[50] = avatarAnimator.GetBoneTransform(rightRingIntermediateBone);
                avatarBones[51] = avatarAnimator.GetBoneTransform(rightRingDistalBone);
                avatarBones[52] = avatarAnimator.GetBoneTransform(rightLittleProximalBone);
                avatarBones[53] = avatarAnimator.GetBoneTransform(rightLittleIntermediateBone);
                avatarBones[54] = avatarAnimator.GetBoneTransform(rightLittleDistalBone);
            }
            else
                Debug.LogError("Animator component not found on target avatar.");
        }

        {
            // 55ボーン × recordFrames の2次元配列で初期化
            recordBones = new Quaternion[55][];
            for (int i = 0; i < 55; i++)
            {
                recordBones[i] = new Quaternion[recordFrames];
            }
            
            // 位置記録配列を初期化
            recordPosition = new Vector3[recordFrames];
            
            // カメラ相対回転記録配列を初期化
            recordRelativeRotations = new Quaternion[recordFrames];
            
            // 録画時カメラ回転記録配列を初期化
            recordCameraRotations = new Quaternion[recordFrames];
        }
    }

    void Update()
    {
        if (isActive)
        {
            
        }
        else if (isPlay)
        {

        }
    }
}

記録部分

アバターの位置・アバターの回転・それぞれのボーンの回転をそれぞれ取得し、配列に保存します。
それぞれ VRCPlayerApiGetPosition GetRotation GetBoneTransformを使うことで取得できます

またこの時点で、アバターの位置・アバターの回転を、カメラの正面方向からの相対的な位置・回転として保存しておきます。

// 録画部分だけ
void Update()
{
    ApplyPosition();

    ApplyBoneRotation(0, sourcePlayer.GetBoneRotation(hipsBone));
    // 中略
    ApplyBoneRotation(54, sourcePlayer.GetBoneRotation(rightLittleDistalBone));

    currentFrame = (currentFrame + 1) % recordFrames;
    if (currentFrame == startFrame)
    {
        isLoop = true; // リングバッファが1周した
    }
}

// アバターの位置・回転の保存
private void ApplyPosition()
{
    Vector3 sourcePosition = sourcePlayer.GetPosition();
    Vector3 positionFromCamera = sourcePosition - this.transform.position;
    // 相対位置を記録
    recordPosition[currentFrame] = positionFromCamera;

    // カメラ相対の回転を計算して記録
    Quaternion relativeRotation = Quaternion.Inverse(this.transform.rotation) * sourcePlayer.GetRotation();
    recordRelativeRotations[currentFrame] = relativeRotation;

    // 録画時のカメラ回転を記録
    recordCameraRotations[currentFrame] = this.transform.rotation;
}

// ボーン情報の保存
private void ApplyBoneRotation(int index, Quaternion boneRotation)
{
    recordBones[index][currentFrame] = boneRotation;
}

再生部分

保存した情報を1フレームずつ復元していきます。ボーンの回転はそのままアバターに適用するだけで良いですが、アバターの位置と回転は座標系をカメラ原点の座標系から原点中心の絶対座標に変換する必要があるので注意が必要です

// 再生部分だけ
void Update()
{
    ApplyPosition();
    for (int i = 0; i <= 54; i++)
    {
        ApplyBoneRotation(i, recordBones[i][currentFrame]);
    }
    if (currentFrame == endFrame)
    {
        isPlay = false;
        currentFrame = startFrame;
    }
    currentFrame = (currentFrame + 1) % recordFrames;
}

private void ApplyPosition()
{
    // 録画時と現在のカメラ回転の差分を計算
    Quaternion cameraRotationDiff = this.transform.rotation * Quaternion.Inverse(recordCameraRotations[currentFrame]);

    // 記録した相対位置を現在のカメラ回転で変換
    Vector3 rotatedPosition = cameraRotationDiff * recordPosition[currentFrame];
    targetAvatar.transform.position = rotatedPosition + this.transform.position;

    // アバターの回転は録画時のソースプレイヤーの回転をそのまま適用
    targetAvatar.transform.rotation = recordRelativeRotations[currentFrame];
}

private void ApplyBoneRotation(int index, Quaternion boneRotation)
{
    // avatarBones[0] = avatarAnimator.GetBoneTransform(hipsBone)
    avatarBones[index].rotation = boneRotation;
}

以上を組み合わせると、アバターの動きを記録し、好きな場所で再生するギミックを作ることができます!

おわりに

今回始めて UdonSharp を触ったので、Unity との違いや、カメラとアバターの回転関係に苦労しました。特にカメラを動かした時に再生アバターも追従して動くための処理が難しくだいぶAIに頼ってしまいました

最後に完成したギミックの全体コードです

完成コード
using System;
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class ModelController : UdonSharpBehaviour
{
    // 動きを反映するもとのプレイヤー
    private VRCPlayerApi sourcePlayer;

    // ワールドに設置したアバターのボーンをインスペクターから設定
    public GameObject targetAvatar;

    public int recordFrames = 3000;

    public bool recordMode = true;

    // すべてのHumanBodyBonesの変数定義
    HumanBodyBones hipsBone = HumanBodyBones.Hips;                      // 尻のボーン
    HumanBodyBones leftUpperLegBone = HumanBodyBones.LeftUpperLeg;      // 左太もものボーン
    HumanBodyBones rightUpperLegBone = HumanBodyBones.RightUpperLeg;    // 右太もものボーン
    HumanBodyBones leftLowerLegBone = HumanBodyBones.LeftLowerLeg;      // 左ひざのボーン
    HumanBodyBones rightLowerLegBone = HumanBodyBones.RightLowerLeg;    // 右ひざのボーン
    HumanBodyBones leftFootBone = HumanBodyBones.LeftFoot;              // 左足首のボーン
    HumanBodyBones rightFootBone = HumanBodyBones.RightFoot;            // 右足首のボーン
    HumanBodyBones spineBone = HumanBodyBones.Spine;                    // 背骨の第一ボーン
    HumanBodyBones chestBone = HumanBodyBones.Chest;                    // 胸のボーン
    HumanBodyBones upperChestBone = HumanBodyBones.UpperChest;          // 上胸のボーン
    HumanBodyBones neckBone = HumanBodyBones.Neck;                      // 首のボーン
    HumanBodyBones headBone = HumanBodyBones.Head;                      // 頭のボーン
    HumanBodyBones leftShoulderBone = HumanBodyBones.LeftShoulder;      // 左肩のボーン
    HumanBodyBones rightShoulderBone = HumanBodyBones.RightShoulder;    // 右肩のボーン
    HumanBodyBones leftUpperArmBone = HumanBodyBones.LeftUpperArm;      // 左上腕のボーン
    HumanBodyBones rightUpperArmBone = HumanBodyBones.RightUpperArm;    // 右上腕のボーン
    HumanBodyBones leftLowerArmBone = HumanBodyBones.LeftLowerArm;      // 左ひじのボーン
    HumanBodyBones rightLowerArmBone = HumanBodyBones.RightLowerArm;    // 右ひじのボーン
    HumanBodyBones leftHandBone = HumanBodyBones.LeftHand;              // 左手首のボーン
    HumanBodyBones rightHandBone = HumanBodyBones.RightHand;            // 右手首のボーン
    HumanBodyBones leftToesBone = HumanBodyBones.LeftToes;              // 左つま先のボーン
    HumanBodyBones rightToesBone = HumanBodyBones.RightToes;            // 右つま先のボーン
    HumanBodyBones leftEyeBone = HumanBodyBones.LeftEye;                // 左目のボーン
    HumanBodyBones rightEyeBone = HumanBodyBones.RightEye;              // 右目のボーン
    HumanBodyBones jawBone = HumanBodyBones.Jaw;                        // 顎のボーン

    // 左手の指のボーン
    HumanBodyBones leftThumbProximalBone = HumanBodyBones.LeftThumbProximal;            // 左親指第一指骨のボーン
    HumanBodyBones leftThumbIntermediateBone = HumanBodyBones.LeftThumbIntermediate;    // 左親指第二指骨のボーン
    HumanBodyBones leftThumbDistalBone = HumanBodyBones.LeftThumbDistal;                // 左親指第三指骨のボーン
    HumanBodyBones leftIndexProximalBone = HumanBodyBones.LeftIndexProximal;            // 左人差し指第一指骨のボーン
    HumanBodyBones leftIndexIntermediateBone = HumanBodyBones.LeftIndexIntermediate;    // 左人差し指第二指骨のボーン
    HumanBodyBones leftIndexDistalBone = HumanBodyBones.LeftIndexDistal;                // 左人差し指第三指骨のボーン
    HumanBodyBones leftMiddleProximalBone = HumanBodyBones.LeftMiddleProximal;          // 左中指第一指骨のボーン
    HumanBodyBones leftMiddleIntermediateBone = HumanBodyBones.LeftMiddleIntermediate;  // 左中指第二指骨のボーン
    HumanBodyBones leftMiddleDistalBone = HumanBodyBones.LeftMiddleDistal;              // 左中指第三指骨のボーン
    HumanBodyBones leftRingProximalBone = HumanBodyBones.LeftRingProximal;              // 左薬指第一指骨のボーン
    HumanBodyBones leftRingIntermediateBone = HumanBodyBones.LeftRingIntermediate;      // 左薬指第二指骨のボーン
    HumanBodyBones leftRingDistalBone = HumanBodyBones.LeftRingDistal;                  // 左薬指第三指骨のボーン
    HumanBodyBones leftLittleProximalBone = HumanBodyBones.LeftLittleProximal;          // 左小指第一指骨のボーン
    HumanBodyBones leftLittleIntermediateBone = HumanBodyBones.LeftLittleIntermediate;  // 左小指第二指骨のボーン
    HumanBodyBones leftLittleDistalBone = HumanBodyBones.LeftLittleDistal;              // 左小指第三指骨のボーン

    // 右手の指のボーン
    HumanBodyBones rightThumbProximalBone = HumanBodyBones.RightThumbProximal;          // 右親指第一指骨のボーン
    HumanBodyBones rightThumbIntermediateBone = HumanBodyBones.RightThumbIntermediate;  // 右親指第二指骨のボーン
    HumanBodyBones rightThumbDistalBone = HumanBodyBones.RightThumbDistal;              // 右親指第三指骨のボーン
    HumanBodyBones rightIndexProximalBone = HumanBodyBones.RightIndexProximal;          // 右人差し指第一指骨のボーン
    HumanBodyBones rightIndexIntermediateBone = HumanBodyBones.RightIndexIntermediate;  // 右人差し指第二指骨のボーン
    HumanBodyBones rightIndexDistalBone = HumanBodyBones.RightIndexDistal;              // 右人差し指第三指骨のボーン
    HumanBodyBones rightMiddleProximalBone = HumanBodyBones.RightMiddleProximal;        // 右中指第一指骨のボーン
    HumanBodyBones rightMiddleIntermediateBone = HumanBodyBones.RightMiddleIntermediate; // 右中指第二指骨のボーン
    HumanBodyBones rightMiddleDistalBone = HumanBodyBones.RightMiddleDistal;            // 右中指第三指骨のボーン
    HumanBodyBones rightRingProximalBone = HumanBodyBones.RightRingProximal;            // 右薬指第一指骨のボーン
    HumanBodyBones rightRingIntermediateBone = HumanBodyBones.RightRingIntermediate;    // 右薬指第二指骨のボーン
    HumanBodyBones rightRingDistalBone = HumanBodyBones.RightRingDistal;                // 右薬指第三指骨のボーン
    HumanBodyBones rightLittleProximalBone = HumanBodyBones.RightLittleProximal;        // 右小指第一指骨のボーン
    HumanBodyBones rightLittleIntermediateBone = HumanBodyBones.RightLittleIntermediate; // 右小指第二指骨のボーン
    HumanBodyBones rightLittleDistalBone = HumanBodyBones.RightLittleDistal;            // 右小指第三指骨のボーン

    // 位置を記録する配列
    private Vector3[] recordPosition;
    // ボーンの回転を記録する配列
    private Quaternion[][] recordBones;
    // カメラ相対のアバター回転を記録する配列
    private Quaternion[] recordRelativeRotations;
    // 録画時のカメラ回転を記録する配列
    private Quaternion[] recordCameraRotations;

    // ボーンに対応するTransformの配列
    private Transform[] avatarBones;
    private bool isActive = false;
    private bool isPlay = false;

    private int currentFrame = 0;
    private int startFrame = 0;
    private int endFrame = 0;
    private bool isLoop = false; // リングバッファが1周したか


    public void setTargetPlayer(VRCPlayerApi targetPlayer)
    {
        sourcePlayer = targetPlayer;
    }

    public void Activate()
    {
        Debug.Log("ModelController is now active.");
        startFrame = currentFrame;
        isActive = true;
    }

    public void Deactivate()
    {
        Debug.Log("ModelController is now inactive.");
        // startFrame ~ endFrame は閉区間
        isActive = false;
        endFrame = (currentFrame - 1 + recordFrames) % recordFrames;
        if (isLoop)
        {
            startFrame = currentFrame;
        }
        currentFrame = startFrame;
    }

    public void Play()
    {
        if (isActive)
        {
            Debug.LogWarning("Cannot play while recording is active.");
        }
        else if (isPlay)
        {
            isPlay = false;
            Debug.Log("Playback stopped.");
        }
        else if (!recordMode)
        {
            Debug.LogWarning("Cannot play while not in record mode.");
        }
        else
        {
            isPlay = true;
            Debug.Log("Playback started from frame " + startFrame + " to frame " + endFrame);
        }
    }

    void Start()
    {
        if (sourcePlayer == null)
        {
            sourcePlayer = Networking.LocalPlayer;
        }

        // アバターのボーン配列を初期化
        if (targetAvatar != null)
        {
            Animator avatarAnimator = targetAvatar.GetComponent<Animator>();
            if (avatarAnimator != null)
            {
                // ボーンの総数分の配列を作成
                avatarBones = new Transform[55]; // HumanBodyBonesの総数

                // 各ボーンのTransformを取得
                avatarBones[0] = avatarAnimator.GetBoneTransform(hipsBone);
                avatarBones[1] = avatarAnimator.GetBoneTransform(leftUpperLegBone);
                avatarBones[2] = avatarAnimator.GetBoneTransform(rightUpperLegBone);
                avatarBones[3] = avatarAnimator.GetBoneTransform(leftLowerLegBone);
                avatarBones[4] = avatarAnimator.GetBoneTransform(rightLowerLegBone);
                avatarBones[5] = avatarAnimator.GetBoneTransform(leftFootBone);
                avatarBones[6] = avatarAnimator.GetBoneTransform(rightFootBone);
                avatarBones[7] = avatarAnimator.GetBoneTransform(spineBone);
                avatarBones[8] = avatarAnimator.GetBoneTransform(chestBone);
                avatarBones[9] = avatarAnimator.GetBoneTransform(upperChestBone);
                avatarBones[10] = avatarAnimator.GetBoneTransform(neckBone);
                avatarBones[11] = avatarAnimator.GetBoneTransform(headBone);
                avatarBones[12] = avatarAnimator.GetBoneTransform(leftShoulderBone);
                avatarBones[13] = avatarAnimator.GetBoneTransform(rightShoulderBone);
                avatarBones[14] = avatarAnimator.GetBoneTransform(leftUpperArmBone);
                avatarBones[15] = avatarAnimator.GetBoneTransform(rightUpperArmBone);
                avatarBones[16] = avatarAnimator.GetBoneTransform(leftLowerArmBone);
                avatarBones[17] = avatarAnimator.GetBoneTransform(rightLowerArmBone);
                avatarBones[18] = avatarAnimator.GetBoneTransform(leftHandBone);
                avatarBones[19] = avatarAnimator.GetBoneTransform(rightHandBone);
                avatarBones[20] = avatarAnimator.GetBoneTransform(leftToesBone);
                avatarBones[21] = avatarAnimator.GetBoneTransform(rightToesBone);
                avatarBones[22] = avatarAnimator.GetBoneTransform(leftEyeBone);
                avatarBones[23] = avatarAnimator.GetBoneTransform(rightEyeBone);
                avatarBones[24] = avatarAnimator.GetBoneTransform(jawBone);

                // 左手の指
                avatarBones[25] = avatarAnimator.GetBoneTransform(leftThumbProximalBone);
                avatarBones[26] = avatarAnimator.GetBoneTransform(leftThumbIntermediateBone);
                avatarBones[27] = avatarAnimator.GetBoneTransform(leftThumbDistalBone);
                avatarBones[28] = avatarAnimator.GetBoneTransform(leftIndexProximalBone);
                avatarBones[29] = avatarAnimator.GetBoneTransform(leftIndexIntermediateBone);
                avatarBones[30] = avatarAnimator.GetBoneTransform(leftIndexDistalBone);
                avatarBones[31] = avatarAnimator.GetBoneTransform(leftMiddleProximalBone);
                avatarBones[32] = avatarAnimator.GetBoneTransform(leftMiddleIntermediateBone);
                avatarBones[33] = avatarAnimator.GetBoneTransform(leftMiddleDistalBone);
                avatarBones[34] = avatarAnimator.GetBoneTransform(leftRingProximalBone);
                avatarBones[35] = avatarAnimator.GetBoneTransform(leftRingIntermediateBone);
                avatarBones[36] = avatarAnimator.GetBoneTransform(leftRingDistalBone);
                avatarBones[37] = avatarAnimator.GetBoneTransform(leftLittleProximalBone);
                avatarBones[38] = avatarAnimator.GetBoneTransform(leftLittleIntermediateBone);
                avatarBones[39] = avatarAnimator.GetBoneTransform(leftLittleDistalBone);

                // 右手の指
                avatarBones[40] = avatarAnimator.GetBoneTransform(rightThumbProximalBone);
                avatarBones[41] = avatarAnimator.GetBoneTransform(rightThumbIntermediateBone);
                avatarBones[42] = avatarAnimator.GetBoneTransform(rightThumbDistalBone);
                avatarBones[43] = avatarAnimator.GetBoneTransform(rightIndexProximalBone);
                avatarBones[44] = avatarAnimator.GetBoneTransform(rightIndexIntermediateBone);
                avatarBones[45] = avatarAnimator.GetBoneTransform(rightIndexDistalBone);
                avatarBones[46] = avatarAnimator.GetBoneTransform(rightMiddleProximalBone);
                avatarBones[47] = avatarAnimator.GetBoneTransform(rightMiddleIntermediateBone);
                avatarBones[48] = avatarAnimator.GetBoneTransform(rightMiddleDistalBone);
                avatarBones[49] = avatarAnimator.GetBoneTransform(rightRingProximalBone);
                avatarBones[50] = avatarAnimator.GetBoneTransform(rightRingIntermediateBone);
                avatarBones[51] = avatarAnimator.GetBoneTransform(rightRingDistalBone);
                avatarBones[52] = avatarAnimator.GetBoneTransform(rightLittleProximalBone);
                avatarBones[53] = avatarAnimator.GetBoneTransform(rightLittleIntermediateBone);
                avatarBones[54] = avatarAnimator.GetBoneTransform(rightLittleDistalBone);
            }
            else
                Debug.LogError("Animator component not found on target avatar.");
        }

        {
            // 55ボーン × recordFrames の2次元配列で初期化
            recordBones = new Quaternion[55][];
            for (int i = 0; i < 55; i++)
            {
                recordBones[i] = new Quaternion[recordFrames];
            }
            
            // 位置記録配列を初期化
            recordPosition = new Vector3[recordFrames];
            
            // カメラ相対回転記録配列を初期化
            recordRelativeRotations = new Quaternion[recordFrames];
            
            // 録画時カメラ回転記録配列を初期化
            recordCameraRotations = new Quaternion[recordFrames];
        }
    }

    void Update()
    {
        if (isActive)
        {
            if (sourcePlayer != null && avatarBones != null)
            {
                ApplyPosition();
                // 各ボーンの回転情報をプレイヤーから取得してアバターに適用
                ApplyBoneRotation(0, sourcePlayer.GetBoneRotation(hipsBone));
                ApplyBoneRotation(1, sourcePlayer.GetBoneRotation(leftUpperLegBone));
                ApplyBoneRotation(2, sourcePlayer.GetBoneRotation(rightUpperLegBone));
                ApplyBoneRotation(3, sourcePlayer.GetBoneRotation(leftLowerLegBone));
                ApplyBoneRotation(4, sourcePlayer.GetBoneRotation(rightLowerLegBone));
                ApplyBoneRotation(5, sourcePlayer.GetBoneRotation(leftFootBone));
                ApplyBoneRotation(6, sourcePlayer.GetBoneRotation(rightFootBone));
                ApplyBoneRotation(7, sourcePlayer.GetBoneRotation(spineBone));
                ApplyBoneRotation(8, sourcePlayer.GetBoneRotation(chestBone));
                ApplyBoneRotation(9, sourcePlayer.GetBoneRotation(upperChestBone));
                ApplyBoneRotation(10, sourcePlayer.GetBoneRotation(neckBone));
                ApplyBoneRotation(11, sourcePlayer.GetBoneRotation(headBone));
                ApplyBoneRotation(12, sourcePlayer.GetBoneRotation(leftShoulderBone));
                ApplyBoneRotation(13, sourcePlayer.GetBoneRotation(rightShoulderBone));
                ApplyBoneRotation(14, sourcePlayer.GetBoneRotation(leftUpperArmBone));
                ApplyBoneRotation(15, sourcePlayer.GetBoneRotation(rightUpperArmBone));
                ApplyBoneRotation(16, sourcePlayer.GetBoneRotation(leftLowerArmBone));
                ApplyBoneRotation(17, sourcePlayer.GetBoneRotation(rightLowerArmBone));
                ApplyBoneRotation(18, sourcePlayer.GetBoneRotation(leftHandBone));
                ApplyBoneRotation(19, sourcePlayer.GetBoneRotation(rightHandBone));
                ApplyBoneRotation(20, sourcePlayer.GetBoneRotation(leftToesBone));
                ApplyBoneRotation(21, sourcePlayer.GetBoneRotation(rightToesBone));
                ApplyBoneRotation(22, sourcePlayer.GetBoneRotation(leftEyeBone));
                ApplyBoneRotation(23, sourcePlayer.GetBoneRotation(rightEyeBone));
                ApplyBoneRotation(24, sourcePlayer.GetBoneRotation(jawBone));

                // 左手の指
                ApplyBoneRotation(25, sourcePlayer.GetBoneRotation(leftThumbProximalBone));
                ApplyBoneRotation(26, sourcePlayer.GetBoneRotation(leftThumbIntermediateBone));
                ApplyBoneRotation(27, sourcePlayer.GetBoneRotation(leftThumbDistalBone));
                ApplyBoneRotation(28, sourcePlayer.GetBoneRotation(leftIndexProximalBone));
                ApplyBoneRotation(29, sourcePlayer.GetBoneRotation(leftIndexIntermediateBone));
                ApplyBoneRotation(30, sourcePlayer.GetBoneRotation(leftIndexDistalBone));
                ApplyBoneRotation(31, sourcePlayer.GetBoneRotation(leftMiddleProximalBone));
                ApplyBoneRotation(32, sourcePlayer.GetBoneRotation(leftMiddleIntermediateBone));
                ApplyBoneRotation(33, sourcePlayer.GetBoneRotation(leftMiddleDistalBone));
                ApplyBoneRotation(34, sourcePlayer.GetBoneRotation(leftRingProximalBone));
                ApplyBoneRotation(35, sourcePlayer.GetBoneRotation(leftRingIntermediateBone));
                ApplyBoneRotation(36, sourcePlayer.GetBoneRotation(leftRingDistalBone));
                ApplyBoneRotation(37, sourcePlayer.GetBoneRotation(leftLittleProximalBone));
                ApplyBoneRotation(38, sourcePlayer.GetBoneRotation(leftLittleIntermediateBone));
                ApplyBoneRotation(39, sourcePlayer.GetBoneRotation(leftLittleDistalBone));

                // 右手の指
                ApplyBoneRotation(40, sourcePlayer.GetBoneRotation(rightThumbProximalBone));
                ApplyBoneRotation(41, sourcePlayer.GetBoneRotation(rightThumbIntermediateBone));
                ApplyBoneRotation(42, sourcePlayer.GetBoneRotation(rightThumbDistalBone));
                ApplyBoneRotation(43, sourcePlayer.GetBoneRotation(rightIndexProximalBone));
                ApplyBoneRotation(44, sourcePlayer.GetBoneRotation(rightIndexIntermediateBone));
                ApplyBoneRotation(45, sourcePlayer.GetBoneRotation(rightIndexDistalBone));
                ApplyBoneRotation(46, sourcePlayer.GetBoneRotation(rightMiddleProximalBone));
                ApplyBoneRotation(47, sourcePlayer.GetBoneRotation(rightMiddleIntermediateBone));
                ApplyBoneRotation(48, sourcePlayer.GetBoneRotation(rightMiddleDistalBone));
                ApplyBoneRotation(49, sourcePlayer.GetBoneRotation(rightRingProximalBone));
                ApplyBoneRotation(50, sourcePlayer.GetBoneRotation(rightRingIntermediateBone));
                ApplyBoneRotation(51, sourcePlayer.GetBoneRotation(rightRingDistalBone));
                ApplyBoneRotation(52, sourcePlayer.GetBoneRotation(rightLittleProximalBone));
                ApplyBoneRotation(53, sourcePlayer.GetBoneRotation(rightLittleIntermediateBone));
                ApplyBoneRotation(54, sourcePlayer.GetBoneRotation(rightLittleDistalBone));
            }
            else
            {
                if (sourcePlayer == null)
                    Debug.LogError("Local player is not initialized.");
                if (avatarBones == null)
                    Debug.LogError("Avatar bones are not initialized.");
            }

            if (recordMode)
            {
                currentFrame = (currentFrame + 1) % recordFrames;
                if (currentFrame == startFrame)
                {
                    isLoop = true; // リングバッファが1周した
                }
            }
        }
        else if (isPlay)
        {
            {
                ApplyPosition();
                for (int i = 0; i <= 54; i++)
                {
                    ApplyBoneRotation(i, recordBones[i][currentFrame]);
                }
            }
            if (currentFrame == endFrame)
            {
                isPlay = false;
                Debug.Log("Playback ended at frame " + endFrame);
                currentFrame = startFrame;
            }
            currentFrame = (currentFrame + 1) % recordFrames;
            Debug.Log("Playback current frame: " + currentFrame);
        }
    }

    private void ApplyPosition()
    {
        if (targetAvatar == null)
        {
            Debug.LogError("Target avatar is not assigned.");
            return;
        }

        if (sourcePlayer == null)
        {
            Debug.LogError("Source player is not assigned.");
            return;
        }

        Vector3 sourcePosition = sourcePlayer.GetPosition();
        Vector3 positionFromCamera = sourcePosition - this.transform.position;

        if (recordMode)
        {
            if (isPlay)
            {
                // 録画時と現在のカメラ回転の差分を計算
                Quaternion cameraRotationDiff = this.transform.rotation * Quaternion.Inverse(recordCameraRotations[currentFrame]);
                
                // 記録した相対位置を現在のカメラ回転で変換
                Vector3 rotatedPosition = cameraRotationDiff * recordPosition[currentFrame];
                targetAvatar.transform.position = rotatedPosition + this.transform.position;
                
                // アバターの回転は録画時のソースプレイヤーの回転をそのまま適用
                targetAvatar.transform.rotation = recordRelativeRotations[currentFrame];
            }
            else
            {
                // 相対位置を記録
                recordPosition[currentFrame] = positionFromCamera;
                
                // カメラ相対の回転を計算して記録
                Quaternion relativeRotation = Quaternion.Inverse(this.transform.rotation) * sourcePlayer.GetRotation();
                recordRelativeRotations[currentFrame] = relativeRotation;
                
                // 録画時のカメラ回転を記録
                recordCameraRotations[currentFrame] = this.transform.rotation;
            }
        }
        else
        {
            targetAvatar.transform.position = positionFromCamera + this.transform.position;
            targetAvatar.transform.rotation = sourcePlayer.GetRotation();
        }
    }

    // ボーンの回転を適用するヘルパーメソッド
    private void ApplyBoneRotation(int index, Quaternion boneRotation)
    {
        if (avatarBones[index] == null || boneRotation == null)
        {
            Debug.LogWarning("Bone index " + index + " is not properly initialized.");
            return;
        }

        if (recordMode)
        {
            if (isPlay)
            {
                avatarBones[index].rotation = boneRotation;
            }
            else
            {
                recordBones[index][currentFrame] = boneRotation;
            }
        }
        else
        {
            avatarBones[index].rotation = boneRotation;
        }
    }
}

Discussion