three.js 曲げとねじれにボーンを分けて腕のスキニングを綺麗にする

# この記事について
まずはデモをご覧ください。（リンク先にWebGLの動作デモがございます）
https://arihide.github.io/demos/swing_twist/
[<img width=600 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323576/103ae172-83f8-9c38-7b3b-e85d533e89fd.png />](https://arihide.github.io/demos/swing_twist/)

デモを開いてギズモを操作すると、自然に腕の曲げ・ねじれが表示されているのがわかります。
本記事ではこのモデルの構造について説明します。

ソースコードはこちら
https://github.com/Arihide/demos/tree/master/swing_twist

# 腕のボーン構造
モデルのボーン構造がどの様になっているのかをまずみていきましょう。

現実世界において、人間の腕は肩-肘-手首の３関節とみなせます。
しかし、腕を３つのボーンで表すのは、現代的なキャラクターでは行われません。

この理由は**スキニング**において問題が発生するためです。
**スキニング**とはボーンが回転したときメッシュの頂点がどのように移動するかを定めることです。

手首には上下左右に**曲げる動作**と、腕の方向に**ねじる動作**がありますが、実際に動かしてみると
曲げる場合は関節部分の皮膚だけが動くのに対して、ねじる場合前腕全体の皮膚が移動するのがわかります。
したがって１つの関節であっても、以下の様に曲げとねじれでボーンを分ける必要があるのです。
* 肩曲げボーン
* 上腕ねじれボーン
* 肘曲げボーン
* 前腕ねじれボーン
* 手首曲げボーン

これは正しいのですが、上腕と前腕のねじれボーンは１本ずつではなく、それぞれ３本ほど追加するのが一般的です。
これはゲームなどでよく用いられる**線形スキニング**では、
ねじる動作において**candy-wrapper効果**という問題があるためです。

#### candy-wrapper効果とは
最も有名なスキニング手法に**線形スキニング**というものがあります。
これは極めて単純で、ボーンの回転量に頂点ウェイトをかけた分だけメッシュ頂点を移動させる手法です。

これは高速な反面、**回転する量が増えるとメッシュの体積が失われる**という問題があります。
特に大きくねじったときに飴の包み紙のようになってしまうことからcandy-wrapper効果と呼ばれています。
![スクリーンショット 2022-05-13 3.45.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323576/5c9c8b32-1a50-0965-6ac0-1b6e2f715db8.png)


これの対処方法には
1. デュアルクォータニオンなどの別のスキニング手法を利用する
2. ボーンを増やして１本あたりの回転量を減らす

という方法がありますが、ゲームなどでは計算量の観点から2.がよく利用されます。
今回作成したデモも2.によって実現しています。

# Swing-Twist分解
前項では、**曲げとねじりにボーンを分割することで綺麗にスキニングさせる**説明をしました。
しかし作業時にこれら1つずつに回転角を指定していくのはとても大変です。

そこで**角度の指定時には肩-肘-手首の３関節とみなし、後から曲げとねじれに分割する**ことを考えます。
本項では関節の回転をどのように分割するのかを説明します。

関節の回転を表すクォータニオンを $q$ とします。クォータニオンは可換則が成り立ちませんから2通りの分割方法があります。

```math
\begin{eqnarray}
q = q_{twist}q_{swing} \tag{1} \\
q = q_{swing}q_{twist} \tag{2} \\
\end{eqnarray}
```

**(1)式は曲げの後にねじり**を行っているという意味なので、肩関節を表しています。
実際の分解は以下のプログラムで表されます。（詳細は参考文献をご覧ください）
```javascript
// quatをtwistAxis周りの回転(twistOut)とそうでない回転(swingOut)に分ける。
// ただし、quat = twistOut * swingOut
function decomposeTwistAfterSwing(quat, twistAxis, swingOut, twistOut) {
    let w = twistAxis.clone().applyQuaternion(quat)

    swingOut.setFromUnitVectors(twistAxis, w)
    twistOut.copy(swingOut).invert().premultiply(quat)
}
```

一方、**(2)式はねじりの後に曲げ**を行っており、手首関節を表しています。
以下の様に実装されます。
```javascript
// quatをtwistAxis周りの回転(twistOut)とそうでない回転(swingOut)に分ける。
// ただし、quat = swingOut * twistOut
function decomposeSwingAfterTwist(quat, twistAxis, swingOut, twistOut) {
    let u = quat.x * twistAxis.x + quat.y * twistAxis.y + quat.z * twistAxis.z
    let l = Math.sqrt(quat.w * quat.w + u * u)

    if (l < 0.000001)
        twistOut.set(0, 0, 0, 1)
    else
        twistOut.set(twistAxis.x * u / l, twistAxis.y * u / l, twistAxis.z * u / l, quat.w / l)

    swingOut.copy(twistOut).invert().premultiply(quat)
}
```
さらに、ねじりボーンを複数用意している場合は $q_{twist}$をオイラー角に変換しボーンの数で割ってここのボーンに割り当てます。
以上により回転の分解を実現することができます。

# おわりに
本記事では、**線形スキニングにおける腕のボーン構造**と**そのボーン構造への角度の設定**について説明しました。

ただ課題もあり、デモを見ていただいた方はお気づきかもですが、180°以上ねじるとねじりの向きが変わってしまいます。
これはクォータニオンが-180°~180°までしか保持できないことに起因しています。

途中にオイラー角への変換処理を挟み込むことで180°以上のねじれにも対応可能ですが、本稿ではここまでにします。

### 参考
Swing Twist分解のアルゴリズムと理論的な説明

https://arxiv.org/abs/1506.05481

モデルは下記のものを改変して使用いたしました。

https://www.blendswap.com/blend/22596