PyQt5でクォータニオンの回転

# はじめに

この記事は[【目次】MMDモーショントレース自動化への挑戦](https://qiita.com/miu200521358/items/d826e9d70853728abc51) の一環です。
導入方法や他の技術解説等は、上記目次から各記事を参照してください。
ほぼ自分用作業メモ。

## Github

[VMD-3d-pose-baseline-multi](https://github.com/miu200521358/VMD-3d-pose-baseline-multi)

## 元ネタ

このソースコードは、[VMD-Lifting](https://github.com/errno-mmd/VMD-Lifting) を改変しています。
3D関節データからVMDフォーマットへの変換処理は、[VMD-Lifting がやっていること - Qiita](https://qiita.com/errno_mmd/items/44a7aedfeb853307e443) を参照してください。

# 課題

生成したVMDファイルをMMDで読み込むと、やや前傾した姿勢になること。

# アプローチ

 - `VMD-Lifting` の出力結果と比較したところ、同じ動作でもモーションがかなり異なる事が判明

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">左：VMD-Lifting (未正規化)<br>　　処理時間：4時間程度<br><br>右：Openpose + 3d-pose-baseline (正規化済)<br>　　処理時間：15分程度(事前学習3日)<br><br>正規化しないとこんなに暴れるのか ^^;<br>VMD-Lifting を正規化して使うか、3d-pose-baseline の姿勢を修正するか…<br>どっちにしましょうかね。 <a href="https://t.co/qEiEIYD1jP">pic.twitter.com/qEiEIYD1jP</a></p>&mdash; miu (@miu200521358) <a href="https://twitter.com/miu200521358/status/993261916314206208?ref_src=twsrc%5Etfw">2018年5月6日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

 - `VMD-Lifting` は前傾していない事から、[miu200521358/3d-pose-baseline-vmd](https://github.com/miu200521358/3d-pose-baseline-vmd) で生成された3D関節データが既に前傾しているものと想定
 - 野球の投球動画などから、モーションを生成したところ、ローカルX軸(上半身等のX軸方向の傾き)ではく、グローバルX軸(MMD上で表示される座標X軸)方向に17度ほど傾いている事が判明

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">出かける直前にやってた、レンくんの投球モーション。<br>こうして見ると、身体の前傾というより、グローバルX軸に対して前傾してる（背面だと反り気味）なんだなー、ということが判明。<br>後、足がちょっと浮いてたり回転しちゃったりもするし、どうやって調整しようかねぇ <a href="https://t.co/pclvmg2ZOO">pic.twitter.com/pclvmg2ZOO</a></p>&mdash; miu (@miu200521358) <a href="https://twitter.com/miu200521358/status/993132868372082693?ref_src=twsrc%5Etfw">2018年5月6日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 解決策

## 1. 補正用クォータニオンの生成

グローバルX軸方向に指定角度(デフォルトで17度)傾けた補正用クォータニオンをまず生成する。

![飛行機軸回転2.png](https://qiita-image-store.s3.amazonaws.com/0/254327/b6d9ee4c-bfdf-8e36-deb6-9a1bd312adbd.png)

飛行機イラスト) [GATAG](http://01.gatag.net/0009532-free-illustraition/)

MMDにおいては、

 - ピッチ = グローバルX軸
 - ヨー = グローバルY軸
 - ロール = グローバルZ軸

```
# 補正角度のクォータニオン
# 3次元の角度指定による回転から、クォータニオンを求める
correctqq = QQuaternion.fromEulerAngles(QVector3D(xangle, 0, 0))
```

> QQuaternion QQuaternion::fromEulerAngles(float pitch, float yaw, float roll)
>
> Creates a quaternion that corresponds to a rotation of roll degrees around the z axis, pitch degrees around the x axis, and yaw degrees around the y axis (in that order).

引用元）[QQuaternion Class | Qt GUI 5.10](http://doc.qt.io/qt-5/qquaternion.html#fromEulerAngles-1)

## 2. 角度の結合

```
# 上半身
direction = pos[8] - pos[7]
up = QVector3D.crossProduct(direction, (pos[14] - pos[11])).normalized()
upper_body_orientation = QQuaternion.fromDirection(direction, up)
initial = QQuaternion.fromDirection(QVector3D(0, 1, 0), QVector3D(0, 0, 1))
# 補正をかけて回転する
bf.rotation = correctqq * upper_body_orientation * initial.inverted()
```

`upper_body_orientation ` …　上半身角度クォータニオン
`initial` …　初期状態からの捻り情報クォータニオン

1. まず、グローバルX軸に補正角度分回転させる
2. 補正角度分回した後、上半身の角度分回転させる
3. 初期状態からの捻り角度分回転させる

順番を間違えると正しく補正されませんでした。

## 2. 角度の結合(末端の場合)

腕（←上半身）、膝（←下半身）等、親ボーンの回転が影響する場合、自分自身の回転にまず角度補正を行い、その後、親ボーンの回転を差し引く。

```
# 左腕
direction = pos[12] - pos[11]
up = QVector3D.crossProduct((pos[12] - pos[11]), (pos[13] - pos[12]))
orientation = QQuaternion.fromDirection(direction, up)
initial_orientation = QQuaternion.fromDirection(QVector3D(1.73, -1, 0), QVector3D(1, 1.73, 0))
rotation = correctqq * orientation * initial_orientation.inverted()
# 左腕ポーンの回転から親ボーンの回転を差し引いてbf.rotationに格納する。
# upper_body_rotation * bf.rotation = rotation なので、
bf.rotation = upper_body_rotation.inverted() * rotation
```

# 結果

[![MMDモーショントレース自動化への挑戦【ver0.02】](http://img.youtube.com/vi/-xJbc6B3aUc/0.jpg)](https://youtu.be/-xJbc6B3aUc)

 - 前傾は概ね直った
 - デフォルト値で概ね修正可能だが、動画によっては微調整した方がいいものもある
 - シーンによっては後傾しちゃってる箇所も…

# 次回予定

とりあえずフレームの間引き。
同一円周上の三点の移動（関節の回転）が、一定角度差以内であれば、間引く…予定。
＃これをクォータニオンでどう表現するのかが分かりませんｗ