MMD on WebGL ボーンモーションを実装し始めた
とりあえず Forward Kinematics (IK ではない普通のボーン)はできた。
10月26日現在、開くと変な盆踊りをしてくれる。Chrome 専用。Firefox は(少なくとも Windows では)シェーダーにバグがあってちゃんと表示されない。Mac だと大丈夫だった。
Forward Kinematics
PMD には各ボーンの「元の位置」が記録され、VMD にはキーフレームごとの各ボーンの「回転」と「平行移動」が記録されている。
これらから各ボーンの「移動後の位置」と「移動後の回転」を計算する。なんで計算する必要があるかというと、VMD の回転と平行移動は、親ボーンからの相対的な移動でしかないので、座標系を揃えるため。
PMD のボーン配列は、一番最初が「センター」で、以降は親から子の順番で出てくる(あるボーンの親がそのボーンより後に来ることはない)ので、最初から順に計算していけばいい。
まず最初のセンターについては、
移動後の位置 = 元の位置 + 平行移動 移動後の回転 = 回転
と安直に求められ、以降は次のようになる。
移動後の位置 = 親の回転 * (元の位置 - 親の元の位置 + 平行移動) + 親の移動後の位置 移動後の回転 = 親の回転 * 回転
回転のところを単に * にしたけど、計算するときはクォータニオンの定義に従うこと。(別に行列に変換してもいいが、あとに書くようにメモリが厳しいのでクォータニオンのままのほうがいいと思う)
Inverse Kinematics (IK) はこれとは逆に、子から親に向かってボーンを動かすので IK と呼ぶ(らしい)。これはまだ実装してない。
スキニング
モデルの各頂点をボーンに沿って動かすことをスキニングという(らしい)。
上のようにして求めた移動を各頂点についても施すのだが、頂点の数は膨大にあるので、JS でループを回してたら遅い。なので、各ボーンの「元の位置」と「移動後の位置」と「移動後の回転」を vertex shader に渡し、シェーダーでやることにする。
uniform vec3 uBonePosOriginal[64]; uniform vec3 uBonePosMoved[64]; uniform vec4 uBoneRotations[64]; // quaternion
ここで問題なのが、ボーンをいくつ渡すかということ。上だと64個までに決め打ちのだが、もうちょっと増やすとメモリが確保できなくてシェーダーのリンクが失敗する。うちの環境だと64個は大丈夫だったけど、もしかするともっと貧弱な環境だとこれでもダメかもしれない。
ところが、これではボーンが65個以上のモデルは表示できない。ってなことが↓このへんに書いてあった。
材質ごとにボーン番号を振り直してやることで、とりあえず1材質64個までのボーンのモデルについては表示できる。(それ以上の場合については、対策してみたけどバグってたので保留)
そのあたりのコードをペタリ。
// ... uniform vec3 uBonePosOriginal[64]; uniform vec3 uBonePosMoved[64]; uniform vec4 uBoneRotations[64]; // quaternion attribute float aBone1; attribute float aBone2; attribute float aBoneWeight; void main() { // ... int b1 = int(aBone1); vec3 o1 = uBonePosOriginal[b1]; vec3 p1 = uBonePosMoved[b1]; vec4 q1 = uBoneRotations[b1]; vec3 r1 = qtransform(q1, position - o1) + p1; int b2 = int(aBone2); vec3 o2 = uBonePosOriginal[b2]; vec3 p2 = uBonePosMoved[b2]; vec4 q2 = uBoneRotations[b2]; vec3 r2 = qtransform(q2, position - o2) + p2; position = mix(r2, r1, aBoneWeight); // ...
一つの頂点につき影響するボーンが2つあるのでこんなふうになってる。詳しくは頂点ブレンディングで検索。あと↓とかを参考にした。
↓これの部分は、
vec3 r1 = qtransform(q1, position - o1) + p1;
上に書いた↓これ
移動後の位置 = 親の回転 * (元の位置 - 親の元の位置 + 平行移動) + 親の移動後の位置
と同じ形。スキンの頂点にとって親はボーンということ。平行移動は無し(モーフがそれにあたるけど)。
クォータニオンによるベクトルの回転
四元数で回転 入門では「ベクトル(α, β, γ)の進む方向に向かって眺めて反時計回りに」回転させるクォータニオンを定義しているが、時計回りに回転させる定義もあるらしい。例えばこことか。
この定義の違いによって、クォータニオンによる回転を にするか、 にするかが変わってくるっぽい。
時計回り()を採用すると、ベクトルの回転の計算は GLSL で
vec3 qtransform(vec4 q, vec3 v) { return v + 2.0 * cross(cross(v, q.xyz) - q.w*v, q.xyz); }
と書ける。反時計回り定義だとマイナスのところがプラスに変わる。
なぜこうなるのかが直感的には分かりにくいので証明しておく。
クォータニオン によってベクトル を回転させることを考える。
ベクトル をクォータニオン に変換し、回転は とする。ただし は の共役クォータニオン(絶対値が1なので逆元と同じ)。
また、2つのクォータニオン と の掛け算の定義
より、
となる。同様に の実部を計算するとゼロになるので割愛し、虚部 を計算すると
となる。さらに、ベクトルについての恒等式 と回転クォータニオンの性質 を使うと、
となる。証明終了。