ホームゲームつくろー!衝突判定編< 球が平面と衝突する場所と時刻を得る

3D衝突編
その10 球が平面と衝突する場所と時刻を得る



 その9で2つのパーティクルが衝突するまでの時刻と場所を取得してみました。同様の考えは平面とパーティクルにも言えます。パーティクルが次の位置に移動しようとする時、その間に壁があれば当然跳ね返るわけです。これを実現するためには、いつパーティクルが平面と衝突するか、そしてそれはどこなのかを調べる必要があります。

 この章では、任意の平面とパーティクルとの衝突を見ていくことにしましょう。



@ 無限に広がる平面とパーティクルの衝突

 ここで考えるのは、空間を真っ二つに分ける無限遠の平面とパーティクルとの衝突です。これにより「パーティクルが平面に衝突するか否か?」そして「衝突するならいつどこか?」という2つの判定が行えます。

 無限遠に広がる平面は、法線と平面を通る1点のみで記述できます。今、標準化した法線をベクトルN、平面上の1点をAとしましょう。パーティクルは今の位置、そして次に進むだろう予定位置を持ちます。今の位置を時刻t=0、そして次の予定位置を時刻t=1デ表わすとしまして、それをP(t=0)およびP(t=1)と表記することにします。具体的なイメージ図は以下の通りです。

 平面上の点Aからパーティクルの位置P(t)に伸ばしたベクトルをc(t)としておきます。そして、時刻tにおけるパーティクルと平面との距離をa(t)と表わすことにします。パーティクルの位置はその中心の座標です。それがある方向に進んで平面に衝突する。もし、パーティクルに半径がなければ中心点は平面に含まれることになりますが、パーティクルには厚みがありますから、実際は上の図のように、高さrだけ余裕が出ることになります。これをどう組み入れるかがポイントです。と言っても難しいことは何もありません。

 まず、a(t)+rを求めてみることにしましょう。これは、中心点と平面の距離に相当します。この求め方は「点と平面の距離」というトピックですでに紹介しておりますが、法線とベクトルcの内積となります。すなわち、

 a(t)+r = Dot( c(t), N ) / |N| = Dot( c(t), N )

です(法線は標準化されているとします)。内積1回で距離が出るのですからお手軽です。ここから、

 a(t) = Dot( c(t), N ) - r

と表わすことが出来ます。a(t)というのは上の図から平面とパーティクルが接するまでの中心点の距離に相当します。この値が0の時、パーティクルは平面に接していると言うわけです。しかも、この距離は時間で決まります。ですから、後はtについて解いてみれば良いわけです。

 右辺を丁寧に紐解くとこうなります。

ベクトルcは(Px(t),Py(t),Pz(t))-(Az,Ay,Az)でして、またP(t) = P(0) + t*(P(1)-P(0))という線形の式で表わせますから、これを上式に全部代入すると、

となります。途中経過は単なる展開ですから無視しても結構です。結果だけご覧下さい。これを見るとパーティクルが平面に接するまでの距離a(t)というのは、2つの内積の和であることがわかります。

 今欲しいのはパーティクルが接する時刻tですから、a(t)=0としてこの式を解きます。

これが、パーティクルが平面に接するまでの時刻です。時刻tが求まれば、平面に接する時に中心点の位置はP(0)+tdと簡単に算出できます。



A 時刻算出式の性質

 上で求めた時刻算出式の性質を知っておきましょう。まず、分母に内積があります。これは、パーティクルの進行方向と法線との関係を表わします。もしDot(d,N)<0であれば、パーティクルは平面に向かって進んでいることになります。内積が0の時、パーティクルは平面と平行に進んでいて、交わることはありません。内積がプラスであれば、パーティクルは平面から遠ざかる方向に進んでいることになります(もしくは平面の裏から表に抜けようとしている)。このことから、パーティクルの衝突を感知するためには、Dot(d,N)<0の時だけを調べれば良い事がわかります。

 平面に向かってパーティクルが進んでいる場合でも、次の到達地点までにはまだ平面に触れないこともあります。その時はt>1になります。



B 壁にめり込んでいるパーティクルの処理

 パーティクルが位置P(t=0)ですでに壁にめり込んでいる場合は特別処理をする必要があります。これにはいくつかルールがあるようです。例えば、めり込んでいる場合は壁の法線方向に押し出す、動こうとする方向に対して”過去の時間”へ押し戻す、などです。今回は衝突までの時刻を知らせるという条件で話を進めてきていますので、後者のルールを採用する事にします。

 後者のルールの性質ですが、壁にめり込んでいてさらにめり込もうという方向に動く場合は「マイナス時間」として算出されてきます。一方、壁にめり込んでいて抜け出そうと言う方向に動く場合はプラス時間となります。これはいずれも「壁の正面に対する衝突の瞬間位置」を求めるのに重宝します。裏面は考えませんので注意してください。

 ただし、ここには「スペシャルケース」があります。平面にめり込んでいて、平面と平行に移動しようとするパーティクルは、どこまで行ってもめり込んでいるため衝突時刻を算出できません。ですから消極的なルールを決めます。時刻は永久に抜け出せない状態なので最大時間を返すことにします。衝突位置も算出できないため、現在の位置を返すようにします。消極的ですが仕方ありません。



C 平面パーティクル衝突時刻位置算出関数

 以上を踏まえて、平面とパーティクルの衝突時刻と位置を算出する関数を実装すると次のようになります。この関数はコピペするとすぐに使用できます

平面パーティクル衝突時刻位置算出関数
#include <math.h>

#define IKD_EPSIRON 0.00001f // 誤差

///////////////////////////////////////////////////
// 平面パーティクル衝突判定・時刻・位置算出関数
// r : パーティクルの半径
// pPre_pos : パーティクルの前の位置
// pPos : パーティクルの次の到達位置
// pNormal : 平面の法線
// pPlane_pos : 平面上の1点
// pOut_t : 衝突時間を格納するFLOAT型へのポインタ
// pOut_colli : パーティクルの衝突位置を格納するD3DXVECTOR型へのポインタ
// 戻り値 : 衝突(true), 非衝突(false)

bool IKD::CalcParticlePlaneCollision(
   FLOAT r,
   D3DXVECTOR3 *pPre_pos, D3DXVECTOR3 *pPos,
   D3DXVECTOR3 *pNormal, D3DXVECTOR3 *pPlane_pos,
   FLOAT *t,
   D3DXVECTOR3 *pOut_colli
)
{
   D3DXVECTOR3 C0 = *pPre_pos - *pPlane_pos; // 平面上の一点から現在位置へのベクトル
   D3DXVECTOR3 D = *pPos - *pPre_pos; // 現在位置から予定位置までのベクトル
   D3DXVECTOR3 N; // 法線
   D3DXVec3Normalize(&N, pNormal); // 法線を標準化

   // 平面と中心点の距離を算出
   FLOAT Dot_C0 = D3DXVec3Dot( &C0, &N );
   FLOAT dist_plane_to_point = fabs( Dot_C0 );

   // 進行方向と法線の関係をチェック
   FLOAT Dot = D3DXVec3Dot( &D, &N );

   // 平面と平行に移動してめり込んでいるスペシャルケース
   if( (IKD_EPSIRON-fabs(Dot) > 0.0f) && (dist_plane_to_point < r) ){
      // 一生抜け出せないので最大時刻を返す
      *t = FLT_MAX;
      // 衝突位置は仕方ないので今の位置を指定
      *pOut_colli = *pPre_pos;
      return true;
   }

   // 交差時間の算出
   *t = ( r - Dot_C0 )/Dot;

   // 衝突位置の算出
   *pOut_colli = *pPre_pos + (*t) * D;

   // めり込んでいたら衝突として処理終了
   if ( dist_plane_to_point < r )
      return true;

   // 壁に対して移動が逆向きなら衝突していない
   if( Dot >= 0 )
      return false;

   // 時間が0〜1の間にあれば衝突
   if( (0 <= *t) && (*t <= 1) )
      return true;

   return false;
}


 この関数で平面とパーティクルの衝突位置と時刻を求めることが出来ます。このパーティクルがどう反射するかは、平面の性質やパーティクルの重量などが関連しますので、別のお話になります。反射が揃いますと、例えば複数の平面で囲まれた領域(箱のようなものなど)内でパーティクルを跳ね回らせることができるようになります。ただ、これを基本として「ポリゴンとパーティクルの衝突」に発展させると、広く応用が利くようになります。



D 謝辞

 ソースに関するバグ報告を頂きましたMONOさんに感謝申し上げます。