1.はじめに
VTuberというトピックはエンタメ的にも技術的にも大変ホットです。技術領域も3Dモデル作成、動画撮影/編集/配信、モーションキャプチャや顔認識などなど多岐に渡ってきました。本記事では人間と3Dモデルの動きをつなぐコア技術であるモーションキャプチャについて説明できればと思います。(全身モーションキャプチャーシステムを自作するぜ!みたいな熱い人の助けになればと思いこの記事を書きました)
なお環境はUnity2018.3+を想定しています。
1.1.今回作った成果物
- WebGLでのデモ: https://naninunenoy.github.io/Quaternion2Humanoid_Demo
- 以降の解説でも使います
- ソースコード: https://github.com/naninunenoy/Quaternion2Humanoid
- UniRx使いまくってます。見通しは悪いかも。
本題から読みたい人
前置きが長めなので、本題から読みたい方はこの辺りから読んでください。
2.モーションチャプチャ
モーションチャプチャにより現実の人間の動き(腕の動き、体の向き、首の向き、指の動きetc)をデジタル化し、解析したりバーチャルなオブジェクトに当て込んだりできるようになります。
モーションキャプチャの機材については11日目の Santarh さんが綺麗にまとめてくれています。
上記記事にもある通り、光学式と慣性センサ式に大別でき、ざっくりと以下のように言えるでしょう。
タイプ | 値段 | 正確さ | 設備 | ボーンの計算 |
---|---|---|---|---|
光学式 | 高いx | 正確o | 大掛かりx | 位置ベース |
慣性センサ式 | 光学式に比べると安いo | 光学式に比べるとズレが蓄積するx | 光学式に比べると簡便o | 角度ベース |
仕事柄、慣性センサ式のセンサに馴染みがあるので、こいつの情報からVTuber的なHumanoidの3Dモデルに動きを反映させる方法を解説します。
3.クォータニオン
3.1.概要
慣性センサ式のモーション情報はクォータニオンで取得できるのが一般的です。これは広く流通しているセンサ類(加速度センサ+ジャイロセンサ(角速度センサ)+地磁気センサ)の情報から物体の3次元の姿勢を計算する方法が確立されており、3次元での姿勢はクォータニオンで表現されることが多いためです。
クォータニオンという言葉に馴染みがない人は以下の記事がおすすめです。
説得力のある記事ですし、UnityでのQuaternion
型にも言及してくれています。
3.2.UnityEngine.Quaternion
Unityで使用されるQuaternion
型です。GameObject
の姿勢はQuaternion
型であるtransform.rotation
のプロパティからset/getできるようになっています。
ところで、transform.rotation
はUnityEditorのインスペクタ上ではVector3
型のようになっており、Roll/Pitch/Yawのオイラー角を[deg]で設定できる親切設計になっています。
Unity上で現れるクォータニオンは基本的にノルム1の単位クォータニオンにとなっており、回転を表現する情報になっています。純粋にQuaternion.x
.y
.z
.w
の値を操作するとどのような姿勢になるのかを可視化するためシーンがデモに含まれています。
ノルム1の拘束を保ったまま、4本のスライダーの値をから成るQuaternion
を計算し、真ん中のオブジェクトに反映しています。以降はこいつをQuaternion
発生装置として使い、本記事の題名の通りHumanoidの各ボーンに当てはめていきます。
クォータニオンを可視化するためのサイトもあったりします:https://quaternions.online/
4. Humanoid
4.1. Humanoidの3Dモデル
Humanoidの3Dモデルについて少し触れておきます。
AssetStoreなどから入手できる人間型の3Dモデルには大抵[Rig]にHumanoid
が設定されていると思われます。よく使われる形式だけあって、共通規格的に利用されているようです。
3Dモデル作成の段階でHumanoidとなっていれば、Humanoid用に作られたAnimationを使いまわしできたり、3Dモデルのどの部品が人間の腰や腕や指に相当するかを既知の情報として取得できたりと利点があります。
4.1.1. 構造
Humanoidの3DモデルはrootやHipを頂点としたピラミッド構造になっています。例えば手は腕に、腕は肩に属します。(さらに肩は胸に属します。)
これにより、肩を回せば腕も手も(指も)親である肩に合わせて回転する仕組みになっています。シンプルながら強力な仕組みといえます。
4.1.2. localRotaion
先ほどUnityEditorのインスペクタではtransform.rotaion
はオイラー角のVector3
で表示されると述べましたが、少し言葉足らずでした。正確には、直親のrotaion
と自分のrotation
の差分の回転を表すQuaternion
(=親オブジェクトからの相対的な姿勢)のオイラー角になっています。
なので、肩を回転させても腕のtransform.rotation
の値はインスペクタ上では変化しません。インスペクタ上に表示される相当のQuaternion
を取得したければ、transform.localRotation
から取得することができます。
-
rotation
: Unityのworld座標系からのグローバルな回転 -
localRotation
: 親オブジェクトからの(相対的な)ローカルな回転
のような使い分けがされています。この辺りの違いを知らずにQuaternion
を操ってると、自力でQuaternion
を座標変換させるコードを書いてしまったりと、なかなか混乱します。
また、オブジェクトのGlobalでの向きとLocalでの向きの表示を上のボタンから切り替えることができます。
この辺りの3Dモデルの各部位のLocalな方向の定義は3Dモデル作成者に依存するものと思われます。
4.2. 片腕編
4.2.1. 概要
上のデモプロジェクトで右上の[>>]ボタンを押すと、Humanoidの右**上腕(upperArm)と右前腕(lowerArm)**のQuaternion
を操作できるシーンが開けます。
ここでは人間の右上腕と右前腕にセンサを取り付けていると仮定し、Tポーズの状態を基準姿勢(Quaternion.identity=[1,0,0,0]
)とした現在姿勢のQuaternion
を出力しているという想定です。上腕が動くと前腕のQuaternion
もそれに合わせて変化する仕様で実装しています。(現実でも前腕の姿勢を変えず、上腕だけ動かすなんてできませんよね。)
4.2.2. 各ボーンの取得
今回お借りしたUnityちゃんの3Dモデルには元からAnimator
コンポーネントがアタッチされていました。HumanoidのモデルにアタッチされたAnimator
からはGetBoneTransform
メソッドで目的のボーンのtransform
が取得できます。以下のコードではHumanBodyBones
型をキーにした辞書に各ボーンのtransform
を保持していってます。
Dictionary<HumanBodyBones, Transform> bonesTransformDict;
void Awake() {
var humanoidAnimator = GetComponent<Animator>();
if (humanoidAnimator == null || !humanoidAnimator.isHuman) {
Debug.LogError("Humanoid Not Found.");
return;
}
bonesTransformDict = new Dictionary<HumanBodyBones, Transform>();
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) {
var bone = (HumanBodyBones)i;
bonesTransformDict.Add(bone, humanoidAnimator.GetBoneTransform(bone));
}
}
4.2.3. Quaternion
の座標変換
センサから取得されたクォータニオンを直接に
rightArm.transform.rotation = sensorQuaternion;
のように当て込んでも、腕はあらぬ方向を向くでしょう。
センサのクォータニオンがゲーム空間内のUnityちゃんの腕の姿勢に相当するQuaternion
になるように座標変換が必要です。座標変換と言っても難しいことはなく、今回は
rightArm.transform.rotation = 最初の腕の姿勢 * sensorQuaternion
としてやればokです。こうすることで、センサのクォータニオンに最初のTポーズでの腕の姿勢分だけ下駄を履かせたことになります。
注意
しかし、一般的なセンサの座標系とUnityのworld座標系はまず一致しないと思われます。Unityは鉛直上方向を+yとする座標系ですが、経験上IMUなどのセンサは鉛直上下方向いずれかを+zとするものが多い印象です。なのでもう1クッション座標変換が必要な場合がほとんどとでしょう。
このあたりは自分のセンサの仕様を理解し、センサを人間のどこにつけるかなどを把握した上で、Unityの仕様(左手座標系です!)と照らし合わせながら試行錯誤する必要があるでしょう。
4.3. 全身編
4.3.1. 概要
最後に全身です。デモプロジェクトの右上の[>>]ボタンをもう一度押すと開けます。
右上腕と右前腕だけだったのを全身(正確には腰/胸/頭/右上腕/右前腕/右手/左上腕/左前腕/左手/右腿/右脛/右足/左腿/左脛/左足)に広げました。センサを取り付けるとしたら15個になります。
4.3.2. 座標変換
片腕だけの場合は腕の初期姿勢を基準として、クォータニオンを座標変換してやるだけで事足りましたが、全身となると、各センサの基準姿勢が部位によって異なる場合が多いでしょう。15個のセンサを全て同じ向きにつけることはおそらく稀で、(Tポーズを初期姿勢として)腰/胸/頭は上向き、右上腕/右前腕/右手は右向き、左上腕/左前腕/左手は左向き、右腿/右脛/左腿/左脛は下向き、/右足/左足は前向きにセンサを取り付けるといったような具合が多いのではないでしょうか。
余談
代表的な慣性センサ式モーションキャプチャシステムであるPerception Neuronでも、センサをマニュアル通り正しく取り付け、ぴっちりTポーズをしてキャリプレーションすることを求められるのはこの基準をしっかり合わせるためと考えられます。このような基準合わせは慣性センサ式モーションキャプチャでも重要な作業です。
4.3.3. 座標変換の実装例
ここでの実装はセンサの仕様やセンサの初期姿勢などに左右されるのであくまで一例と捉えてください。
ここでは人間の腰/胸/頭/右上腕/右前腕/右手のセンサから取得されたQuaternion
を3Dモデルの各部位のtransform.rotation
に当てはめます。
なお、腰/胸/頭のセンサの基準姿勢は腰、右上腕/右前腕/右手のセンサの基準姿勢は右上腕(腰ではなく)と想定しています。
[SerializeField] Animator humanodiAnimator; // 3Dモデルをインスペクタから設定する想定
Transform humanoidHip; // 3Dモデルの腰
Transform humanoidSpine; // 3Dモデルの胸
Transform humanoidNeck; // 3Dモデルの首
Transform humanoidRightUpperArm; // 3Dモデルの右上腕
Transform humanoidRightLowerArm; // 3Dモデルの右前腕
Transform humanoidRightHand; // 3Dモデルの右手
Quaternion world2Hip; // worldから腰への回転
Quaternion world2RightArm; // worldから右腕への回転
void Start() {
// 各ボーン取得
humanoidHip = humanodiAnimator.GetBoneTransform(HumanBodyBones.Hip);
humanoidSpine = humanodiAnimator.GetBoneTransform(HumanBodyBones.Spine);
humanoidNeck = humanodiAnimator.GetBoneTransform(HumanBodyBones.Neck);
humanoidRightUpperArm = humanodiAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm);
humanoidRightLowerArm = humanodiAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm);
humanoidRightHand = humanodiAnimator.GetBoneTransform(HumanBodyBones.RightHand);
// 初期姿勢を回転として取得
world2Hip = humanoidHip.rotation;
world2RightArm = humanoidiRghtUpperArm.rotation;
}
// センサ情報が揃った時に呼ばれる想定の関数
void OnAllSensorUpdate(Quaternion[] sensorQuaternions) {
// この3つはセンサの初期姿勢が同じ
Quaternion sensorQuaternionHip = sensorQuaternions[0];
Quaternion sensorQuaternionSpine = sensorQuaternions[1];
Quaternion sensorQuaternionNeck = sensorQuaternions[2];
// この3つはセンサの初期姿勢が同じ(上3つとは異なる)
Quaternion sensorQuaternionRightUpperArm = sensorQuaternions[3];
Quaternion sensorQuaternionRightLowerArm = sensorQuaternions[4];
Quaternion sensorQuaternionRightHand = sensorQuaternions[5];
// センサ情報を3Dモデルに反映(.rotaionはworld座標)
humanoidHip.rotation = world2Hip * sensorQuaternionHip;
humanoidSpine.rotation = world2Hip * sensorQuaternionSpine;
humanoidNeck.rotation = world2Hip * sensorQuaternionNeck;
humanoidRightUpperArm.rotation = world2RightArm * sensorQuaternionRightUpperArm;
humanoidRightLowerArm.rotation = world2RightArm * sensorQuaternionRightLowerArm;
humanoidRightHand.rotation = world2RightArm * sensorQuaternionRightHand;
}
ここでも初期姿勢とQuaternion
を掛け合わせているだけであまり難しいことはありません。
しかし、この仮想コード書いてて都合良すぎるなと自分でも思いました。同じように実装して動いたとしても、残念ながら期待した方向と違った方向に腕が動くなどの現象に見舞われると思います。
注意にもあるように、自前のセンサとUnity上での仕様を擦り合わせるのが苦労するところです。
4.3.4.だからIK
なのでViveTrackerなりカメラ画像なりで手先位置を取得して、その位置から逆運動方程式で3Dモデルの各関節角度を決定する方法が広く使われているのだと思います(歩行やジャンプとかの移動もできるし)。位置から角度を求める分、回りくどい気もしますが、3Dモデルの仕様差異を受けない(FinalIKなどのアセットが吸収してくれる)ことも強みだと思います。
5. おわりに
VTuber自体は新しい領域ですが、その根幹をなすモーションキャプチャは古くからあるものです。高価なものであったモーションキャプチャがVTuberやVRのムーブメントで広く民主化されていく流れは大変好ましいものだと思います。
6. さらにおわりに
Moffでは自社開発のハードウェアであるMoffBandを使った様々な3Dモーションアプリを開発しています。モーションキャプチャ技術そのものや人間の動作解析に興味のあるエンジニアはぜひ話を聞きにきてみてください
採用ページ(wantedlyが開きます)