Bulletでゲームを作ってみるPart2 キャラ移動の準備

投稿日:

今回は、Bullet用意されているコリジョン判定を利用してキャラクターの移動処理を作成する準備をします。コリジョン判定を実装するには数学的な知識や特有なテクニックが必要になりますが、Bulletを使えば基礎的な数学知識だけで複雑なコリジョン判定を行うことができます。コリジョン判定関数の解説、使用上の注意など説明していきます。


Bulletのコリジョン判定関数

コリジョン判定方法には、rayTest、convexSweepTest、contactTestの3種類があります。rayTestは、直線とコリジョン形状が交差するか判定し、交点を求めます。convexSweepTestは凸形状のコリジョンを直線(線形)移動させた時の衝突位置を求める判定、contactTestは2つのコリジョン形状の距離を測定する判定です。contactTestは距離が負になることがあり、その場合2つの形状同士が重なっている状態でどのぐらい重なっているかがわかるようになっています。
判定関数は、btCollisionWorldクラス(物理ワールドクラスの基底クラス)とゴーストクラス(btGhostObject)に用意されています。btCollisionWorldの判定は、ワールドすべてのオブジェクトと判定、ゴーストクラスの判定は、ゴーストに接触しているオブジェクトに対してだけ判定するため効率の良い判定が可能です。


判定用コールバッククラス

コリジョン判定関数は結果をコールバッククラスを使って取得します。それぞれの判定に対応した基底クラスから派生させたクラスを判定関数に渡します。判定により衝突などが検出されるとaddSingleResultという仮想関数が呼ばれ、引数から衝突した位置、法線、コリジョンオブジェクトクラスのポインタ、接触距離など様々な情報を取得できます。その情報をもとにゲームの判定や処理の分岐を行います。
rayTestとconvexSweepTestには、事前にコールバック関数が用意されているので、そのまま利用したり、コールバッククラス作成時の参考になります。rayTestは、一番近い交点を取得するClosestRayResultCallback、すべての交点を収集するAllHitsRayResultCallback、convexSweepTestにはClosestConvexResultCallbackなどが用意されています。

//コールバッククラス例
struct  MyAllHitsRayResultCallback : public btCollisionWorld::AllHitsRayResultCallback
{
  //コンストラクタなど省略
  
  virtual  btScalar addSingleResult(LocalRayResult& rayResult,bool normalInWorldSpace)
  {
    // ここに独自の処理
    // 味方には銃弾が当たらないなど
    if(判定)return 1.0f;//1.0->障害物なし

    return addSingleResult(rayResult,normalInWorldSpace);
  }  
};

rayTest
void btCollisionWorld::rayTest(
        const btVector3& rayFromWorld, const btVector3& rayToWorld,
        RayResultCallback& resultCallback) const;

void btGhostObject::rayTest(
        const btVector3& rayFromWorld, const btVector3& rayToWorld,
        RayResultCallback& resultCallback) const;

rayFromWorldからrayFromWorldまでの線分に交差するコリジョンオブジェクトとその交点、法線を取得します。銃弾のような小さくて高速移動する物体の衝突判定、障害物の有無判定、オブジェクトの選択など判定処理の基本となる処理です。ポリゴンのつなぎ目、剛体の角、隣接している剛体の境目などで計算誤差のために結果が不安定になることがあります。問題ないはずの判定処理が正常に動作しない原因となることがあります。


convexSweepTest
void btCollisionWorld::convexSweepTest (const btConvexShape* castShape,
        const btTransform& from, const btTransform& to, ConvexResultCallback& resultCallback,
        btScalar allowedCcdPenetration = btScalar(0.)) const;

void btGhostObject::convexSweepTest (const btConvexShape* castShape,
        const btTransform& from, const btTransform& to, ConvexResultCallback& resultCallback,
        btScalar allowedCcdPenetration = btScalar(0.)) const;

//使用例 ゴーストの移動判定
  btCollisionWorld* pCollisionWorld = コリジョンワールド;
  btGhostObject* ghost = 移動させるゴースト;
  
  //btConvexShapeの派生クラス限定
  btConvexShape* shape = (btConvexShape*)ghost_body->getCollisionShape();

  btVector3 velocity = 移動速度;
  btScalar allowedCcdPenetration = 侵入距離(後述);
  
  btTransform t_from = ghost_body->getWorldTransform();
  btTransform t_to = t_from;
  t_to.setOrigin(t_to.getOrigin() + velocity);
  btCollisionWorld::ClosestConvexResultCallback cb(t_from.getOrigin(), t_to.getOrigin());
  ctx.pCollisionWorld->convexSweepTest(shape, t_from, t_to, cb, allowedCcdPenetration);

  btVector3 next_pos;//移動先
  if(cb.hasHit()){
    //コリジョンに衝突 衝突位置まで移動
    // m_closestHitFractionに移動割合 t_from:0.0 -> t_to:1.0
    next_pos.setInterpolate3(t_from.getOrigin(), t_to.getOrigin(), cb.m_closestHitFraction);
  }else{
    //衝突なし
    next_pos = t_to.getOrigin();
  }

凸形状のコリジョンをfromからtoまで直線(線形)移動させたときに衝突するすべてのコリジョンオブジェクトとその接触点、法線を取得します。形状同士の衝突検出、高速移動するオブジェクト(と遅いオブジェクト)の衝突判定が可能です。
rayTestは計算により正確な交点を求めることができますが、convexSweepTestでは計算で正確に位置を求めることが困難なため、形状を何度も移動(イテレーション)させながら衝突位置を求めます。Continuous Collision Detection(CCD)の実装方法の1つです。何度も判定処理を繰り返すため計算に時間がかかります。


allowedCcdPenetrationについて convexSweepTestの弱点

convexSweepTestで判定できない状況があります。床の上ある剛体を床と平行(水平)に移動させると、床に接触してその場から移動できないとconvexSweepTestが判定してしまいます。その状況を解消するためにallowedCcdPenetrationというパラメータが用意されています。allowedCcdPenetrationの距離まで形状同士が重なっても衝突したことにしないというパラメータです。これにより、床の上を滑る(移動する)事が可能になります。ただし、移動後形状同士がわずかに重なっている状態になるため、次に説明するcontactTestなどを利用して重なり状態を解消する必要があります。


contactTestとcontactPairTest
void btCollisionWorld::contactTest(
        btCollisionObject* colObj, ContactResultCallback& resultCallback);
void btCollisionWorld::contactPairTest(
        btCollisionObject* colObjA, btCollisionObject* colObjB,
        ContactResultCallback& resultCallback);

void btGhostObject::contactTest(
        btCollisionObject* colObj, ContactResultCallback& resultCallback);
void btGhostObject::contactPairTest(
        btCollisionObject* colObjA, btCollisionObject* colObjB,
        ContactResultCallback& resultCallback);

//コールバッククラス例
struct MyContactResultCallback : public btCollisionWorld::ContactResultCallback
{
  //省略
  
  virtual  btScalar addSingleResult(btManifoldPoint& cp,
     const btCollisionObjectWrapper* colObj0Wrap, int partId0, int index0,
     const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1)
  {
    //btManifoldPoint& cp 接触情報
    //btCollisionObjectWrapper コリジョンオブジェクトの情報

    btScalar dist = cp.getDistance();
    // dist > 0の場合接触していない 最短距離
    // dist < 0なら重なっている 侵入距離

    //接触情報使った判定処理や情報収集

    return 0;//戻り値に意味なし 呼ぶ側が使っていない
  }
};

2つの形状の重なりを検出する関数で、重なり具合を距離で表します。正のときは接触せず2つの形状の最短距離、負の時は接触、侵入距離を表します。距離のほかに接触点や接触法線(方向)などが取得できます。contactTestはワールドすべてのオブジェクトと判定、contactPairTestは特定のコリジョン間だけの判定処理になります。


btPairCachingGhostObjectを使った接触判定

btPairCachingGhostObjectはcontactTestを使わず別の方法で接触判定を行います。内部で実行されているものは同じです。

  btPairCachingGhostObject* ghost = ゴースト;
  
  // 境界BOXを更新
  btVector3 minAabb, maxAabb;
  btTransform transform = ghost->getWorldTransform();
      ghost->getCollisionShape()->getAabb(transform, minAabb, maxAabb);
      pCollisionWorld->getBroadphase()->setAabb(ghost->getBroadphaseHandle(),
      minAabb, maxAabb, pCollisionWorld->getDispatcher());

  // 更新した境界BOXで再度接触の可能性のあるオブジェクトを検出
  // OverlappingPairCacheを更新
  pCollisionWorld->getDispatcher()->dispatchAllCollisionPairs( ghost->getOverlappingPairCache(),
           pCollisionWorld->getDispatchInfo(), pCollisionWorld->getDispatcher());

  auto pair_cache = ghost->getOverlappingPairCache();
  int pair_num = pair_cache->getNumOverlappingPairs();
  for(int i=0; i<pair_num; ++i){
    btBroadphasePair* collision_pair = &pair_cache->getOverlappingPairArray()[i];
  
    // 接触判定計算
    btManifoldArray mani;
    if(collision_pair->m_algorithm){
      collision_pair->m_algorithm->getAllContactManifolds(mani);
    }

    // 凸と凹、btCompoundShapeなど接触点が複数になる場合がある
    for(int j=0; j<mani.size(); ++j){
      btPersistentManifold* manifold = mani[j];

      // ペアのどっちが自分か決まっていないので判定
      bool is_A0 = manifold->getBody0()==ghost;

      for(int k=0; k<manifold->getNumContacts(); ++k){
        const btManifoldPoint& pt = manifold->getContactPoint(k);

        // 接触距離
        btScalar dist = pt.getDistance();
        // dist > 0の場合 境界ボックスが重なっただけ 接触していない
  
        // 接触法線
        btVector3 normal = is_A0 ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB;
        
        //接触情報使った判定処理や情報収集
      }
    }
  }


internal edge collision問題

btBvhTriangleMeshShapeなど三角形ポリゴンメッシュで構成されたコリジョン形状で発生する問題(仕様)です。
図のように、平坦な面のポリゴンのつなぎ目の上を通過すると、何かにつまずいたような現象が発生します。これは、btBvhTriangleMeshShapeなどのポリゴンメッシュクラスは、三角形ポリゴンの単なる集合として扱い、共有している辺などの情報を考慮しないために発生します。そのため図のような、通常三角形の法線と同じになるはずの接触法線が異なった方向になってしまい、あたかもそこに段差があるような挙動をしてしまいます。


btAdjustInternalEdgeContactsについて

internal edge collisionは仕様なので修正されることはありませんが、専用の初期化処理を行うことでinternal edge collision問題を回避できるようになっています。btAdjustInternalEdgeContacts関数により、ポリゴンのつなぎ目でも接触法線が三角形の法線と同じになるよう補正してくれます。

#include "BulletCollision/CollisionDispatch/btInternalEdgeUtility.h"

//コールバック関数
bool CustomMaterialCallback(btManifoldPoint& cp,
  const btCollisionObjectWrapper* colObj0Wrap, int partId0, int index0,
  const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1)
{
  btAdjustInternalEdgeContacts(cp, colObj1Wrap, colObj0Wrap, partId1, index1);
  return true;
}


extern ContactAddedCallback    gContactAddedCallback;

//初期化処理
void Init????()
{
  //グローバル変数にコールバック関数ポインタ
  gContactAddedCallback = CustomMaterialCallback;
}


//btBvhTriangleMeshShapeと剛体のの作成
{
  btTriangleMesh* mesh = メッシュ情報;
  
  btBvhTriangleMeshShape* bvh_mesh = new btBvhTriangleMeshShape(mesh, true);

  //補正用情報の生成
  btTriangleInfoMap* info_map = new btTriangleInfoMap;
  btGenerateInternalEdgeInfo(bvh_mesh, info_map);
  
  btRigidBody* rigid = new btRigidBody( bvh_mesh,,,);
  
  // 専用のコールバック呼び出しフラグON
  rigid->setCollisionFlags(rigid->getCollisionFlags()
                   | btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK);
  
  //contactTestなどの判定結果は、btAdjustInternalEdgeContactsで補正された法線情報になる
}

まとめ&次回予告

Bulletのコリジョン判定を利用すればキャラ移動処理が簡単に、とはいきませんが面倒くさい計算をBulletがやってくてるので楽に実装することができると思います。実際、コリジョン判定の使い方がわかっていれば、簡単なキャラ移動処理がすぐに作れます。ただし、複雑なキャラ移動処理になると、Bulletコリジョン判定の挙動を正確に把握する必要があり、それなりの覚悟をしておいてください。本当に大変です。

複雑の地形などの正確なコリジョン判定はBulletのおかげで比較的簡単に実現できますが、正確すぎると角に引っかかったり、操作性がものすごく悪くなります。操作性を良くしたり、壁擦り移動、斜面の滑り落ち、低い段差を無視して移動などゲーム特有の処理を実装するには、物理法則や正確さをわざとなくす必要があります。そうするとコリジョン抜けなどの不具合が発生、それを改善すると操作性が悪くと、不具合の無限ループに陥ります。それができたと喜んでいると今度は、特定の場所でキャラがブルブル振動し始めて、修正すると今度は別の場所でブルブル、修正するとコリジョン抜けが、、、最初に戻る。コリジョンの正確性、操作性、安定性、これらすべて同時に実現するには時間とアイデアと強い精神力が必要です。今回のゲーム仕様はかなり複雑なコリジョン判定になり、不具合の無限ループに何度も心が折れそうになりましたが、何とか完成できそうです。

次回は、今回紹介したコリジョン判定機能を使ってキャラ移動を実装していきます。最終的に作成したキャラ移動の実装方法の説明だけではなく、他にも簡単なキャラ移動の実装方法などいくつか紹介する予定です。

Bulletでゲームを作ってみる

Bulletでゲームを作ってみるPart1 舞台の準備
Bulletでゲームを作ってみるPart2 キャラ移動の準備
Bulletでゲームを作ってみるPart3 キャラの移動

「Bulletでゲームを作ってみるPart2 キャラ移動の準備」への2件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.