これは Unity Advent Calendar 2019 の25日目の記事です。
昨日は @youri_ssk さんによる 2.5Dキャラクターアニメーション - Mirror Animation Playable でした。
Animation Curveを使わずにC#だけでVRMを走らせてみる
Animation Curveを使えば、便利なGUIで3Dアニメーションを作ることができます!
そう!普通の人ならAnimation Curveを使いましょう!!(もちろん、BlenderとかUnity外のツールでもいいです)
しかし!Qiita読者の皆さんは プログラマー なんです!
プログラマーだったらプログラミングで3Dアニメーションを作りたいですよね!
というわけで、VRMアバターをC#で動かしてみます!
最終的にはこんな感じのアニメーションが作れました。
動作環境
以下の環境でやりました。
- Windows 10 64bit
- Unity 2019.2.11f1
- UniVRM 0.54.0
初期状態
とりあえずVRMファイルをシーンに読み込んでみます。
今回は 今里尚吾 くんにご協力いただきます!(VRoid Studioで作りました!)
Hierarchyを見てみたらこんな感じでした!
股関節を30度に曲げてみる
まずは、股関節を曲げてみましょう。
以下のスクリプトを作成します。
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にアタッチします。
脚が前後に開きました!
振り子のように足を揺らしてみる
せっかくなので、アニメーションさせたいですよね。
ということで、振り子のように揺らしてみます!
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);
}
いいかんじ!
膝を曲げてみる
膝も曲げてみたくなったので、 LowerLeg
を追加してみました。あと係数とかを微調整したのがこちらです。
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);
}
}
膝も曲がりました。
全身動かしてみる
同じ要領で、全身の関節の動かしてみます。色々調整したら、以下のようなコードになりました。
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);
}
}
だんだんそれっぽくなってきました。
ただ、重心が上下しないのはちょっと違和感がありますね。
重心を上下させてみた
重心も上下させてみます。
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);
}
}
ちょ、ちょっとだけ、マシになったかな?
ゲシュタルト崩壊してきたので、このあたりで止めておきます。
一応、AnimationCurveは使わずにC#だけでアニメーションを作成することができました!
AnimationClipに保存してみる
せっかくだからAnimationClipに保存してみます。
Unityの GameObjectRecorder というAPIを使えば、シーン実行中のアニメーションをAnimationClipに保存できます。
以下のスクリプトを作成し、シーン内のVRMにアタッチしてください。
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ファイルを作成します。
ここでは仮に pendulum-running
というファイル名にします。
これを RecordTransformHierarchy
の Clip
に割り当てます。
これでシーンを実行すれば、AnimationClipに動きが保管されます!
FBX Exporter等を使えば、FBXに変換することもできますね!
さいごに
C#でもAnimationClipを作成することができました!
そのことに、果たして意味があるかはわかりませんが、個人的には Quaternion
の勉強をできたのが収穫です。
本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。