MMD on WebGL 速くしてみた
今日は小粒の変更。
(後ろ向いたときに髪の色がおかしいな…)
Lat 式でも普通の速さで再生できるようになった。というか前遅かった原因の半分は僕のコードの些細な間違いのせいだったんだけど。
あと Firefox のシェーダーがバグってる件を回避したので Firefox でも再生できる。ただし Firefox はうちでは 22fps ぐらいしか出ない。Chrome だとちゃんと 30fps 出て、最高では 40fps ぐらいまで耐えられるみたい。
現在は物理演算を実装してる。物理と IK は Workers の中でやることにすると思うので、たぶん Firefox でも 30fps におさまると思う。希望的観測。
MMD on WebGL 踊れるようになった(あと IK について)
IK を実装して踊れるようになった。
11月2日現在、「きしめん」を踊ってくれる。普通のスピードで再生できれば25秒ぐらい。
MMD のフレームは 30fps なので、リアルタイムに再生するなら 1000ms/30=33ms ぐらいで1フレームの処理を終えなければならないのだけど、IK の計算で 30ms ぐらいかかってしまう時もあり(GC のためか)、物理演算までやるのは厳しくなってきた。今でさえちょっと遅いパソコンだとスローモーションになってしまう。あと Firefox より Chrome のほうがかなり速い。
Lat 式から頂点数の少ないモブ子さんに替えてみた。Firefox でもヌルヌル動くはず。ただし Windows の Firefox にはバグがあるので真っ白になることもある。
物理までやったらもうちょっと最適化してみる。
IK について
↓この動画の5分40秒目以降を先に見たほうが理解しやすいはず。
【ゆっくり解説】 第8回 MMDモデルを踊らす 【3Dプログラミング】 ‐ ニコニコ動画(原宿)
以下は主に数式の説明になる。前回の Forward Kinematics の説明と一部かぶるところがある。
IK 演算に関係するボーンはこんな感じになっている。(IK 影響下のボーンは平行移動はしないみたいなので考えないことにする)
ボーン名 | タイプ | 位置 | 個別回転 | 合成回転 |
---|---|---|---|---|
センター | 回転・移動 | |||
... | ... | ... | ... | ... |
右髪1 | 回転 | |||
右髪2 | IK影響下 | |||
右髪3 | IK影響下 | |||
右髪4 | IK影響下 | |||
右髪5 | IK影響下 | |||
右髪6 | IK影響下 | |||
右髪7 | IK接続先 | ... | ... | |
右髪IK | IK | ... | ... | |
右髪IK先 | 非表示 | ... | ... | ... |
「個別回転」というのは VMD ファイルから得られる数字で、モデルの初期姿勢からのそのボーンの回転をグローバル座標で表したもの(他の 3D のフォーマットではボーンのローカル座標で回転・平行移動表すこともあるらしい)。「合成回転」というのは各ボーンの親からの回転を順々に掛けていったもので、これも当然グローバル座標。
例えば「右髪6」の合成回転を求めるには、
のようにしてセンターから順に回転を掛けていけばいい。
下の図で、緑は「右髪7」に、黒は先から順に「右髪6」「右髪5」…にそれぞれ対応する。一番左が初期位置で、真ん中は一番先の回転を適用したところ、次は二番目の回転を適用したところ、となっている。
そうやってまず Forward Kinematics 計算をする。ちなみに「右髪7」までは上からずっと親子関係になってて、「右髪IK」はセンターの直接の子になっている(足の IK はセンターの子ですらない)。「右髪IK先」は計算には関係ないので気にしなくていい。
さて、IK 接続先(IK ターゲットと呼ばれることのほうが多いかも)である「右髪7」までの合成回転と合成位置を求めた後で、IK 影響下のボーンのうち一番先端である「右髪6」に注目する。
「右髪6」から IK ターゲットである「右髪7」までのベクトル と、「右髪6」から IK ボーンである「右髪IK」までのベクトル を求め、「右髪7」の位置が「右髪6」と「右髪IK」を結んだ線上に来るような回転 を求める。
赤い点は IK ボーン。
回転 は回転軸 と回転角 から求められ、それぞれ
となる。( しか考えなくてもいい。なぜならそれ以上の角度のときは回転軸が逆向きになるだけだからである。)
「右髪6」の合成回転 に先ほど求めた を掛けて修正合成回転 とする。この を使ってターゲットである「右髪7」の新しい位置を計算して終わり。
同じようにして順に「右髪5」「右髪4」等についても計算する。「右髪5」の修正合成回転を求めた後はまた「右髪6」「右髪7」の順に新しい位置を計算していく必要がある。「右髪4」の時は「右髪5」「右髪6」「右髪7」という順。
これを IK 影響下の一番根元である「右髪2」までやり、次はまた「右髪6」から、という具合に PMD ファイルで指定された回数繰り返す。
以上で、実は二点端折ったところがある。まず、MMD の IK にはそれぞれ「一回のステップで一つのボーンが曲がれる角度制限(単位制限角)」がある。
極北Pさんの PMDEditor の Readme (一番下に引用した)の説明によると、指定された値×4 radian までしか曲がれないらしい。なので、上で を求めたときに、この単位制限角との max を取る必要がある。
もう一つのポイントは、「ひざ」という名の付くボーンは X 軸方向にしか動かないというもの。(PMX だと好きなボーンに角度制限が付けられるらしいが)
これをやるにはちょっと面倒なことをしないといけない。なぜなら、先ほど求めた などは、いわば「修正『合成』回転」になるため、そこから「修正『個別』回転」 を求め、これに角度制限をかけ、さらにそれを合成回転に直さないといけない。(回転軸制限は個別回転に適用されるもので、合成回転にではない)
(修正合成回転は合成回転に を掛けたもの)と (修正合成回転は修正個別回転に親ボーンの合成回転を掛けたもの)を組み合わせると、
となる。(両辺に の逆元 をかけた)
は親の回転を加味しない絶対的なものなので、これの Y 軸方向と Z 軸方向の回転をゼロにして X 軸方向の回転をそのぶん増やしてやれば、「ひざ」の「制限付き修正個別回転」 が求められる。
「制限付き修正合成回転」は単に親の合成回転を掛ければいいだけなので
となる。
以上のプロセスを、ボーンの位置を逆から求めていくので Inverse Kinematics (IK) と呼ぶ。上の方法だけが唯一の IK の計算方法というわけではない。検索してみたら別の方法もあるらしい。ただ、PMD ファイルの「単位制限角」や「ループ回数」というパラメーターを使うなら上の方法しかないと思う。
以下は参考にならないかもしれないけどイメージを膨らませるのにはいい。
PMDEditor 0.0.9.9 の Readme.txt より引用
●[IK] IKリスト : IKボーンとして機能するボーンの一覧。 → IK(数値) : 対応するIKボーンIndex Target(数値) : IKボーンの位置にこのボーンを一致させるようにIK処理が行われる IKループ回数(整数) : IK処理での計算回数(最大255) 単位制限角(実数) : 一回のIK計算での制限角度(数値はrad値の模様? | 1.0=4[rad](230度程度) 180度:0.7854 = 3.141592/4) 影響下ボーンリスト : IKの影響下にあるボーン一覧 | IK接続先に近い方からリスト順にする必要がある ※各関連値は[ボーン]側で設定されている項目もありますが、IKリストはボーンリストとは独立して必要。 ※IK処理においては、特定の制限角度がボーン名に従ってMMD側で調整されるようです。 例:ひざ→縦(X軸方向)にしか稼動しない ○degボタン 単位制限角を角度で入力/有効範囲は0-180後程度(あくまで解析情報からの推測値となります) ○IK値(IKループ回数/単位制限角)について IK値によるIKの制御パラメータの関係について、大体以下のようになるようです。 <IKループ回数>, <単位制限角> 小, 大 : IKのLink先ボーンに強く依存する=先だけよく曲がる(系列が短い場合はこれで十分) 大, 小 : IK系列全体で曲がるようになる(しなりが強くなり直線形状になりやすい) 大, 大 : 比較的 (小, 大) と同様の傾向を示すと思われる。 小, 小 : 非常に曲がりにくくなり、IKに対する追従も悪くなる 多くのボーン系列を持つような場合は、単位制限角を非常に小さな値にする必要がある。 単位制限角による制限はIK影響下ボーンが下位に進むにつれ(ボーン列的には根元に行くになるに従って)、 制限角度が拡がっていく仕様なので、多関節の場合、先端付近より根元の方が比較して大きく曲がるようになる。 ※IK変形は通常のボーン変形に比べ繰り返し回数がかなり多いので、非常に負荷の高い変形になります。
MMD on WebGL レンダリング部分のマイナーチェンジ
Lat 式を表示できるようにして以来の変更。
Lat 式と構造の似ている ulaP さんの雪歌ユフを表示してみたところ、なんかヒゲができてしまった。
それ以外はほとんど同じなのになぜ…
とアレコレ捻り回して眺めてたら、顔の前にほぼ真透明の材質があることに気づいた。左が透過を有効にした法線マップ。右が透過無効の法線マップ。
この前は気づかなかったけど Lat 式も同じようになってたみたい。
これを見て閃いた。もしかすると透明な材質の奥ではエッジを描画しないことになっているのでは?
ということで「透明な髪」でピンと来たデフォ子(byななみさん)で試してみたら、予想は当たってたっぽい。(赤で囲んだところ)
それでまた色々試した結果、レンダリング部分をこういう感じから
gl.enable(gl.CULL_FACE); gl.enable(gl.BLEND); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA, gl.DST_ALPHA); model.materials.forEach(function(material){ gl.cullFace(gl.BACK); renderMaterial(material); gl.cullFace(gl.FRONT); renderEdge(material); }); gl.disable(gl.BLEND); gl.disable(gl.CULL_FACE);
こういうふうに変えて、エッジ描画の前にアルファブレンディングを切ることで対処した。
gl.enable(gl.CULL_FACE); gl.enable(gl.BLEND); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA, gl.DST_ALPHA); model.materials.forEach(function(material){ gl.cullFace(gl.BACK); renderMaterial(material); }); gl.disable(gl.BLEND); model.materials.forEach(function(material){ gl.cullFace(gl.FRONT); renderEdge(material); }); gl.disable(gl.CULL_FACE);
ループが2つになって、(しかも今やってる方法だと材質ごとにボーンの付け替えをするので)処理としては数ミリ秒増えてしまったのが残念。
しかしこれで雪歌ユフも表示できるようになりましたとさ。
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つのクォータニオン と の掛け算の定義
より、
となる。同様に の実部を計算するとゼロになるので割愛し、虚部 を計算すると
となる。さらに、ベクトルについての恒等式 と回転クォータニオンの性質 を使うと、
となる。証明終了。
MMD on WebGL 番外編・本当は恐ろしい Lat 式
MMD on WebGL は GitHub では MMDGL.js というプロジェクトだったんですが、nyamadan さんの mmdgl-coffee っていうプロジェクトが既にあったことに気づいたので、名前を変えました。
幸い MMDGL.js という名前は宣伝して来なかったので、この際 MMD.js という直球な名前にしました。まあプロジェクト名は全面に押し出すつもりもないし、これまでどおり MMD on WebGL という呼び方をすると思うので。
本当にあざとい Lat 式
衝撃の初音ミク3Dモデル: GA-j blog
Lat 式は特殊だ、とよく言われますが、どう特殊なのか。
それは法線マップ*1を標準モデルと比べてみれば一発でわかります。
鳥山明↓を思い出してしまいました。
口と目の周りは浮いてるわ、眉は後ろ向いてるわ、鼻や頬や前髪は一枚板だわ、影だと思ったら実は影色の透明フィルムだったりするわ、まじ酷いです。こんなすごいのよく作ろうと思いましたね。感服します。ほんと Lat さんはあざといです。
こういうことなので、Lat 式は Blender など他の 3D ソフトで使えないらしいです。MMD on WebGL を作り始めたきっかけは、MMD のシェーダーを理解して公開することで、他の 3D ソフトにもシェーダーを移植できればいいなと思ったからでした。(自分でやるかどうかは別として)
*1:法線マップというのは、法線(表面がどの方向を向いているか)ベクトルの xyz をそれぞれ色 rgb に置き換えたものです。x が1なら右を向いてて(色だと r が1)、x が-1なら左を向いている(色だと r が0)ことになります。
MMD on WebGL やっと Lat 式を表示できた
MikuMikuStudio を作ってる chototsumoushin さんに、「culling がおかしいんじゃね?」的な助言をいただいたので、頑張ってみた。
↓デモ。10月20日現在、Lat 式になってる。たぶんあとで標準モデルに戻す。「コッチミンナ」目をオンにしてあるので、どのアングルを向いてもこっちを見てくれる。
Mac の Chrome で確認したら酷いことになってた… あとで直す。
Mac でもちゃんと見られるようになった。pow(0.0, 0.0)
が GLSL で未定義なので、0.0だったとこを0.001にした。
ここから本題。
アルファブレンディングについて
この前シェーダーがほぼ完成したと書いた時は↓こんな感じ。
前やってたことは、まず全部の材質を drawElements して、次に cullFace(FRONT)
して(各頂点を法線方向にちょっとずらして)エッジを描画するという感じだった。ただし、これだけだと材質の裏面が黒くならないので、gl_FrontFacing で裏面を判断してそのときも黒くしてたんだけど、こんなヘンテコなことするのはおかしいよなーと思ってた。
今回は、まず cullFace(BACK)
して材質を drawElement して、次に cullFace(FRONT)
してエッジを同じように描画するようにした。これだけで裏面も黒くなる。こんな簡単なことに気づかなかったなんて…
これで↓こうなる。Lat 式は顔の作りが特殊で、法線が後ろ向いてるらしくてどうたらこうたら(よく知らない)のため、顎が黒くなってたっぽい。
次に、MikuMikuStudio (MMDLoaderJME) のコードで setBlendMode(BlendMode.Alpha)
というのがあったので、blending というのをやってみる。これについてはこのへんを参照。
- テクスチャでアルファ(透過)を使用する - 強火で進め
- ブレンド処理 - OpenGL de プログラミング - livedoor Wiki(ウィキ)
- ブレンディング::計算式の設定 - OpenGL de プログラミング - livedoor Wiki(ウィキ)
要はこれでアルファブレンディングというのをしないと、gl_FragColor
にせっかくセットしたアルファの効果が発揮されなくて、常に最前面の面のみが見えてる状態になってるらしい。
WebGL には BlendMode などというのは無くて、
gl.enable(gl.BLEND); gl.blendFunc(...);
という感じでやる。上の Wiki の blendFunc の例を見ながら
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
とすると…
おお!顔が出てきた。
…しかし、よく見ると顔に影が無い。もっと(照明を落として)よく見ると、
影になる部分が逆に明るくなってる。
実は Lat 式の顔には影ができないようになってて。影に見える部分は黒っぽい透明の材質が貼られているだけ。
黒っぽい透明なのになんで白くなるのか…と数時間悩んだところ、WebGL の gl.blendFuncSeparate
というのを発見。
なるほどそういうことか!ってことでやってみたら、
大成功。
つまり、色成分とアルファ成分を別々にブレンドするということだった。
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // これは↓こういうこと // ピクセルの色(rgba) = 下層の面の色(rgba) * (1 - 上層のアルファ) + 上層の面の色(rgba) * 上層のアルファ
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA, gl.DST_ALPHA); // これは↓こういうこと // ピクセルの色(rgb) = 下層の面の色(rgb) * (1 - 上層のアルファ) + 上層の面の色(rgb) * 上層のアルファ // ピクセルのアルファ = 下層の面のアルファ + 上層の面のアルファ
そりゃこうじゃないとおかしいね。
MMD on WebGL カメラとライトと表情のモーションに対応(あと補間曲線について)
タイトルの通りです。VMD を読み込んだり、CoffeeScript で全部書きなおしてたりしたら時間がかかってしまいました。
- http://edv.sakura.ne.jp/mmd/ (10月16日現在、開くと動き始めるはずです)
ここからが本題。
補間曲線について
MMD の補間曲線はベジエ曲線で、ベジエ曲線に必要な4つの点のうち、最初と最後が固定されている。すなわち、
こんな感じ。
を表す4つの数字は VMD ファイルに0から127までの整数として記録されていて、これらを127で割ってそれぞれの座標とする。
また、例えば登録されているキーフレームが10と20だったとして、13番目のフレームを描画したいとすると、 となる。
この5つの数字からまず を求め、次に を求めるのだが、 を求めるためには三次方程式を解かなければいけない。
幸いにして、この は常に( によらず)単調増加らしい。厳密に証明したわけではないが、MMD の補間曲線のところでバツ印をどのように動かしても、 と について「いったん上がってまた下がる」ということが無いことから推測できる。
が単調増加であると仮定すると、 を満たす はひとつしか無く、二分法やニュートン法で求められる。
自分は収束が早いとされるニュートン法ではなく、二分法を使った。その理由は以下のとおり。
まず仮に を1/2と置き、 を計算し、それが正ならば から1/4を引いて、負ならば1/4を足して、再度 を計算する。このように1/8, 1/16, 1/32…と進んでいくのだが、求められる精度は1/65536もあれば十分だと思うので、15回ほどの繰り返せば十分に収束したとみなすことができる。二分法の収束が遅くても、高々15回ぐらいの繰り返しなので問題ない。
なんせ10分の動画でも 10 min * 60 sec * 30 fps = 18000 frames しかないので、10分の動画の最初から最後までを一つのベジエ曲線で補間したとしても、隣り合うフレーム同士は1/18000の精度でしか離れてないことになる。
そんなわけでアルゴリズムを載せておく。
function interpolateBezier(x1, x2, y1, y2, x) { var t = 0.5, s = 0.5; for (var i = 0; i < 15; i++) { var ft = (3 * s * s * t * x1) + (3 * s * t * t * x2) + (t * t * t) - x; if (ft === 0) break; // Math.abs(ft) < 0.00001 でもいいかも if (ft > 0) t -= 1 / (4 << i); else // ft < 0 t += 1 / (4 << i); s = 1 - t; } return (3 * s * s * t * y1) + (3 * s * t * t * y2) + (t * t * t); }
MMDAgent はニュートン法でやってるみたい。Chrome と Opera と IE9 ではそっちのほうが早かった。Firefox では逆。と言っても100万回で200ミリ秒ぐらいの違いしかないけど→http://jsfiddle.net/km96k/
ところで、カメラモーションの場合は補間用のパラメータが24個( のペアが「X軸移動」「Y軸移動」「Z軸移動」「回転」「距離」「視野角」について、この順番にそれぞれ)あるのだけど、
ボーンの場合はパラメーターが64個あって、これはどういう内訳になってるんだろう?
あった。VMDメモ 針金のブログ/ウェブリブログ(というか数字眺めてたら分かった。なんでこんなふうになってるんだろ)