今回は、
three.jsのMeshにおける境界球(bounding sphere)の
扱いに気になる所を見つけたので、
それについて書いてみたいと思います。
three.jsではMeshを作る際に、
geometryとmaterialを渡すのですが、
geometryに境界球が設定されてなかった場合は、
自動的に作られるようになっています。
視錐台カリング(frustum culling)で必要となるからです。
視錐台カリングについては後ほど触れます。
上記で作られる境界球は以下のようになります。
中心=Meshの原点(0,0,0)
半径=geometryの頂点情報から計算
ここで気になるのが、
頂点情報から重心(中心)を求めた後に、
半径を計算していないことです。
Meshの原点と、構成する頂点の加算平均(重心)は、
必ずしも一致しないはずですが、
three.jsでは原点が中心という前提になっている?
ちなみにMMDなミクさんのモデルで境界球を求めてみると、
だいたい以下の様な数値となっていました。
・three.jsのデフォルトの場合
中心=(0, 0, 0) 半径=22.115
・境界ボックスを求め、それに外接する球を計算した場合
中心=(0, 10.139, -0.874) 半径=12.489
three.jsのデフォルトでは半径が倍くらいになってしまうので、
視錐台カリングの際に効率が悪くなってしまいそうです。
視錐台カリングというのは、
カメラの視界(視錐台)に含まれないオブジェクトを、
あらかじめ除外することで無駄な描画をしない方策のことです。
こちらのページの図を見ると分かりやすいかと思います。
three.jsでは
オブジェクトの境界球を使って視錐台カリングを行なっていますので、
出来るだけフィットするような境界球が望ましいことになります。
ところで
ミクさんのモデルはSkinnedMeshとなっています。
SkinnedMeshでは、自身の座標系以外に
内部にボーンによる座標系の階層を持っており、
Skeleton(骨格)が構成されています。
ミクさんのモデルの原点は、
両足の中間あたりで地面に接する位置、
骨格のルートである0番目のボーンの原点は、
だいたい体の中心あたりの位置、
となっているようです。
重要なことはこの状態でskinアニメーションすると、
ルート配下のボーンの位置とか変化しますが、
モデルの位置は変わりません。
アニメーションで対象となるのはボーンなので。
したがって、
ルートボーンの位置に応じて、
境界球の中心も調整してやる必要が出てきますが、
three.jsでは考慮されてないので
自前で対処する必要があります。
また本来なら、
アニメーションを更新する毎に、
境界球の中心と半径を再計算するのが理想ですが、
それなりにコストがかかるのでやってません。
初期のバインドポーズ時に求めた境界球のままでも、
おおむね問題なさそうにフィットしている感じです。
ミクさんが髪の毛を振り回した時とか微妙に気になるけど(^_^;)
色々書きましたが、
説明だけでは分かりにくいですよね(^_^;)
ということで、視覚化してみました。
こちらです。
左のモデルはthree.jsのデフォルトな境界球で、
中央のモデルは調整した境界球です。
調整した方は、
アニメーションに追従するようにしてあるので、
適正な視錐台カリングになってくれると思います。
なお、
緑色の球はモデルの原点の位置を表しています。
アニメーションによってルートボーンの位置は変わったりしますが、
モデル原点は最初のままの位置なことに注目してみてください。
ところで、
視錐台カリングのコードは
だいたい以下のような感じになっています。
camera.matrixWorldInverse.getInverse( camera.matrixWorld );
projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
frustum.setFromMatrix( projScreenMatrix );
if ( frustum.intersectsObject( object ) ) {
// draw object
} else {
// cull object
}
frustumは行列から計算された6つの平面から成っており、
閉じた領域を示しています。
これら平面と境界球との距離を求めることで、
frustumとの交差が判定されるようになっています。
ただ、
交差判定のコードを参照してみると、
なぜか境界球の中心位置を使わず、
オブジェクトのワールド行列から求めた位置を使っています。
つまりオブジェクトのローカル座標系における原点に対応する位置です。
これはちょっとおかしいと思うので、
境界球の中心位置を使うように変更しました。
変更後のコードは以下の様な感じになります。
intersectsObject: function () {
var center = new THREE.Vector3();
return function ( object ) {
var matrix = object.matrixWorld;
var planes = this.planes;
var negRadius = - object.geometry.boundingSphere.radius * matrix.getMaxScaleOnAxis();
// center.getPositionFromMatrix( matrix ); // original
center.copy( object.geometry.boundingSphere.center ).applyMatrix4( matrix ); // MOD
for ( var i = 0; i < 6; i ++ ) {
var distance = planes[ i ].distanceToPoint( center );
if ( distance < negRadius ) {
return false;
}
}
return true;
};
}()
上記のコードにあるように、
交差判定はワールド座標系で行われていますが、
なぜprojScreenMatrixから求められたfrustumが
ワールド座標系での6つの平面を示すことになるのか、
よく理解できてなかったりします(^_^;)
う~ん、なんでこれでいいんだろ?
よく分かってないけど、
そういうもんなんだとしておきます(^_^;)
でも、
このように視覚化してみると
見事にうまく行っています。
黄色い線で囲まれた領域が視錐台に相当します。
青い球がカリングされたオブジェクトで、
赤い球が交差判定されたオブジェクトとなります。
マウスでグリグリ動かしてみると、
分かりやすくなると思います。