ホームゲームつくろー!衝突判定編< 壁にめり込んだOBBを戻す

3D衝突編
その14 壁にめり込んだOBBを戻す


 RPGのキャラクタの衝突境界をOBB(有向境界ボックス)で判定している時、ビルや家などの壁に衝突した後でめり込んだ分を戻す作業が必要になる事があります。戻す方向は壁の法線だろうと思いますが、問題は戻す距離です。OBBですから、壁に対して斜めに突入するのが通常だと考えますと、8つある角のどこかがめり込んでいる可能性があります。上方向が揃っている場合は、辺や面がめり込むことにもなります。いずれの場合でも、これは「OBBと平面の最近接距離」を求める問題に帰着します。

 この章では、OBBと平面の衝突についてご紹介します。尚この章の内容は「ゲームプログラミングのためのリアルタイム衝突判定(Christer Ericson著作 中村達也翻訳)」を参照しています。



@ 平面の法線が分離軸

 私たちが地面に触れているか空中にまだいるかは、足元に空間があるか無いかで簡単に分かります。その空間を見るためには、地面にべたっと寝そべって、地平線を眺めるようにすれば良いですよね。この段階で、私たちは3Dのオブジェクトを2Dの視点で見ています。

3D視点 2D視点


 2D平面となった上の直方体は、さらに地面に平行な軸(法線)に射影する事で、1次元となってしまいます。

1次元に射影

 感覚的にも分かると思いますが、縦方向の法線上の赤い線と点が離れていますから、地面とOBBは衝突していません。逆に、赤い射影線に地面の射影点が含まれていれば、OBBは地面と衝突している事になります。つまり、「平面(地面)の法線は分離軸」になることがわかります。



A 分離軸判定

 では、具体的な計算方法に移りましょう。まず、OBBはど真ん中(対角線の交点)に制御点Cがあるとします。そして、OBBの各軸方向を表す軸方向ベクトルをuxuyuz、各軸ベクトルのスケールをex、ey、ezとします。図にするとこ次のような感じです。


 考えやすいように2Dにしてあります。平面が壁のように垂直に立っている状態です。求めたいのは下にある赤い線の長さと青い線の長さです。

 まずは赤い線から行きましょう。この線の長さはOBBの中心点Cからその頂点に引いた線を法線に射影した長さです。「射影線の長さ」というのは内積の計算で簡単に求まります。OBBの中心点Cから各頂点に伸ばしたベクトルを法線に射影した時の長さrは、

という式で求まります。±が付いているのは、各頂点を表すためです。つまり、射影線の長さは最大4つあることになります。内積というのは分解が可能な性質を持っています。上の式をちょっと分解しますと、次のようになります:

 さて、こう分解しますと、例えばベクトルuxと法線の内積は、同じ値で符号違いになる事がわかります。そこで上の図の緑の射影線を見て頂きたいのですが、これは2つある対角線の長い方の射影線です。それは上の式で最大となるrを求める事と同じ意味です。rを最大限の長さにするには、各内積の答えの絶対値を取ればいいんです。つまり、次のようにすると、最大の射影線の長さ(上図の赤い線)が算出できます:

あ、ちなみに、法線は正規化しておく必要があります。

 これで赤い線の長さrは計算できました。次はOBBの中心点Cと平面までの距離s(青い線の長さ)です。これは非常に簡単でして、やはり内積を用います:

ベクトルPCと法線の内積で一発です。

ところで、点Pの位置ってどこなんでしょうか?実はこれは平面上の点であればどこでも良いんです。一般に平面は、法線ベクトルと平面上の一点で定義されます



B 衝突判定とめり込みを戻す距離の算出

 Aまでの段階で衝突を判定するのは極めて簡単ですよね。OBBの射影線である長さrが平面までの距離であるsよりも短ければ衝突していません。逆なら衝突しています。これだけでもかなり役に立ちます。

 衝突がわかったとして、壁にぴったりくっつくように戻す作業を追加する時には、ちょっと考えなければならない事があります。


上の図で平面にOBBがちょっとだけめり込んでいます。目で見て分かるように、この戻し分は下の黄色い線で表した長さです。具体的には(r-s)になります。ところが、次の似たような状況をご覧下さい:


 これは随分とめり込んでしまって、中心点Cもめり込んでいます。さてこの時の戻し分は上の黄色い線なのですが、先ほどのようにr-sにするとこの長さにはなりません。黄色い線の長さを求めるには「rの長さの2倍(緑の線の長さ)からr-sを引く」必要があります。少し整理すると、

 2*r-(r-s) = r + s

となり、rに青い線の長さsを足せばよい事になります。つまり、状況によって戻し分の長さの計算方法が変わってしまうと言う事です。

 (r-s)なのか(r+s)なのか?これを判断するには、法線とベクトルPCの内積の符号を見ます。2つ前の図では、法線nとベクトルPCの内積はプラスになりそうです(鋭角です)。上の図だとマイナスですよね。これで簡単に判定ができます。



C 壁摺り位置補正関数

 以上で壁にめり込んだOBBを壁にぴったり這う位置に戻す処理が実現できます。実装を公開します。

OBBと平面の衝突判定関数
// OBB vs Plane
bool OBBvsPlane( OBB &obb, PLANE &plane, FLOAT *Len=NULL )
{
   // 平面の法線に対するOBBの射影線の長さを算出
   FLOAT r = 0.0f;          // 近接距離
   D3DXVECTOR3 PlaneNormal; // 平面の法線ベクトル
   plane.GetNormal_W( &PlaneNormal );
   int i;
   for(i=0; i<3; i++){
      D3DXVECTOR3 Direct = obb.GetDirect(i); // OBBの1つの軸ベクトル
      r += fabs(D3DXVec3Dot( &(Direct * obb.GetLen_W(i)), &PlaneNormal ));
   }

   // 平面とOBBの距離を算出
   D3DXVECTOR3 ObbPos = obb.GetPos_W();
   D3DXVECTOR3 PlanePos = plane.GetPos_W();
   FLOAT s = D3DXVec3Dot( &(ObbPos-PlanePos), &PlaneNormal );

   // 戻し距離を算出
   if( Len != NULL ){
   if(s>0)
      *Len = r - fabs(s);
   else
      *Len = r + fabs(s);
   }

   // 衝突判定
   if( fabs(s)-r < 0.0f )
      return true; // 衝突している

   return false; // 衝突していない
}


 この関数は引数にあるOBB(有向境界ボックス)およびPLANE(平面)という形状クラスを設定すると完全に使えます。この内OBBは衝突判定編その13で示したOBBクラスと同じです。PLANEクラスの宣言部は次のようになります。

PLANEクラス
class PLANE : public POINT
{
   protected:
   D3DXVECTOR3 m_LocalNormal; // ローカル座標での法線ベクトル
   D3DXVECTOR3 m_Pos;         // 平面の位置

public:
   PLANE()
   {
      m_LocalNormal.x =0.0f;
      m_LocalNormal.y =0.0f;
      m_LocalNormal.z =1.0f;
   }
   // ローカル座標での法線ベクトルを設定
   bool SetNormal_L( D3DXVECTOR3 *Norm );
   // ローカル座標での法線ベクトルを取得
   void GetNormal_L( D3DXVECTOR3 *Norm );
   // ワールド座標での法線ベクトルを取得
   void GetNormal_W( D3DXVECTOR3 *Norm );
   // ローカル位置を設定
   void SetPos_L( FLOAT x, FLOAT y, FLOAT z);
   void SetPos_L( D3DXVECTOR3 *Pos );
   // ローカル座標位置を取得
   D3DXVECTOR3 GetPos_L();
   // ワールド座標位置を取得
   D3DXVECTOR3 GetPos_W();
};

 ローカル座標とワールド座標に分けているのは、境界図形の使用座標と取得時の座標系が通常異なるためです。それぞれのメソッドの意味はコメントの通りで、難しい点は1つもありません。

 OBBvsPlane関数にそれぞれの境界図形を渡し、第3引数に有効なFLOAT型のポインタを渡すと、衝突判定と同時に戻し距離を算出してくれます。後は、めり込んだ位置から平面の法線方向に戻し距離だけオフセットするだけで、壁摺りが実現されます(たぶん(^-^;)。



D 謝辞

 この記事を書くきっかけを与えて下さいましたKoichiさんに感謝申し上げます。