cocos3d+Bulletで物理シミュレーション (実装編) #cocos2d_2011_adcal

@yoichinejiさん主催のcocos2d Advent Calendar 2011、1日遅れで19日目の記事です。
前日の記事: ゲームを作るなら効果音、声、BGMも必要だよね (@aoi68kさん)

前回導入編の続きで、実装編です。
サンプルコードは前回と同じものです。(こちらからダウンロードできます)
固定された床にキューブが降り注ぎ、積み重なっていくデモです。

ccadcal16_07.png


3Dモデルの読み込みと表示

cocos3dでの3Dモデルの読み込み・表示は、テンプレートから作成したプロジェクトのCC3World派生クラスにもある通り、とても簡単です。

[self addContentFromPODResourceFile: @"hello-world.pod"];

これだけ。

モデルのフォーマットはPOD(PowerVR Object Data)ですが、Collada2PODというツールでCOLLADAフォーマット(.dae)から変換できます。
大概のモデリングツールはCOLLADAで出力できると思います。
Collada2PODのダウンロードはこちらから(要サインアップ)。

ccadcal19_01.png

Collada2PODの設定ファイル(Collada2PODSettings.txt)がcocos3dのToolsフォルダに同梱されているので、これをLoad Optionsから読み込んで変換するとうまい具合にいきます。

テクスチャを貼っている場合は、その画像をプロジェクトのリソースとして追加すればOKです。


物理シミュレーションの準備

物理シミュレーションの実装を記述するソースコードでbtBulletDynamicsCommon.hをimportします。

#import "btBulletDynamicsCommon.h"

BulletはC++で記述されているため、Objective-Cから使う場合はObjective-C++で記述します。
該当するソースコードの拡張子を「.m」から「.mm」に変更します。

サンプルコードではcocos2dAdventCalendarDay16World.hでbtBulletDynamicsCommon.hをimportしているため、このヘッダファイルをimportしているすべてのファイルの拡張子が「.mm」になっていますが、実際にC++のコードを記述しているのはcocos2dAdventCalendarDay16World.mmだけです。


ダイナミクスワールドの作成

まず物理シミュレーションを行うための世界・ダイナミクス(力学)ワールドを作成します。
今回は単純な剛体による物理シミュレーションのため、btDiscreteDynamicsWorldクラスを使います。

collisionConfiguration = new btDefaultCollisionConfiguration();
collisionDispatcher = new btCollisionDispatcher(collisionConfiguration);
broadphaseInterface = new btDbvtBroadphase();
constraintSolver = new btSequentialImpulseConstraintSolver();
dynamicsWorld = new btDiscreteDynamicsWorld(collisionDispatcher, broadphaseInterface, constraintSolver, collisionConfiguration);
dynamicsWorld->setGravity(btVector3(0.0f, -9.8f, 0.0f));

最後にY軸のマイナス方向に1Gの重力を設定しています。


コリジョンシェイプの作成

コリジョンシェイプとは衝突判定を行うために物体がどんな形状をしているかを表すオブジェクトです。
btCollisionShapeクラスを基底として、直方体、球、円柱、円錐、カプセル型などそれぞれの形状の派生クラスが用意されています。
床用のコリジョンシェイプ(直方体)の作成はこんな感じです。

groundCollisionShape = new btBoxShape(btVector3(4.0f, 0.25f, 4.0f));

コンストラクタの引数にX、Y、Z方向の大きさを渡して作成します。
Bulletでは大きさを表すときによくhalf extent(半分の大きさ)が使われています。なので、このシェイプの大きさも縦横8×8、高さ0.5となります。

コリジョンシェイプは使い回すことができます。
サンプルコードでも降り注ぐキューブの方は、一つのコリジョンシェイプをインスタンス変数として保持しておいて使い回しています。


モーションステートの作成

モーションステートとはBulletによる物理シミュレーションの結果変化した向きや位置を受け渡すためのインターフェースです。
通常はbtMotionStateの派生クラスを自分で作る必要はないようなので、デフォルトのbtDefaultMotionStateクラスを使います。

btDefaultMotionState *groundMotionState = new btDefaultMotionState(btTransform(btQuaternion(0.0f, 0.0f, 0.0f, -1.0f), 
  btVector3(groundMeshNode.location.x, groundMeshNode.location.y, groundMeshNode.location.z)));

コンストラクタに初期状態の回転・位置を示す変換情報を渡して作成します。位置は配置済みのCC3MeshNodeオブジェクトのlocationに合わせています。
回転を示すクオータニオンのWが-1な理由は後ほど。


リジッドボディの作成

リジッドボディ(剛体)とはBulletにより物理シミュレーションを行う物体を表すオブジェクトです。
先に作成しておいたコリジョンシェイプとモーションステートをリジッドボディに関連付けます。

btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0.0f, groundMotionState, groundCollisionShape, btVector3(0.0f, 0.0f, 0.0f));
btRigidBody *groundRigidBody = new btRigidBody(groundRigidBodyCI);

btRigidBodyConstructionInfo構造体で、物体の質量、モーションステート、コリジョンシェイプ、慣性を設定します。

Bulletには、シミュレーションによって動かされる「動的剛体」、衝突するが動かない「静的剛体」、ユーザーによって動かされ動的剛体を押すことはできるがそれ自体は影響を受けない「キネマティック剛体」の3種類があります。
質量を正の値にすると動的剛体、0にすると静的剛体になります。静的剛体にキネマティックフラグを設定するとキネマティック剛体になります。
床は衝突するが動かない静的剛体なので、質量に0を指定しています。

降り注ぐキューブは動的剛体なので、以下のようなコードになります。

GLfloat mass = 1.0f;
btVector3 localInertia(0.0f, 0.0f, 0.0f);
cubeCollisionShape->calculateLocalInertia(mass, localInertia);
// (中略)
btRigidBody::btRigidBodyConstructionInfo rigidBodyCI(mass, motionState, cubeCollisionShape, localInertia);
btRigidBody *rigidBody = new btRigidBody(rigidBodyCI);

動的剛体の場合は、慣性をcalculateLocalInertia()関数によって算出します。

こうして作成したリジッドボディには、パラメータで表面の摩擦や弾み具合など物体の材質を設定できます。
このリジッドボディをダイナミクスワールドに追加することによって物理シミュレーションされるようになります。

dynamicsWorld->addRigidBody(rigidBody);


シミュレーションの実行

まずCCSchedulerを使って、更新用メソッドが毎フレーム呼ばれるようにしておきます。

[[CCScheduler sharedScheduler] scheduleSelector:@selector(updatePhysics:) forTarget:self interval:0 paused:NO];

更新用メソッドの方では、dynamicsWorldのstepSimulation()関数を呼びます。

dynamicsWorld->stepSimulation(dt, 2);

これで物理シミュレーションが1ステップ実行され、各物体の向きや位置が更新されます。
あとは、各物体のモーションステートから変換情報を取り出して、対応するCC3Nodeオブジェクトに反映していけばいいわけです。

CC3MeshNode *meshNode = cube.meshNode;
btRigidBody *rigidBody = cube.rigidBody;

btTransform transform;
rigidBody->getMotionState()->getWorldTransform(transform);
btVector3 pos = transform.getOrigin();
meshNode.location = cc3v(pos.getX(), pos.getY(), pos.getZ());
btQuaternion quaternion = transform.getRotation();
meshNode.quaternion = CC3Vector4Make(quaternion.getX(), quaternion.getY(), quaternion.getZ(), -quaternion.getW());

物体の位置は座標をそのまま渡せばOKです。
回転は、クオータニオンのWを-1倍して渡します。回転方向が合わず物体同士がめり込むことがあったのでこうしています。

完成したデモはこんな感じです。

ちょうど0.6.5がリリースされたcocos3dですが、3Dオブジェクトを簡単に扱え、かつ、CCActionによるアクションももちろん使えるという非常に便利なフレームワークです。
ぜひ使ってみてください!


参考資料:
cocos3d APIリファレンス
http://brenwill.com/docs/cocos3d/0.6.5/api
cocos3d プログラミングガイド
http://brenwill.com/2011/cocos3d-programming-guide/
Bullet APIリファレンス
http://bulletphysics.com/Bullet/BulletFull/
Bullet 日本語マニュアル
http://bulletjpn.web.fc2.com/


次の記事: Cocos3dをさわってみた。 (@kclabさん)