LoginSignup
4
2

More than 1 year has passed since last update.

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

Posted at

この記事について

まずはデモをご覧ください。(リンク先にWebGLの動作デモがございます)
https://arihide.github.io/demos/swing_twist/

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

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

腕のボーン構造

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

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

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

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

  • 肩曲げボーン
  • 上腕ねじれボーン
  • 肘曲げボーン
  • 前腕ねじれボーン
  • 手首曲げボーン

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

candy-wrapper効果とは

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

これは高速な反面、回転する量が増えるとメッシュの体積が失われるという問題があります。
特に大きくねじったときに飴の包み紙のようになってしまうことからcandy-wrapper効果と呼ばれています。
スクリーンショット 2022-05-13 3.45.58.png

これの対処方法には

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

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

Swing-Twist分解

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

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

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

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

(1)式は曲げの後にねじりを行っているという意味なので、肩関節を表しています。
実際の分解は以下のプログラムで表されます。(詳細は参考文献をご覧ください)

// 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)式はねじりの後に曲げを行っており、手首関節を表しています。
以下の様に実装されます。

// 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分解のアルゴリズムと理論的な説明

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2