AnimationCurveを使わずにVRMをC#で走らせてみる【Unity】

これは [Unity Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unity) の25日目の記事です。
昨日は @youri_ssk さんによる [2.5Dキャラクターアニメーション - Mirror Animation Playable](https://qiita.com/youri_ssk/items/eef0e857b3b0dbee1c46) でした。



# Animation Curveを使わずにC#だけでVRMを走らせてみる

Animation Curveを使えば、便利なGUIで3Dアニメーションを作ることができます！

そう！普通の人ならAnimation Curveを使いましょう！！（もちろん、BlenderとかUnity外のツールでもいいです）

しかし！Qiita読者の皆さんは **プログラマー** なんです！

**プログラマーだったらプログラミングで3Dアニメーションを作りたいですよね！**

というわけで、VRMアバターをC#で動かしてみます！

最終的にはこんな感じのアニメーションが作れました。

![201912241856a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/68379ee3-1715-ce3b-cdc6-208f7dc5ca17.gif)



# 動作環境

以下の環境でやりました。

- Windows 10 64bit
- Unity 2019.2.11f1
- UniVRM 0.54.0



# 初期状態

とりあえずVRMファイルをシーンに読み込んでみます。

今回は [今里尚吾](https://hub.vroid.com/characters/4381082594801486356/models/1126331747952034917) くんにご協力いただきます！（VRoid Studioで作りました！）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/17030858-60c2-1e30-3f24-d930c1bb28fa.png)



Hierarchyを見てみたらこんな感じでした！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/3a6331c5-8288-6f23-121e-527bf0177e12.png)



# 股関節を30度に曲げてみる

まずは、股関節を曲げてみましょう。

以下のスクリプトを作成します。

```c#
using System;
using UnityEngine;

public class PendulumRunning : MonoBehaviour
{
    // transformを保管する変数
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform rUpperLeg;

    void Start()
    {
        // 腰のtransformを取得
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        
        // 股関節のtransformを取得
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
    }

    void FixedUpdate()
    {
        // 脚を30°傾ける
        lUpperLeg.rotation = Quaternion.AngleAxis(-30.0f, Vector3.right);
        rUpperLeg.rotation = Quaternion.AngleAxis(30.0f, Vector3.right);
    }
}
```

作成できたら、シーン内のVRMにアタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/584ccec0-12c6-f4b6-6471-ac87b2c449e7.png)

脚が前後に開きました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/709321e3-9082-935e-be91-3df66d9684ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/c5da328e-80aa-57aa-d43a-1c4095d74e7e.png)



# 振り子のように足を揺らしてみる

せっかくなので、アニメーションさせたいですよね。

ということで、振り子のように揺らしてみます！

```c#
    void FixedUpdate()
    {
        // 1秒周期の振り子を用意する
        float pendulum = (float)Math.Sin(Time.time * Math.PI);
        
        // 股関節を右軸（x軸）を中心に±60°幅で揺らす
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum, Vector3.right);
    }
```

![201912241743a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/b77656b3-d5d2-fd28-a5e4-e53702350a5d.gif)

いいかんじ！



# 膝を曲げてみる

膝も曲げてみたくなったので、 `LowerLeg` を追加してみました。あと係数とかを微調整したのがこちらです。

```c#
public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform rUpperLeg;
    private Transform lLowerLeg;
    private Transform rLowerLeg;

    void Start()
    {
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
        
        // 膝のtransformを取得
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
    }

    void FixedUpdate()
    {
        float pendulum = (float)Math.Sin(Time.time * Math.PI);
        
        // 股関節の動きを少し変更
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);
        
        // 膝を揺らす
        lLowerLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 60.0f, Vector3.right);
    }
}
```

![201912241818a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/5af36fa9-f67a-441b-c060-b6f0f26630a7.gif)

膝も曲がりました。



# 全身動かしてみる

同じ要領で、全身の関節の動かしてみます。色々調整したら、以下のようなコードになりました。

```c#
public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform lLowerLeg;
    private Transform rUpperLeg;
    private Transform rLowerLeg;
    private Transform cSpine;
    private Transform cChest;
    private Transform cUpperChest;
    private Transform lShoulder;
    private Transform lUpperArm;
    private Transform lLowerArm;
    private Transform rShoulder;
    private Transform rUpperArm;
    private Transform rLowerArm;


    void Start()
    {
        // 全身の関節のtransformを取得
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
        cSpine = cHips.Find("J_Bip_C_Spine");
        cChest = cSpine.Find("J_Bip_C_Chest");
        cUpperChest = cChest.Find("J_Bip_C_UpperChest");
        lShoulder = cUpperChest.Find("J_Bip_L_Shoulder");
        lUpperArm = lShoulder.Find("J_Bip_L_UpperArm");
        lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm");
        rShoulder = cUpperChest.Find("J_Bip_R_Shoulder");
        rUpperArm = rShoulder.Find("J_Bip_R_UpperArm");
        rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm");
    }

    void FixedUpdate()
    {
        // 速度を3倍に変更
        float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f);
        
        // 脚を揺らす        
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);
        lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right);
        
        // 腰にひねりを加える
        cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right);
        
        // 胸は腰と反対にひねる
        cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up);
        cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up);
        
        // 腕を揺らす
        lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward);
        rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward);
        lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up);
        rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up);
    }
}
```

![201912241847a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/6d0da5e5-9a84-ad8b-6d61-ea77bddbc6db.gif)

だんだんそれっぽくなってきました。

ただ、重心が上下しないのはちょっと違和感がありますね。



# 重心を上下させてみた

重心も上下させてみます。

```c#
public class PendulumRunning : MonoBehaviour
{
    private Transform cHips;
    private Transform lUpperLeg;
    private Transform lLowerLeg;
    private Transform rUpperLeg;
    private Transform rLowerLeg;
    private Transform cSpine;
    private Transform cChest;
    private Transform cUpperChest;
    private Transform lShoulder;
    private Transform lUpperArm;
    private Transform lLowerArm;
    private Transform rShoulder;
    private Transform rUpperArm;
    private Transform rLowerArm;
    
    // 腰の初期位置を保管する変数
    private Vector3 firstHipsPosition;


    void Start()
    {
        cHips = transform.Find("Root")
                        .Find("J_Bip_C_Hips");
        lUpperLeg = cHips.Find("J_Bip_L_UpperLeg");
        lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg");
        rUpperLeg = cHips.Find("J_Bip_R_UpperLeg");
        rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg");
        cSpine = cHips.Find("J_Bip_C_Spine");
        cChest = cSpine.Find("J_Bip_C_Chest");
        cUpperChest = cChest.Find("J_Bip_C_UpperChest");
        lShoulder = cUpperChest.Find("J_Bip_L_Shoulder");
        lUpperArm = lShoulder.Find("J_Bip_L_UpperArm");
        lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm");
        rShoulder = cUpperChest.Find("J_Bip_R_Shoulder");
        rUpperArm = rShoulder.Find("J_Bip_R_UpperArm");
        rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm");

        // 腰の初期値を取得する
        firstHipsPosition = cHips.localPosition;
    }

    void FixedUpdate()
    {
        float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f);
        lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right);
        rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right);
        lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right);
        rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right);
        cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right);
        cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up);
        cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up);
        lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward);
        rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward);
        lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up);
        rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up);

        // 周期が半分の振り子を用意する
        float halfPendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f * 2.0f);
        
        // 腰の位置を上下させる
        cHips.localPosition = firstHipsPosition + new Vector3(0.0f, 0.04f * halfPendulum, 0.0f);
    }
}
```

![201912241856a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/68379ee3-1715-ce3b-cdc6-208f7dc5ca17.gif)

ちょ、ちょっとだけ、マシになったかな？

ゲシュタルト崩壊してきたので、このあたりで止めておきます。

一応、AnimationCurveは使わずにC#だけでアニメーションを作成することができました！



# AnimationClipに保存してみる

せっかくだからAnimationClipに保存してみます。

Unityの [GameObjectRecorder](https://docs.unity3d.com/ScriptReference/Animations.GameObjectRecorder.html) というAPIを使えば、シーン実行中のアニメーションをAnimationClipに保存できます。

以下のスクリプトを作成し、シーン内のVRMにアタッチしてください。

```c#
using UnityEngine;
using UnityEditor.Animations;

public class RecordTransformHierarchy : MonoBehaviour
{
    public AnimationClip clip;

    private GameObjectRecorder m_Recorder;

    void Start()
    {
        m_Recorder = new GameObjectRecorder(gameObject);
        m_Recorder.BindComponentsOfType<Transform>(gameObject, true);
    }

    void LateUpdate()
    {
        if (clip == null){
            return;
        }
        m_Recorder.TakeSnapshot(Time.deltaTime);
    }

    void OnDisable()
    {
        if (clip == null){
            return;
        }

        if (m_Recorder.isRecording)
        {
            m_Recorder.SaveToClip(clip);
        }
    }
}
```

適当なフォルダーに空のAnimationClipファイルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/1ce2e12e-5e4e-7e7b-7e47-b7153d9b7eb0.png)

ここでは仮に `pendulum-running` というファイル名にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/4ebff5a6-6b38-8aed-aff1-60ca816a8636.png)

これを `RecordTransformHierarchy` の `Clip` に割り当てます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/27947726-b734-ee7d-65c4-67a6b09cd819.png)

これでシーンを実行すれば、AnimationClipに動きが保管されます！

FBX Exporter等を使えば、FBXに変換することもできますね！



# さいごに

C#でもAnimationClipを作成することができました！

そのことに、果たして意味があるかはわかりませんが、個人的には `Quaternion` の勉強をできたのが収穫です。

本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。

- [【Unity】 Quaternion API解説](https://www.f-sp.com/entry/2017/08/11/194125)
- [Unity / GameObjectRecorder](https://docs.unity3d.com/ScriptReference/Animations.GameObjectRecorder.html) 
- [Unity GameObjectRecorderが面白いかもしれない](https://www.shibuya24.info/entry/gameobjectrecorder)