ホームゲームつくろー!衝突判定編

2D衝突編
その15 楕円と直線の最短距離


 前章で楕円と点の最短距離を求めました。4次方程式を解いて云々という大変さでした。では楕円と直線の最短距離はというと、なんとこれは点よりもずっと簡単に求まるのです。直線の方が図形として点よりも複雑なのにです。簡単になる理由は「写像」にあります。



@ 楕円を直線に写像する

 今回の考え方を図にしてみました:

 直線Lと楕円がどの位離れているか知りたい(最近接点P0も知りたい)とします。直線Lの方向ベクトルVに垂直な直線をNとします。直線LをNに写像すると、直線は点になってしまいます(赤点)。同様に楕円を直線Nに写像すると、赤い直線になります。図から分かるように、元の直線と楕円の最短距離dと赤い点と赤い直線の最短距離(端点までの距離)は一緒です。ですから、写像した図形で考えると物凄く簡単に最短距離が求まります。

 では、楕円を直線Nに写像した時の赤い線分を求めてみましょう。これは思っているよりずっと簡単です(^-^)。

 楕円は次のように極座標で表現出来ます:

点Pの接線ベクトルは上のPx、Pyをそれぞれ微分すると求まります:

この接線が写像方向になる事が上の図からわかりますね。一方直線の方向ベクトルVに垂直なベクトルNは、VのX成分、Y成分を入れ替えて一方の符号を反転すると求まります:

このNとTは互いに垂直ですから、その内積はゼロになります:

このθを調節すると上手い事ゼロになるはずです。そういうθはというと、次のように展開するとわかります:

 このθからP0もしくはP1点の座標が直接わかります。もう一方の点は、先の図から点対象の位置にある事がわかるのでθ+π…というかP0の符号反転がP1になります。点P0とP1が分ればもう勝ったも同然で、後は直線L上の点Q、P0、P1それぞれをベクトルNへ写像して終わりです。

 直線Nは同じ方向を向いていれば別にどこにあってもかまいません。であれば点Qから伸ばしても問題無い訳です。よって点Qは写像計算するまでもなくなります。点Qを含む直線NへP0、P1を写像するには以下の式を用います:

 標準化したベクトルNとQからP0に伸びるベクトルの内積を取れば、QからNをどれだけ伸ばせばよいか計算出来ます。それをまとめると上式のようになる訳です。

 求めたい最短距離dはQ-P0もしくはQ-P1の距離が短い方となります:

 d0もしくはd1が最短とわかれば、最接近点もP0もしくはP1と機械的に分かります。



A 楕円と直線の最短距離を求める関数

 では楕円と直線の最短距離と最接近点を求める関数です:

//
// 楕円と直線との距離を算出
// xLen : 楕円のX軸方向の長さ
// yLen : 楕円のY軸方向の長さ
// x,y : 直線上の点の座標
// vx, vy: 直線の方向ベクトル
// dist : (出力)最短距離
// px, py: (出力)最近接点の座標
// cx, cy: (出力)直線上の最近接点
bool distEllipseLine( double xLen, double yLen, double x, double y, double vx, double vy, double &dist, double &px, double &py, double &cx, double &cy ) {
    double vd = sqrt( vx * vx + vy * vy );
    if ( vd == 0.0 ) {
        // 直線の方向が定義されていない
        return false;
    }
    vx = vx / vd;
    vy = vy / vd;
    double nx = vy;
    double ny = -vx;
    double th0 = atan2( -vx * yLen, vy * xLen );
    double p0x = xLen * cos( th0 );
    double p0y = yLen * sin( th0 );
    double p1x = -p0x;
    double p1y = -p0y;
    double t0 = nx * ( p0x - x ) + ny * ( p0y - y );
    double t1 = nx * ( p1x - x ) + ny * ( p1y - y );
    double r0x = x + t0 * nx;
    double r0y = y + t0 * ny;
    double r1x = x + t1 * nx;
    double r1y = y + t1 * ny;

    double d0_2 = ( r0x - x ) * ( r0x - x ) + ( r0y - y ) * ( r0y - y );
    double d1_2 = ( r1x - x ) * ( r1x - x ) + ( r1y - y ) * ( r1y - y );

    if ( d0_2 < d1_2 ) {
        // P'0が最接近点
        dist = sqrt( d0_2 );
        px = p0x;
        py = p0y;
        cx = px + ( x - r0x );
        cy = py + ( y - r0y );
    }
    else {
        // P'1が最接近点
        dist = sqrt( d1_2 );
        px = p1x;
        py = p1y;
        cx = px + ( x - r1x );
        cy = py + ( y - r1y );
    }
    return true;
}

 短い(^-^)/
 点との時と比べて涙が出るほど短くて嬉しくなります。点との距離及び直線との距離が分かるとカプセルとの衝突が取れます(めり込んでいる時に距離をマイナスとする処理が必要ですが)。楕円をそのまま使う事はあんまり無いとはいえ、種類が増える事は嬉しい事です。