Humanoidの3DモデルをQuaternionで直接動かす方法

# 1.はじめに
VTuberというトピックはエンタメ的にも技術的にも大変ホットです。技術領域も3Dモデル作成、動画撮影/編集/配信、モーションキャプチャや顔認識などなど多岐に渡ってきました。本記事では人間と3Dモデルの動きをつなぐコア技術であるモーションキャプチャについて説明できればと思います。(全身モーションキャプチャーシステムを自作するぜ:fire:！みたいな熱い人の助けになればと思いこの記事を書きました)

なお環境はUnity2018.3+を想定しています。

## 1.1.今回作った成果物
 * WebGLでのデモ: https://naninunenoy.github.io/Quaternion2Humanoid_Demo
   * 以降の解説でも使います 
 * ソースコード: https://github.com/naninunenoy/Quaternion2Humanoid
   * UniRx使いまくってます。見通しは悪いかも。 

## 本題から読みたい人
前置きが長めなので、本題から読みたい方は[この辺り](#42-片腕編)から読んでください。

# 2.モーションチャプチャ
モーションチャプチャにより現実の人間の動き(腕の動き、体の向き、首の向き、指の動きetc)をデジタル化し、解析したりバーチャルなオブジェクトに当て込んだりできるようになります。

モーションキャプチャの機材については11日目の Santarh さんが綺麗にまとめてくれています。

* [フルボディトラッキングのための機材比較](https://qiita.com/Santarh/items/de41ca5662f7e3520091)

上記記事にもある通り、**光学式**と**慣性センサ式**に大別でき、ざっくりと以下のように言えるでしょう。

|タイプ|値段|正確さ|設備|ボーンの計算|
|:--|:--|:--|:--|:--|
| 光学式 | 高い<font color="red">x</font color> | 正確<font color="green">o</font color> |大掛かり<font color="red">x</font color>|位置ベース|
| 慣性センサ式 | 光学式に比べると安い<font color="green">o</font color> | 光学式に比べるとズレが蓄積する<font color="red">x</font color> | 光学式に比べると簡便<font color="green">o</font color> |角度ベース|

仕事柄、慣性センサ式のセンサに馴染みがあるので、こいつの情報からVTuber的なHumanoidの3Dモデルに動きを反映させる方法を解説します。

# 3.クォータニオン
## 3.1.概要
慣性センサ式のモーション情報は**クォータニオン**で取得できるのが一般的です。これは広く流通しているセンサ類(加速度センサ＋ジャイロセンサ(角速度センサ)＋地磁気センサ)の情報から物体の3次元の姿勢を計算する方法が確立されており、3次元での姿勢はクォータニオンで表現されることが多いためです。

クォータニオンという言葉に馴染みがない人は以下の記事がおすすめです。

 * [クォータニオン (Quaternion) を総整理！ ～ 三次元物体の回転と姿勢を鮮やかに扱う ～](https://qiita.com/drken/items/0639cf34cce14e8d58a5)

説得力のある記事ですし、Unityでの`Quaternion`型にも言及してくれています。

## 3.2.`UnityEngine.Quaternion`
Unityで使用される`Quaternion`型です。`GameObject`の姿勢は`Quaternion`型である`transform.rotation`のプロパティからset/getできるようになっています。
ところで、`transform.rotation`はUnityEditorのインスペクタ上では`Vector3`型のようになっており、Roll/Pitch/Yawのオイラー角を[deg]で設定できる親切設計になっています。
![スクリーンショット 2018-12-11 19.38.18.png](https://qiita-image-store.s3.amazonaws.com/0/274974/482d2778-27ba-ce49-ee14-f7059f460966.png)

Unity上で現れるクォータニオンは基本的にノルム1の単位クォータニオンにとなっており、回転を表現する情報になっています。純粋に`Quaternion.x` `.y` `.z` `.w`の値を操作するとどのような姿勢になるのかを可視化するためシーンが[デモ](https://naninunenoy.github.io/Quaternion2Humanoid_Demo)に含まれています。
![quat1.gif](https://qiita-image-store.s3.amazonaws.com/0/274974/b20a6270-f051-2461-7bf9-eaf6978ad3fe.gif)
ノルム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モデルのどの部品が人間の腰や腕や指に相当するかを既知の情報として取得できたりと利点があります。
![スクリーンショット 2018-12-11 20.43.11.png](https://qiita-image-store.s3.amazonaws.com/0/274974/b08ebfa1-ffc2-34f4-c82c-2547aab9e2ba.png)

### 4.1.1. 構造
Humanoidの3DモデルはrootやHipを頂点としたピラミッド構造になっています。例えば手は腕に、腕は肩に属します。(さらに肩は胸に属します。)
![スクリーンショット 2018-12-11 20.50.27.png](https://qiita-image-store.s3.amazonaws.com/0/274974/73583652-2a6c-bee7-8d1d-ecb65146b263.png)

これにより、肩を回せば腕も手も(指も)親である肩に合わせて回転する仕組みになっています。シンプルながら強力な仕組みといえます。

### 4.1.2. `localRotaion`
先ほどUnityEditorのインスペクタでは`transform.rotaion`はオイラー角の`Vector3`で表示されると述べましたが、少し言葉足らずでした。正確には、**直親の`rotaion`と自分の`rotation`の差分の回転を表す`Quaternion`**(=親オブジェクトからの相対的な姿勢)のオイラー角になっています。

なので、肩を回転させても腕の`transform.rotation`の値はインスペクタ上では変化しません。インスペクタ上に表示される相当の`Quaternion`を取得したければ、`transform.localRotation`から取得することができます。

 * `rotation` : Unityのworld座標系からのグローバルな回転
 * `localRotation` : 親オブジェクトからの(相対的な)ローカルな回転

のような使い分けがされています。この辺りの違いを知らずに`Quaternion`を操ってると、自力で`Quaternion`を座標変換させるコードを書いてしまったりと、なかなか混乱します。
また、オブジェクトのGlobalでの向きとLocalでの向きの表示を上のボタンから切り替えることができます。
![スクリーンショット 2018-12-11 21.15.22.png](https://qiita-image-store.s3.amazonaws.com/0/274974/c2e1723b-8a2c-0a19-f970-d6d42ec19d13.png) ![スクリーンショット 2018-12-11 21.15.33.png](https://qiita-image-store.s3.amazonaws.com/0/274974/ff512994-f973-601e-dca3-2aab7aa79106.png)

この辺りの3Dモデルの各部位のLocalな方向の定義は3Dモデル作成者に依存するものと思われます。

## 4.2. 片腕編
### 4.2.1. 概要
上の[デモプロジェクト](https://naninunenoy.github.io/Quaternion2Humanoid_Demo)で右上の[>>]ボタンを押すと、Humanoidの右**上腕(upperArm)**と右**前腕(lowerArm)**の`Quaternion`を操作できるシーンが開けます。
![right.gif](https://qiita-image-store.s3.amazonaws.com/0/274974/f075d248-b768-8b2b-f5b7-5423a30ea457.gif)
ここでは人間の右上腕と右前腕にセンサを取り付けていると仮定し、Tポーズの状態を基準姿勢(`Quaternion.identity=[1,0,0,0]`)とした現在姿勢の`Quaternion`を出力しているという想定です。上腕が動くと前腕の`Quaternion`もそれに合わせて変化する仕様で実装しています。(現実でも前腕の姿勢を変えず、上腕だけ動かすなんてできませんよね。)

### 4.2.2. 各ボーンの取得
今回お借りしたUnityちゃんの3Dモデルには元から`Animator`コンポーネントがアタッチされていました。Humanoidのモデルにアタッチされた`Animator`からは`GetBoneTransform`メソッドで目的のボーンの`transform`が取得できます。以下のコードでは`HumanBodyBones`型をキーにした辞書に各ボーンの`transform`を保持していってます。

```C#
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`の座標変換
センサから取得されたクォータニオンを直接に

```c#
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. 概要
最後に全身です。[デモプロジェクト](https://naninunenoy.github.io/Quaternion2Humanoid_Demo)の右上の[>>]ボタンをもう一度押すと開けます。
右上腕と右前腕だけだったのを全身(正確には腰/胸/頭/右上腕/右前腕/右手/左上腕/左前腕/左手/右腿/右脛/右足/左腿/左脛/左足)に広げました。センサを取り付けるとしたら15個になります。
![body2.gif](https://qiita-image-store.s3.amazonaws.com/0/274974/7d91df0b-3589-648f-684a-9b7ece409b70.gif)

### 4.3.2. 座標変換
片腕だけの場合は腕の初期姿勢を基準として、クォータニオンを座標変換してやるだけで事足りましたが、全身となると、各センサの基準姿勢が部位によって異なる場合が多いでしょう。15個のセンサを全て同じ向きにつけることはおそらく稀で、(Tポーズを初期姿勢として)腰/胸/頭は上向き、右上腕/右前腕/右手は右向き、左上腕/左前腕/左手は左向き、右腿/右脛/左腿/左脛は下向き、/右足/左足は前向きにセンサを取り付けるといったような具合が多いのではないでしょうか。

### 余談
代表的な慣性センサ式モーションキャプチャシステムである**Perception Neuron**でも、センサをマニュアル通り正しく取り付け、ぴっちりTポーズをしてキャリプレーションすることを求められるのはこの基準をしっかり合わせるためと考えられます。このような基準合わせは慣性センサ式モーションキャプチャでも重要な作業です。

### 4.3.3. 座標変換の実装例
ここでの実装はセンサの仕様やセンサの初期姿勢などに左右されるのであくまで一例と捉えてください。
ここでは人間の腰/胸/頭/右上腕/右前腕/右手のセンサから取得された`Quaternion`を3Dモデルの各部位の`transform.rotation`に当てはめます。
なお、腰/胸/頭のセンサの基準姿勢は**腰**、右上腕/右前腕/右手のセンサの基準姿勢は**右上腕**(腰ではなく)と想定しています。

```c#
[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モーションアプリを開発しています。モーションキャプチャ技術そのものや人間の動作解析に興味のあるエンジニアはぜひ話を聞きにきてみてください:muscle: 
[採用ページ(wantedlyが開きます)](https://www.wantedly.com/projects/240435)