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;

上に書いた↓これ

移動後の位置 = 親の回転 * (元の位置 - 親の元の位置 + 平行移動) + 親の移動後の位置

と同じ形。スキンの頂点にとって親はボーンということ。平行移動は無し(モーフがそれにあたるけど)。

クォータニオンによるベクトルの回転

四元数で回転 入門では「ベクトル(α, β, γ)の進む方向に向かって眺めて反時計回りに」回転させるクォータニオンを定義しているが、時計回りに回転させる定義もあるらしい。例えばこことか。

この定義の違いによって、クォータニオンによる回転を Q^*VQ にするか、QVQ^* にするかが変わってくるっぽい。

時計回り(QVQ^*)を採用すると、ベクトルの回転の計算は GLSL で

  vec3 qtransform(vec4 q, vec3 v) {
    return v + 2.0 * cross(cross(v, q.xyz) - q.w*v, q.xyz);
  }

と書ける。反時計回り定義だとマイナスのところがプラスに変わる。


なぜこうなるのかが直感的には分かりにくいので証明しておく。

クォータニオン Q = (w; {\bf x}) によってベクトル {\bf v} を回転させることを考える。

ベクトル {\bf v}クォータニオン V = (0; {\bf v}) に変換し、回転は  QVQ^* とする。ただし  Q^* = (w; -{\bf x})Q の共役クォータニオン(絶対値が1なので逆元と同じ)。

また、2つのクォータニオン A = (a; {\bf a})B = (b; {\bf b}) の掛け算の定義


{\large AB = (ab - {\bf a}\cdot{\bf b}; a{\bf b} + b{\bf a} + {\bf a}\times{\bf b}) }

より、


{\large VQ^* = ({\bf v}\cdot{\bf x}; w{\bf v} - {\bf v}\times{\bf x}) }

となる。同様に  QVQ^* の実部を計算するとゼロになるので割愛し、虚部  {\bf v}' を計算すると


{\large {\bf v}' = w (w{\bf v} - {\bf v}\times{\bf x}) + ({\bf v}\cdot{\bf x}) {\bf x} + {\bf x} \times (w{\bf v} - {\bf v}\times{\bf x}) }

となる。さらに、ベクトルについての恒等式 ({\bf v}\cdot{\bf x}) {\bf x} = {\bf x}\times({\bf x}\times{\bf v}) + |{\bf x}|^2 {\bf v} と回転クォータニオンの性質 w^2 + |{\bf x}|^2 = 1 を使うと、


{\large {\bf v}' = {\bf v} + 2 ({\bf v}\times{\bf x} - w {\bf v}) \times {\bf x} }

となる。証明終了。