情報メディア実験 物理エンジンを使ったアプリケーション開発(5)

3Dモデルファイルを用いた形状設定

これまでは球やボックス,円筒など決まった形状のみを扱っていた. しかし,アプリケーションを制作する上で任意の形状を扱いたいと思うだろう. 1つの方法としてはbtCompoundShapeを使うことである. これを使うと複数の形状を組み合わせた形状を作成することができる. ただ,やはり元になる形状は球やボックスなどに限られてしまう.

任意の形状を扱う別の方法として, ポリゴン(三角形メッシュ)で構成された形状をファイルから読み込んで, それをBulletに設定する方法がある. Bullet User Manualのp.19にあるConvex Hull Shapes や Concave Triangle Meshes を使うと, ポリゴン情報から形状を作成できる. 以下ではポリゴン情報が書かれたモデルファイルを読み込んで,任意形状の剛体や弾性体を設定する方法について順番に説明する. なお,User ManualではbtBvhTriangleMeshShapeを使うと書いてあるが, ここではbtGImpactMeshShapeを用いた方法について説明する.

3Dモデルファイルの読み込み

3Dモデルファイルとは, Mayaや3D Studio Max,Rhinocerosなどの3Dモデリングソフトで3次元形状を作成し, そのポリゴン(主に三角形ポリゴン)情報をファイルに書き出したものである. (3Dモデリングソフトには無料で使えるものもいくつかあり,代表的なのはBlenderやMetasequoiaLEなどがある. ちなみに実習室のPCにはRhinocerosとMetasequoiaLEがインストールされている). 3Dモデルファイルには様々な形式があり, 代表的なものだけでも,OBJ, 3DS, VRML, DXF, STLなどである (参考:3Dモデルファイルの種類). BulletはOBJ形式(正確にはWavefrontOBJ)の入力には対応しているが, 機能としてはかなり限定的なので,ここでは私の方で作成したOBJファイル入出力ヘッダを用いる.

サンプルプログラムのshared/incフォルダには rx_obj.h というファイルがすでに含まれているので, これをインクルードすることでOBJ形式のファイルを読むことができる.

OBJ形式を読み込むためにはまず,rx_obj.h をインクルードする

#include "rx_obj.h"

rx_obj.h はヘッダファイル内に全ての実装を書いているヘッダオンリーのライブラリなので, libファイルのリンクは必要ない. なお,OBJ以外の3DS,VRML,DXF,STLなどを読み込みたい場合は, こちらのページ に私の方で作成したライブラリがあるのでそれを使ってみてほしい (サンプルプログラムに含まれている rx_obj.h と違ってヘッダオンリーではないので, Bullet Physicsをビルドしたように,自分の環境でビルドしてlibファイルを作成する必要があるので注意)

3Dモデルファイルを読み込む場合は以下のように設定する.

// 3Dファイル読み込み
rxMTL mats;
vector<glm::vec3> vrts, nrms;
vector<rxFace> tris;
rxOBJ obj;
obj.Read(filename, vrts, nrms, tris, mats);

filename(string型)に3Dモデルファイルのパスを設定する. rxOBJ::Read関数ではfilenameで示されたOBJファイルを読み込み, vrtsにポリゴンの頂点座標情報,nrmsに頂点法線の情報, trisに接続情報,matsに質感(表面色など)の情報を格納して返す.

テスト用に3Dモデルファイルをいくつか置いておく.

テキストファイルとしてブラウザ上で表示されてしまう場合は, リンクを右クリック→リンク先を名前を付けて保存 すること.

メッシュをBulletに設定

btRigidBodyの場合はbtTriangleIndexVertexArrayクラス,btSoftBodyの場合はbtSoftBodyHelpers::CreateFromTriMesh関数 を用いてメッシュを形状として設定する. 上記のOBJファイル読み込みでは,glm::vec3型の配列に頂点情報,rxFace型に接続情報を入れていたが, これらをそのままCreateFromTriMesh関数に渡すことはできないので, データを変換する必要がある(頂点法線情報はここでは必要なし).

// メッシュデータから頂点とポリゴン(三角形メッシュ)情報を取得して配列に格納
int vertex_count = (int)vrts.size(); // 総頂点数
int index_count = (int)tris.size(); // 総ポリゴン数
btScalar *vertices = new btScalar[vertex_count*3]; // 頂点座標を格納する配列
int *indices = new int[index_count*3]; // ポリゴンを構成する頂点番号を格納する配列

// 頂点座標の取り出し
for(int i = 0; i < vertex_count; ++i){
  vertices[3*i] =   vrts[i][0];
  vertices[3*i+1] = vrts[i][1];
  vertices[3*i+2] = vrts[i][2];
}
// ポリゴンを構成する頂点番号の取り出し
for(int i = 0; i < index_count; ++i){
  indices[3*i]   = tris[i][0];
  indices[3*i+1] = tris[i][1];
  indices[3*i+2] = tris[i][2];
}

verticesに頂点の座標情報,indicesにポリゴンを構成する頂点番号が格納されている.

なお,ここまでの設定は 形状の重心=頂点の座標系原点 であることを想定している. もし原点がずれている場合,btRigidBodyにおける動き/回転の中心が形状ローカル座標系の原点となるため, 意図しない動きになってしまうことがある.その場合は,上記の"頂点座標の取り出し"のところを以下のように変更して, 重心=原点となるようにすれば良い.ただし,このままだとすべての追加した物体がグローバル座標の原点に集まってしまうので, btRigidBodyに追加するときに.trans.setOrigin(mc); のように剛体を重心位置に移動させるのを忘れずに.

// 形状の重心を求める
btVector3 mc(0, 0, 0);
for(int i = 0; i < vertex_count; ++i){
  mc += btVector3(vrts[i][0], vrts[i][1], vrts[i][2]);
}
mc /= (double)(vertex_count);

// 重心を原点とした座標系に変換しながら頂点座標を取り出し
for(int i = 0; i < vertex_count; ++i){
  mqo_vertices[3*i] =   vrts[i][0]-mc[0];
  mqo_vertices[3*i+1] = vrts[i][1]-mc[1];
  mqo_vertices[3*i+2] = vrts[i][2]-mc[2];
}

以下でこれらの情報からbtRigidBody, btSoftBodyそれぞれどのようにして設定するかを説明する.

btRigidBodyの場合
btRigidBodyの場合はbtTriangleIndexVertexArrayクラスを用いる.

int vertex_stride = 3*sizeof(btScalar);
int index_stride = 3*sizeof(int);

// 三角形メッシュ形状の作成
btTriangleIndexVertexArray* tri_array = new btTriangleIndexVertexArray(index_count, indices, index_stride,
                                                                       vertex_count, vertices, vertex_stride);

index_stride, vertex_strideはそれぞれindices,vertices配列において, 次のポリゴン,頂点を指し示す位置までのメモリ上のサイズ(Byte)である.

btTriangleIndexVertexArrayを作成したら, btGImpactMeshShapeで形状を設定し,それをbtRigidBodyに設定する. btGImpactMeshShapeを使うためには,まず,以下のインクルードを追加する.

#include "BulletCollision/Gimpact/btGImpactCollisionAlgorithm.h"
#include "BulletCollision/Gimpact/btGImpactShape.h"

そして,btTriangleIndexVertexArray型の変数を引数として,btGImpactMeshShapeをnewする.

btGImpactMeshShape *shape = new btGImpactMeshShape(tri_array);
shape->updateBound();

updateBound()を忘れないように. あとはこれをCollisionShapeとしてbtRigidBodyを設定すればよい. この処理は通常のbtRigidBodyの定義と同じなので省略する.

なお,セットし終わった後のindicesとverticesはdeleteせずにそのまま残しておくこと. 内部的にポインタ参照しているのでこれをdeleteすると参照ミスでプログラムが落ちることがある. ただし,これはbtGImpactMeshShapeの場合で, 次の説明ページで使うbtSoftBodyの場合はデータをコピーしているので逆にdeleteする必要があることに注意.

dispatcherをbtGImpactMeshに設定
ポリゴンメッシュ同士を衝突させるには,btGImpactMeshにdispatcherを登録する必要がある. Bulletの初期化関数(InitBullet関数)内のdispatcher定義後に以下のコードを追加する.

btGImpactCollisionAlgorithm::registerAlgorithm(dispatcher);

3Dモデルの描画(興味がある人向け)
btGImpactMeshShapeをOpenGLで描画するためのコードはすでにサンプルプログラム2には含まれているので, 描画するために新たに実装は必要ない.ただ,どのようにして描画しているのかを知りたい場合のためにOpenGLでの描画方法を説明する.

btGImpactMeshShapeやbtBvhTriangleMeshShapeはこれまでと異なり, 直接ポリゴンの情報を取得するのではなく, ポリゴンを描画するクラスを定義して,そのインスタンスを渡すことで描画を行う. ポリゴン描画クラスは例えば以下のようなものである(このコードはmain.cppではなくutils.hにある).

// ポリゴン描画処理のためのクラス(btTriangleCallbackの継承クラス)
class TriangleDrawCallback : public btTriangleCallback
{
public:
    TriangleDrawCallback(){}
    // 三角形ポリゴンを処理する関数.必ず設定しなければならない仮想関数(つまり純粋仮想関数)
    virtual void processTriangle(btVector3* triangle, int partId, int triangleIndex)
    {
        // 頂点座標からポリゴン法線を外積により計算
        btVector3 n = ((triangle[0]-triangle[1]).cross(triangle[0]-triangle[2])).normalize();
        glNormal3f(n[0], n[1], n[2]);

        glBegin(GL_TRIANGLES);
        glVertex3d(triangle[0][0], triangle[0][1], triangle[0][2]);
        glVertex3d(triangle[1][0], triangle[1][1], triangle[1][2]);
        glVertex3d(triangle[2][0], triangle[2][1], triangle[2][2]);
        glEnd();
    }
};

btTriangleCallbackクラスを継承し,processTriangle仮想関数が定義されていればどのようなものでもよい.

そして,Bulletオブジェクトを描画する関数(サンプルプログラムではDrawBulletObjects関数)での btRigidBodyの形状に応じた処理部分に以下のように追加する.

else if(shapetype == GIMPACT_SHAPE_PROXYTYPE){
    // 三角形メッシュ(GImpact)
    const btGImpactMeshShape* mesh = static_cast<const btGImpactMeshShape*>(shape);
    TriangleDrawCallback draw_callback; // ポリゴン描画クラスのインスタンス
    mesh->processAllTriangles(&draw_callback, world_min, world_max);
}

ちなみにGImpactはメッシュなどの処理や衝突検出のためのライブラリで, Bulletに統合されている.

bunny.objを読み込んで設定した例
bunny.objを読み込んで設定した例

練習問題1

説明部分にあった3Dモデルファイルを読み込むコードを実装して,三角形ポリゴンの登録を実際にやってみよう.

注) ポリゴン描画したときに一部のポリゴンが正常に描画されない(衝突判定はされている)状態になった場合は, ブロードフェーズにAABB木(btDbvtBroadphase)を使わずに, 3Dスイープ&プルーン(btAxisSweep3 or bt32BitAxisSweep3)などを使うように変更してみてください.

練習問題2(option)

検索すると様々なサイトでフリーの3Dモデルファイルが提供されているので, これらのサイトから対応する3Dモデルファイル(obj,wrl,dxf,3df,offなど)をダウンロードして読み込ませてみよう. こちらのページ にもいくつかのサイトへのリンクを載せてあるので参照してほしい(一部リンク切れがあるかも).

もし読めないファイル or 対応していない形式だったならば,3Dモデリングソフトでインポートして,他の形式(objなど)でエクスポートしてみよう. 実習室のPCにインストールされているソフトウェアの中では, MeshLabは多くの3Dファイル形式に対応している (MeshLabはオープンソースソフトウェアなので自分のPCにもインストールできる).

戻る