衝突応答
難解だった・・・。
たった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();
ここまで来れば一目瞭然ですね。
ようやく理解できました。
最初の②、③の処理は最初から最適化してあったのです。
数学に強い人なら、一読してここまで読み取れるのかもしれませんが、私にはかなり難解でした。