角度制限付きIKの実装 3D編(改)

前回の実装では問題が山積みだったので改良してみた。

この記事ではMMDに限らず、一般的な角度制限付きIKを実装方法を書いた。
一応、MMDのIKとして実装したので、MMDのIKの仕様などは別記事にて詳細を書く。

この記事に書いていることは、あくまで実装の一手法です。
効率が良くなかったり、バグがあるかもしれません。自分の頭ではこれが限界でした。

前提
今回はMMDの仕様に沿った実装。
なので、仕様としては以下の通り。
・アルゴリズムはCCD-IK
・角度制限はオイラー角


1.IKの理屈
2d編の記事を参照せよ。
ああいう感じが基本になる。
ただし、3dだからややこしくなるよ。

2.準備
まずIKのおさらいから。

普通、IKには連動するボーンがある。
例えば、図1のように、左足首がIKボーンで、左足と左ひざが従属(リンク)しているとする

ik211.png図1 リンクボーンの例

このとき、左足首を好きな位置に動かすと、図2のように左足と左膝が「いい感じの回転」をしてくれる。
ik212.png図2 IKボーンについていくリンクボーン
今回は、こういった「いい感じの回転」を求めることが必要になる。
つまり、左足、左膝なんかのリンクボーンの回転(クォータニオン)を正しく求められれば目標達成となる。

これを実現するためには、IKの計算に必要な情報を用意しなければならない
具体的には、1.ターゲット、2.リンクボーン配列、3.ワールド->ローカル変換クォータニオン
の3つが必要になる。


2.1.ターゲットについて
ターゲットはIKの目標点、図2でいう左足首ボーンの位置。
ちなみに、基本的にターゲットはワールド座標での位置で、計算中に適宜ローカル座標に変換していくことになる。
ただし、MMDの場合はターゲットの設定に癖があるので、別記事を参照せよ。

2.2.リンクボーン配列
これはIKの計算のために必要な情報を格納した配列(連想配列)。
多分、実装者によって必要な情報は異なると思うので、以下は自分なりに必要だと考えたものです。

・limit/ボーンの回転に制限があるか
・top_limit/角度上限(ラジアン)
・bottom_limit/角度下限(ラジアン)
・rot_local/ローカル回転(クォータニオン)
・pos/ボーン開始位置(ワールド座標)
・end/ボーン終端位置(ワールド座標)
・end_vector/ボーンの長さ
・to_local_rot/ワールド空間->ローカル空間にするためのクォータニオン
・to_local_trans/ワールド空間->ローカル空間にするためのベクトル
・name/ボーン名

例えば以下のような値になる。
リンクボーン[0] = {
 limit:true,
 top_limit:[3.14,0,0],
 bottom_limit:[0,0,0],
 rot_local:[0,0,0,1], //回転なし
 pos:[0.06,0.42,0.02],
 end:[0.06,0.12,0],
 end_vector:[0,-0.3,0], //end-posで求められる
 to_local_rot:[0,0,0,1], //後で初期化するので何でもOK
 to_local_trans:[0,0,0], //後で初期化するので何でもOK
 name:左足
}
リンクボーン[1] = {
 limit:false,
 top_limit:[0,0,0],
 bottom_limit:[0,0,0],
 rot_local:[0,0,0,1], //回転なし
 pos:[0.06,0.76,0],
 end:[0.06,0.42,0.02],
 end_vector:[0,-0.34,0.02], //end-posで求められる
 to_local_rot:[0,0,0,1], //後で初期化するので何でもOK
 to_local_trans:[0,0,0], //後で初期化するので何でもOK
 name:左足
}


2.3.ワールド->ローカルクォータニオン
ワールド座標系をローカル座標系にするためのクォータニオン。
テキストでは解説が困難なので図で説明。
ik23.png


仮にFKで下半身ボーンがz軸で少し回転していた場合、図のワールド空間みたいなボーン配置になる。

このような場合、ワールド空間を左足のローカル空間(図中のローカル空間(位置、回転))に変換するためには、
まず左足ボーンの開始位置を原点に移動し、さらに回転する必要がある。

処理の都合上、図中のローカル変換ベクトルは不要なのだが、図中のローカル変換クォータニオンは必須。


作り方
ルートボーンの親を辿っていくと作れます。
IK処理とは直接関係ないので省略しますが、知りたい方は以下の参考サイトを見るか、この記事最後のソースコードを見てください。


3.IK処理

ここからが本題。長い!!!!

IK処理の流れとしては、

for(ループ制限回数まで){ // いわゆる試行回数。増やすと精度上がります
 for(リンクボーンの数){
  ボーン情報の更新(3.1)
  終了判定(3.2)
  各点をローカルに直す(3.3)
  理想回転を作る(3.4)
  残存回転の反映(3.5)
  制限処理と実際回転の作成(3.6)
  制限時の残存回転の作成(3.7)
  実際回転の反映(3.8)
 }
}

って感じ。
角度制限が無いIKなら3.1~3.4で済んだのに制限があると処理が増える。
では1つずつ解説します。

3.1.ボーン情報の更新

この処理では以下の2つを行います。

1.ボーンのendの作成
 終了判定(3.2)で使います
2.ワールド->ローカル変換クォータニオン&ベクトルの作成
 各点をローカルに直す(3.3)で使います

1と2を別個にやると面倒くさいので両方一気に作ります
処理の流れはこんな感じ

for(リンクボーンの数){ // 必ずルートに近い方から処理する。(例:一回目:左足、二回目:左膝)
 posの作成(3.1.1)
 ローカル変換の作成(3.1.2)
 endの作成(3.1.3)
 変換回転の更新(3.1.4)
}

3.1.1.posの作成
posはボーンの開始位置です。
ルートボーンだったら更新する必要はないですね。
ルートボーンでないなら、親のend=現在ボーンのposなので、更新してください。

3.1.2ローカル変換の作成
ワールド空間をローカル空間にするためのベクトルとクォータニオンを作ります。

ik23.png

ローカル変換ベクトルは図のように、ワールドでの座標(pos)->原点へ動かすようなベクトルです。
これは先程作ったposから簡単につくれます。

ローカル変換クォータニオンは正しいローカル空間に変換するために必要です。
視覚的に考えると図のようになっています。

まず、ワールド空間からローカル変換ベクトルでローカル空間(位置のみ)を作ります、
次にローカル変換クォータニオンで位置と回転が正しいローカル空間にします。

このクォータニオンは2.3で作ったワールド->ローカルクォータニオンのことなので簡単ですね。

3.1.3.endの作成
さて、ここからがややこしい。ik313.png

end_vectorにローカル変換クォータニオンをかけることで、ローカル空間でのボーンの終端位置が求まります。図中のend(ローカル)です。

次に、ローカル空間でのend_vectorにrot_localを掛けます。これでローカル空間で回転したend_vectorとなります。
rot_localの初期値では何の回転もなされないため、仮にrot_localに”z軸で-90度回転”というクォータニオンが入っているとしましょう。そうすると図中ではend(ローカル回転済み)になります。
そして、次はローカル座標をワールド座標にする必要があります。
これはend_vectorに対し、ローカル変換クォータニオンの共役をかけた後、ローカル変換ベクトルの逆ベクトルをかけることで、ローカル->ワールドへ変換できます。図中ではend(ワールド)へ変換ができました。


3.1.4変換回転の更新
さて、一回目のループでは左足に関してendを作りましたが、左ひざは左足のローカル回転が加わりますので、左足の回転を考慮したローカル->ワールド変換をしなければなりません。
これはワールド->ローカルクォータニオンに左足のローカル回転の共役クォータニオンをかけるだけでokです。
そうすることで、例えば、ワールド->ローカルクォータニオンがz軸-30度回転としましょう。
左足のrot_localが-90度なら共役をとるとz軸90度回転となり、この2つのクォータニオンをかけるとz軸60度の回転となります。
この回転を使うことでワールド空間->左膝ローカル空間へ変換することができます。

3.2.終了判定
ターゲットと、先端ボーンのendの距離を二点間の距離の公式を使って測るだけです。
十分に近い場合は終了、という感じの処理を書きましょう。

3.3.各点をローカルに直す
3.1で面倒くさかった分、超簡単です。
ターゲット、現在ボーンのpos、先端ボーンのendに対してto_local_rotとto_local_transを順番に掛けるだけでローカル空間に直せます。

3.4.理想回転を作る
現在ボーンのposから、ターゲット、先端ボーンendに対するベクトル(bone_target,bone_tip)を作る
次に作成した2つのベクトルの外積と内積を作成し、外積と内積から図のような回転を行うクォータニオン(理想回転)を作成する
※図はyz平面に変わったことに注意!
ik341.png
3.5.残存回転の反映
※この処理は初回(つまり先端ボーン)の時には不要です。また、先に3.6と3.7を読んだほうが分かりやすいです。
残存回転がある場合のみ、この処理を行ってください。
3.7で作成した残存回転を理想回転に反映します。反映といっても掛けるだけです。
こうすることで、理想回転 = 理想回転+残存回転となり下図のような回転となります。

ik35.png
3.6.制限処理と実際回転の作成
ボーンには回転制限があります。
例えば、左足には制限が全くないですが、左ひざにはx軸が0~3.14radの制限があり、yz軸は全く回転ができません。
ik36.png
図のような理想回転はx軸で回ろうとしているので、左膝では制限違反になることは明らかです。
そこで、理想回転から「制限された上で、実際に行う回転」を作ります。

やり方としては、理想回転のxyz成分に対し、ボーンの各軸が持っているオイラー角からクォータニオンの成分を作成、制限内かどうかを判定することで行います。

例えばx軸だとlimit_bottom=sin(0rad*0.5)=0, limit_top=sin(3.14*0.5)=1となります。
次に、理想回転の成分と比較します。
図の場合だと、x軸で-100度くらいの回転でしょうか。これからクォータニオンを作ると、
Q = [x=1*sin(1.74/2), y=0*sin(1.74/2), z=0*sin(1.74/2), w=cos(1.74/2)]
 = [x=-0.764, y=0, z=0, w=0.645]
つまり制限は0~1で、Qのxは-0.764なので制限に引っかかりましたね。
制限に引っかかった場合は近い方の数値、0へ丸めます。
結果的に理想回転[-0.764,0,0,0.645]は[0,0,0,0.645]となります。この修正したクォータニオンを実際回転と呼ぶことにします。
さらに、実際回転は正規化されていなくて扱いづらいので正規化してください。そうすると[0,0,0,1]となります。


3.7.制限時の残存回転の作成
次に、理想回転から実際に回転できた量を引くことで、現在のボーンで残ってしまった回転量(残存回転)を作成します。
残存回転は単純に理想回転を実際回転で割ればOKです。
クォータニオンを割るという馴染みが無いことをしますが、やっていることは「理想回転に実際回転の共役を掛ける」ということと全く同じです。
これによって、図のような残存回転を作成できました。今回は理想回転と残存回転が全く同じですね…
ik362.png
ただし、ここで一つ注意が必要で、「全く回転できない」軸は0にしておいてください
例えば、下図のようなxy平面で、理想回転にz軸が含まれている場合です。
ik37.png
このような場合、残存回転は理想回転に一致しますが、完全に制限されている軸(y軸とz軸)は残存回転のyz成分を0にしてしまって残存回転に影響させないでください。
また、成分を書き換えると再び正規化が必要になるので注意してください。
これが必要な理由は4.xで書いています

3.8.実際回転の反映
作成した実際回転をrot_localに反映します。
左膝のときは実際回転が[0,0,0,1]、つまり回転なしなので省略しますが、
左足のときは以下のような回転が行われます。
ik38.png

以上のことを繰り返していけば制限を考慮したIK処理が可能となります。

4.その他
こんなところです。
他に気をつけるべきことは4.xに書きます。
あと、MMDでのIKは基本的に記述した方法で問題ないのですが、一応別記事として書きました。
多分MMDにかぎらずに様々な形式で対応できるアルゴリズムではないかと思います。

4.1.ソースコードについて
ソースコード:bone.js
IK以外の処理も含んでいるけど…
IK2FK関数で2.1~2.3の処理をやって、calcBonePositionは3.1、calcIKで3.2~3.8の処理をやっている感じ。
ただし、IK2FKはMMD互換にするための処理を含んでいるため見づらい。
あと、行列計算用のライブラリとしてminMatrixb.js ( https://wgld.org/d/library/l002.html )を使っています。

4.x
3.7で取り扱った「完全に制限されている軸は残存回転の成分を0にする」ですが、これをやらないと図のようなことが発生します。
まず、以下のような回転が左足ボーンのときに行われます。
ik37x1.png

次のループの時も似たような制限が掛かり、結果として以下のような回転が行われます。
ik37x2.png

最初と同じようなところへ移動した………
つまり、収束しなくなり「振動」が発生します。
だから「動けない軸」は引き継がないようにしないと収束しないのです。
もしかしたら別の解法があるかもしれませんが、この方法で上手くいってるので使っています。


5.参考サイト
非常に参考になるサイト。全部日本語なので理解しやすいよ!

WebGLを使ってブラウザ上で3Dモデルを描画した話:http://labotech.dmm.com/entry/2016/06/30/122403
 IKのアルゴリズムが非常に参考になった。GitHubでのソースコードが公開されていなければ、この記事は書けなかったレベル。

WebGL 開発支援サイト wgld.org:https://wgld.org/
 IKに限らずいろんな意味でお世話になった。今回はクォータニオンの解説記事を特に参考にした。

「ゲームつくろー!」より「その16 オブジェクトを任意の平面に立たせる姿勢制御」:http://marupeke296.com/DXG_No16_AttitudeControl.html
 URL先の記事以外でも大体役に立つサイト。めちゃくちゃ参考になる。ただし、DirectXを扱っていることが多く、WebGLとは座標系が違うことに注意。

この記事へのコメント