前回からだいぶ日が経ってしまいましたが、
ミクさんに物理演算を適用すべくチマチマやっておりました。
ただ残念ながら期待したようにはなってくれてません。
よく分からなかったり、うまく行ってくれなかったり、
自分の力不足な所が大きいと思うのですが、
JavaScriptでは荷が重いのかもしれません。
しばらく動作させるとハングアップしたりします・・・(^_^;)
これ以上はうまく行きそうにない感じなので、
中途半端ではありますが、この辺りで〆にしようと思います。
それでも、それなりに得たものがないこともないので、
それをまとめておこうと思います。
前回でも触れましたが、
three.jsで物理演算をやるには、
BulletのJavaScript版であるammo.jsを使いますが、
three.jsからは直接ammo.jsを使いません。
physi.jsを仲介してその機能を利用する形になります。
ammo.jsはWebWorkerを利用して別スレッドで動作するようになっています。
physi.jsおよびphysijs_worker.jsによって、
スレッド間のイベントによる
メッセージ送受信で機能する仕組みになっています。
物理演算は別スレッドでやって、
どうにか性能を稼ごうという仕様なのだと思います。
three.jsでの使い勝手を損なわないように、
physi.jsはScene,Material,Meshを拡張する形で実装されています。
Meshは物理演算で言うところの剛体に対応しており、
Box,Sphere,Capsuleなどの形状をサポートしています。
注意点としては、Meshを生成しないと剛体を設定できないことです。
つまりphysi.jsでは、見える物体と剛体は形状が一致しているという
前提に立っています。
しかし実際には、まずMeshが先にあって、
そこへ抽象的な剛体を設定するという順になると思います。
これはちょっと困りました。
ミクさんはまずモデルの形状があって、
そこへボーンや剛体などが設定されているからです。
そこでシーンを分けることで対応することにしました。
つまり通常のシーンにはモデルを登録し、
もう一つの物理演算なシーンには剛体Meshを登録し、
演算結果を2つのシーン間で相互に反映させるというやり方です。
続いて、
MMDなファイル内にある物理演算な情報が、
物理エンジン側の何に対応しているかを調べました。
MMDで言う所のrigidはその名の通り剛体を示しており、
jointはconstraint(拘束条件)に対応していることが分かりました。
こちらのBulletに関する日本語のページを参考にしました。
なお、MMDは拘束条件として6DofSpringというのを採用していますが、
physi.jsではサポートしてなかったので6Dofで代用しています。
(ammo.jsはBulletの機能を全て網羅しているはずなので、physi.jsを改造すれば6DofSpringも対応可能だとは思います)
問題なのは「衝突フィルタリング」です。
現状のphysi.jsではサポートしてないので要改造となります。
以下のようにしました。
// 剛体Meshオブジェクトに以下のようなプロパティを追加。
// obj._physijs.group および obj._physijs.mask
// physijs_worker.js: public_functions.addObject()のaddRigidBodyの所を以下のように改造。
world.addRigidBody( body, description.group, description.mask); // MOD
// massを更新することが無いなら、以降の改造はなくても構わないはず。
// physijs_worker.js: public_functions.updateMass()を以下のように改造。
public_functions.updateMass = function( details ) {
_object = _objects[details.id];
world.removeRigidBody( _object );
_object.setMassProps( details.mass, new Ammo.btVector3(0, 0, 0) );
world.addRigidBody( _object, details.group, details.mask ); // MOD
_object.activate();
};
// physi.js: mass の setter を以下のように改造。
Physijs.Mesh.prototype.__defineSetter__('mass', function( mass ) {
this._physijs.mass = mass;
if ( this.world ) {
this.world.execute( 'updateMass', { id: this._physijs.id, mass: mass, group: this._physijs.group, mask: this._physijs.mask } ); // MOD
}
});
衝突フィルタリングでは group と mask の2つのパラメータを使いますが、
MMD側の数値を変換する必要があります。
そのまま渡してもうまく行ってくれないので、かなり悩まされました(^_^;)
mask はそのままでよいのですが、
MMD側のgroupはビット番号なので、以下のようにシフト演算が必要です。
physics.group = 1 << mmd.group; さて、そんなこんなで
どうにかパラメータをセットアップするところまでは行けたのですが、
その後が大変でした。 ときに、
剛体には大きく分けて3種類あります。
- 動的(動きのある)剛体
- 正の質量。毎フレームごとにワールド変換行列が更新される。
- 静的剛体
- 質量ゼロ。衝突はするが、動かない。
- Kinematic剛体
- 質量ゼロ。ユーザーが動かすことができる。動的オブジェクトを押したりすることはできるが、オブジェクトからは影響を受けない。つまり一方通行。
MMD側も剛体の種類は3種類ですが、
「静的」、「動的」、「動的&ボーン位置合わせ」
ということになっています。
しかし3番目のがよくわかっていません。
質量ゼロなkinematicとは矛盾するので、これには該当しないと思われます。
結局どうすべきか分からなかったので単に「動的」扱いとしています。
物理演算を適用する手順は以下の様な感じになると思っています。
- 頂点モーフ処理。
- ボーンによるアニメーション処理。
- IK処理。
- 対応するボーンから取得した位置と回転を静的剛体Meshに反映して物理演算側に通知。
- 物理演算の結果、動的剛体Meshの位置と回転を対応するボーンに反映させる。
注意すべきは、剛体は全てグローバルな座標系で扱われることです。
全ての剛体Meshはシーン直下にぶら下がっています。
というかそうしないと物理演算の対象とならないのです。
親子関係とかないので剛体Meshの位置と回転はグローバル座標系となります。
剛体が位置する座標と剛体の重心点は一致するようになっていることにも注意が必要です。
さて、剛体にはそれぞれ対応するボーンが設定されていますが、
ボーンは親子関係を持ち、それぞれにローカル座標系が存在します。
さらにボーンはバインドポーズを基準とした変位量でアニメーションします。
バインドポーズとはモデルの初期の形状のことを指しています。
ミクさんで言えば、両腋を45度くらい開いて立っている状態に該当します。
バインドポーズ時のボーンの回転量は全てゼロとして設定されていますが、
対応する剛体は個々にグローバル座標系における位置と回転値を持っています。
そんなわけで、色々と変換が面倒そうなのでめまいを起こしそうです(^_^;)
物理演算で必要な手順をもう少し詳しくまとめてみます。
モデルを登録する側をシーンA、剛体を登録する側をシーンBとします。
- シーンAに属するボーンから取得した位置と回転値を変換して、
シーンBに属する静的剛体Meshに適用する。 - 変更したことをマークして物理演算の更新処理を呼び出す。
- 物理演算が更新されたイベントを受けて、
シーンBに属する動的剛体Meshの位置と回転値を変換して、
シーンAに属するボーンに適用する。
という流れになります。
この際の「変換」がなんかややこしくて、
正直、自分のやり方はなんか怪しい感じがしています(^_^;)
また、本来なら物理演算の更新処理を呼び出したら、
その結果を待ってから次のステップへ進むべきなのですが、
これだとWorkerを利用して別スレッドにしている意味がなくなってしまうので、
シーン間での変更のやり取りは行いますが、
同期はとってなかったりします(^_^;)
そんなわけでシーンA側とシーンB側でタイミングがあっていません。
さて、
そんなこんなで激しく中途半端な感じではありますが、
ミクさんに物理演算を適用した結果はこれです。
なお、ワイヤーフレームで表示されているのが剛体です。
なんかそれっぽいことをやっているという
雰囲気だけでも感じてもらえれば幸いです(^_^;)
※2013.2.6追記
現状では物理演算の結果をミクさんにうまく反映させることが出来ていません。
また、PhysijsはWorkerを使わない版に改造しています。