衝突応答

難解だった・・・。
たった4行のコードの意味を理解するのにずいぶんと時間をかけてしまいました。

OpenGLで作るiPhone SDKゲームプログラミング
第5章 衝突判定と衝突応答 P285

衝突応答理論の説明箇所です。
2次元空間において、静止している円oと、慣性を持つ円cがぶつかったときに、円cを跳ね返らせる処理をどうするか、という問題です。

【書籍から引用】

まずは、ballCircle(ボール)とobstacleCircle(障害物)の中心を結ぶ直線を、ballCircleからobstacleCircleへ向かうベクトルvBallToObstacleとして表します。そして、ボールに働こうとしている力ballPowerがvBallToObstacle方向の成分vPowerVerticalを求めます。この成分を逆方向に2倍してballPowerに足すと、ballPowerが接触面に対して反射した状態となり、その結果、ボールは跳ね返ってきます。

なんのこっちゃ・・・

図は省略しますが、この説明文の下にどのような力の作用が働くのか、図で示されています。
図を見ながら、説明文を5回くらい読んだら、なんとなく、意味がわかったような気にはなれました。

そして、コード例がこちら。

// ボールから障害物に向かうベクトルを求めます
Vector2D vBallToObstacle = obstacleCircle.center - ballCircle.center;  // 1

// 慣性力のvBallToObstacle方向への力を求めます
float dotProduct = ballPower.dotProduct(vBallObstacle);             // 2
Vector2D vPowerVertical = vBallToObstacle * dotProduct / 
                               vBallToObstacle.getSquareLength();   // 3

// 慣性力に対して、接触面から反発する方向に力を加えます
ballPower = ballPower - vPowerVertical * 2.0f;                      // 4

(コード内で①が表示できませんでした。以下、それぞれ①、②、③、④として扱います)

Vector2Dクラスは既に定義されている、一般的な2次元ベクトルクラスです。
dotProductメソッドは、内積を返します。
return x * param.x + y * param.y;

getSquareLengthメソッドは、長さの二乗を返します。
return x * x + y * y;

①と④はそのまま説明文に当てはめることができるのですが、②、③の意味がサッパリでした。

コードを直訳するとつまりこうです。

慣性力と、2つの円の中心を結ぶベクトル(vBallToObstacle)の内積を取る。
vBallToObstacleに内積結果を乗算して、長さの二乗で除算する。
すると、反発する力のベクトルが求まる。

・・・

おかしいやろが!
なんで長さの2乗で割るのかが全く理解できない。



私は、一度サンプルコードを忘れて、自分なりに処理手順を考えてみました。

まず、①のコードは理解できます。
ボールの中心から障害物の中心へ向かうベクトルを求めています。

Vector2D vBallToObstacle = obstacleCircle.center - ballCircle.center;

これはOK

よくわからないのが②で、おそらく、やりたい事としては、
「慣性力(ballPower)がvBallToObstacle方向に働く力を求める」
ということだと思うのです。

だとすれば、内積に使うのは、vBallToObstacleの「単位ベクトル」でなければなりません。
(2つの円の中心点がどれだけ離れているかは関係なく、vBallToObstacleの方向だけを元の慣性力に適用したいため)
よって、このように書き直します。

float dotProduct = ballPower.dotProduct(vBallObstacle.unitVector());

そして、求めたスカラー値をvBallToObstacleの「単位ベクトル」に乗算して反転して2倍してやれば、反発する力が求まるはずです。

Vector2D vPowerVertical = vBallToObstacle.unitVector() * dotProduct * -2.0f;

最後に、元の慣性力に上記の計算結果を足してやれば、衝突応答となります。

ballPower = ballPower + vPowerVertical;


これなら理解できます。
そして計算結果は、書籍のサンプルコードと同じ結果になります。
(場合によってはわずかな誤差が出るかも)

【修正版】
私が思う分かりやすいコード

// ボールから障害物に向かうベクトルを求めます
Vector2D vBallToObstacle = obstacleCircle.center - ballCircle.center;

// 慣性力のvBallToObstacle方向への力を求めます
float dotProduct = ballPower.dotProduct(vBallObstacle.unitVector());

// ボールから障害物方向への慣性力ベクトルを反転して2倍することによって、
// 衝突応答ベクトルを求めます
Vector2D vPowerVertical = vBallToObstacle.unitVector() * dotProduct * -2.0f; 

// 元の慣性力に衝突応答ベクトルを加算します
ballPower = ballPower + vPowerVertical;
  • -

さて、私が修正したコードには、少し計算に無駄があります。

float dotProduct = ballPower.dotProduct(vBallToObstacle.unitVector());
Vector2D vPowerVertical = vBallToObstacle.unitVector() * dotProduct * -2.0f;

このunitVectorというのは単位ベクトルを求めるメソッドなので、具体的には、
return *this / sqrt(x * x + y * y)
を行っています。

コードを短くするために、ballPowerをv1、vBallToObstacleをv2とします。
そして、内積とunitVectorを展開すると、以下のようになります。

float dotProduct =
v1.x * v2.x / sqrt(v2.x * v2.x + v2.y * v2.y) +
v1.y * v2.y / sqrt(v2.x * v2.x + v2.y * v2.y);

Vector2D vPowerVertical =
Vector2D(v2.x / sqrt(v2.x * v2.x + v2.y * v2.y), v2.y / sqrt(v2.x * v2.x + v2.y * v2.y)) *
dotProduct * -2.0f;

dotProduct変数を除去してインライン化すると以下になります。

Vector2D vPowerVertical =
Vector2D(v2.x / sqrt(v2.x * v2.x + v2.y * v2.y), v2.y / sqrt(v2.x * v2.x + v2.y * v2.y)) *
(v1.x * v2.x / sqrt(v2.x * v2.x + v2.y * v2.y) + v1.y * v2.y / sqrt(v2.x * v2.x + v2.y * v2.y))
* -2.0f;

少し式を変形させて、平方根の除算の意味が変わらないように外に出すと、

Vector2D vPowerVertical =
v2 / sqrt(v2.x * v2.x + v2.y * v2.y) *
(v1.x * v2.x + v1.y * v2.y) / sqrt(v2.x * v2.x + v2.y * v2.y)
* -2.0f;

とすることができます。

一連の乗除算の中で、同じ値の平方根で2回除算しています。
ある値の平方根で2回除算することは、値そのもので1回除算することと同じです。
最適化すると以下のようになります。

Vector2D vPowerVertical =
v2 *
(v1.x * v2.x + v1.y * v2.y) / (v2.x * v2.x + v2.y * v2.y)
* -2.0f;

ここで、v1とv2を元の名前に戻します。
更に、内積計算メソッドをdotProduct, 長さの二乗を求めるメソッドをgetSquareLengthに戻します。
マイナス2倍の計算を後回しにすると、

Vector2D vPowerVertical = vBallToObstacle *
ballPower.dotProduct(vBallObstacle) /
vBallToObstacle.getSquareLength();

ここまで来れば一目瞭然ですね。
ようやく理解できました。
最初の②、③の処理は最初から最適化してあったのです。


数学に強い人なら、一読してここまで読み取れるのかもしれませんが、私にはかなり難解でした。