Stack Overflowコミュニティの助けを借りて、かなり基本的ではあるが楽しい物理シミュレーターを作成しました。
マウスをクリックしてドラッグし、ボールを起動します。それは跳ね返り、最終的には「床」で止まります。
追加したい次の大きな機能は、ボール同士の衝突です。ボールの動きは、xとyの速度ベクトルに分割されます。重力(各ステップでyベクトルの小さな減少)があり、摩擦(壁との衝突ごとに両方のベクトルの小さな減少)があります。ボールは、驚くほど現実的な方法で正直に動き回ります。
私の質問には2つの部分があると思います。
「壁」の衝突検出と、結果として生じるベクトルの変更の処理は簡単でしたが、ボールとボールの衝突にはさらに複雑な問題があります。壁の場合、適切なxまたはyベクトルの負の値を取得するだけで、正しい方向に進みます。ボールについては、そうは思いません。
いくつかの簡単な説明:簡単にするために、今のところ完全に弾力性のある衝突で大丈夫です。また、すべてのボールの質量は現在同じですが、将来的に変更する可能性があります。
編集:役に立つとわかったリソース
ベクトルを使用した2Dボール物理学: Trigonometry.pdfなしの2次元衝突
2dボールの衝突検出の例: 衝突検出の追加
私はボール衝突の検出と応答が非常にうまく機能しています!
関連コード:
衝突検出:
for (int i = 0; i < ballCount; i++)
{
for (int j = i + 1; j < ballCount; j++)
{
if (balls[i].colliding(balls[j]))
{
balls[i].resolveCollision(balls[j]);
}
}
}
これにより、すべてのボール間の衝突はチェックされますが、冗長チェックはスキップされます(ボール1がボール2と衝突するかどうかをチェックする必要がある場合、ボール2がボール1と衝突するかどうかをチェックする必要はありません。また、それ自体との衝突のチェックもスキップします)。
次に、ボールクラスにcolliding()およびresolveCollision()メソッドがあります。
public boolean colliding(Ball ball)
{
float xd = position.getX() - ball.position.getX();
float yd = position.getY() - ball.position.getY();
float sumRadius = getRadius() + ball.getRadius();
float sqrRadius = sumRadius * sumRadius;
float distSqr = (xd * xd) + (yd * yd);
if (distSqr <= sqrRadius)
{
return true;
}
return false;
}
public void resolveCollision(Ball ball)
{
// get the mtd
Vector2d delta = (position.subtract(ball.position));
float d = delta.getLength();
// minimum translation distance to Push balls apart after intersecting
Vector2d mtd = delta.multiply(((getRadius() + ball.getRadius())-d)/d);
// resolve intersection --
// inverse mass quantities
float im1 = 1 / getMass();
float im2 = 1 / ball.getMass();
// Push-pull them apart based off their mass
position = position.add(mtd.multiply(im1 / (im1 + im2)));
ball.position = ball.position.subtract(mtd.multiply(im2 / (im1 + im2)));
// impact speed
Vector2d v = (this.velocity.subtract(ball.velocity));
float vn = v.dot(mtd.normalize());
// sphere intersecting but moving away from each other already
if (vn > 0.0f) return;
// collision impulse
float i = (-(1.0f + Constants.restitution) * vn) / (im1 + im2);
Vector2d impulse = mtd.normalize().multiply(i);
// change in momentum
this.velocity = this.velocity.add(impulse.multiply(im1));
ball.velocity = ball.velocity.subtract(impulse.multiply(im2));
}
ソースコード: ball to ball colliderの完全なソース
この基本的な物理シミュレータを改善する方法について何か提案があれば教えてください!まだ追加していないことの1つは、angular運動量です。これにより、ボールがよりリアルに転がります。他の提案はありますか?コメントを残す!
2つのボールが衝突するかどうかを検出するには、中心間の距離が半径の2倍未満かどうかを確認します。ボール間で完全に弾力性のある衝突を行うには、衝突の方向にある速度の成分のみを考慮する必要があります。他のコンポーネント(衝突の接線)は、両方のボールで同じままです。衝突コンポーネントを取得するには、1つのボールから他のボールへの方向を指す単位ベクトルを作成し、ボールの速度ベクトルとの内積を取ります。その後、これらのコンポーネントを1Dの完全弾性衝突方程式にプラグインできます。
ウィキペディアにはかなり良いものがあります プロセス全体の要約 。任意の質量のボールの場合、新しい速度は式を使用して計算できます(v1とv2は衝突後の速度、u1、u2は前からの速度です)。
ボールの質量が同じ場合、速度は単純に切り替えられます。同様のことを行うコードをいくつか示します。
void Simulation::collide(Storage::Iterator a, Storage::Iterator b)
{
// Check whether there actually was a collision
if (a == b)
return;
Vector collision = a.position() - b.position();
double distance = collision.length();
if (distance == 0.0) { // hack to avoid div by zero
collision = Vector(1.0, 0.0);
distance = 1.0;
}
if (distance > 1.0)
return;
// Get the components of the velocity vectors which are parallel to the collision.
// The perpendicular component remains the same for both fish
collision = collision / distance;
double aci = a.velocity().dot(collision);
double bci = b.velocity().dot(collision);
// Solve for the new velocities using the 1-dimensional elastic collision equations.
// Turns out it's really simple when the masses are the same.
double acf = bci;
double bcf = aci;
// Replace the collision velocity components with the new ones
a.velocity() += (acf - aci) * collision;
b.velocity() += (bcf - bci) * collision;
}
効率については、Ryan Foxが正しいです。リージョンをセクションに分割し、各セクション内で衝突検出を行うことを検討する必要があります。ボールはセクションの境界で他のボールと衝突する可能性があるため、コードが非常に複雑になる可能性があることに注意してください。ただし、数百個のボールを取得するまで、効率はおそらく重要ではありません。ボーナスポイントの場合、各セクションを異なるコアで実行するか、各セクション内の衝突の処理を分割できます。
さて、何年も前に、私はあなたがここで紹介したようなプログラムを作りました。
1つの隠れた問題があります(または多くは、視点によって異なります)。
また、ほぼ100%の場合、新しい速度が間違っています。まあ、速度ではなく、位置。新しい速度を計算する必要があります正確に正しい場所で。それ以外の場合は、前の個別のステップで得られたわずかな「エラー」量でボールをシフトします。
解決策は明らかです。タイムステップを分割する必要があります。そのため、まず正しい場所に移動し、次に衝突してから、残りの時間を移動します。
この問題を解決するには、スペースパーティションを使用する必要があります。
Binary Space Partitioning および Quadtrees を参照してください
画面を領域に分割し、領域内の衝突のみをチェックするというRyan Foxの提案の明確化として...
例えばプレイエリアを正方形のグリッドに分割し(任意に各辺が1単位の長さであると言います)、各グリッドの正方形内で衝突をチェックします。
それは絶対に正しい解決策です。 (別のポスターが指摘したように)それに関する唯一の問題は、境界を越えた衝突が問題であることです。
これに対する解決策は、最初のグリッドに対して0.5単位の垂直および水平オフセットで2番目のグリッドをオーバーレイすることです。
次に、最初のグリッドの境界を越える(したがって検出されない)衝突は、2番目のグリッドのグリッド正方形内にあります。すでに処理した衝突を追跡している限り(ある程度重複している可能性が高いため)、Edgeケースの処理について心配する必要はありません。すべての衝突は、いずれかのグリッド上のグリッドの正方形内にあります。
衝突チェックの回数を減らす良い方法は、画面を異なるセクションに分割することです。次に、各ボールを同じセクションのボールとのみ比較します。
最適化するためにここに表示される1つのこと。
距離が半径の合計であるときにボールがヒットすることに同意しますが、実際にこの距離を計算することはありません!むしろ、平方を計算し、そのように操作します。その高価な平方根演算の理由はありません。
また、衝突が見つかったら、衝突がなくなるまで衝突を評価し続ける必要があります。問題は、最初のものが正確な画像を取得する前に解決する必要がある他の人を引き起こす可能性があることです。ボールが端でボールに当たった場合どうなるか考えてみてください。 2番目のボールはエッジに当たり、すぐに最初のボールに跳ね返ります。コーナーでボールの山にぶつかった場合、次のサイクルを繰り返す前に解決する必要があるかなりの数の衝突が発生する可能性があります。
O(n ^ 2)に関しては、ミスしたものを拒否するコストを最小限に抑えることができます。
1)動いていないボールは何も打てません。床に適度な数のボールが横たわっている場合、これにより多くのテストを節約できます。 (静止したボールに何かが当たったかどうかを確認する必要があることに注意してください。)
2)やりがいのあること:画面をいくつかのゾーンに分割しますが、線はぼやけているはずです-ゾーンの端にあるボールは、すべての関連する(4つになる可能性がある)ゾーンにあるとしてリストされます。 4x4グリッドを使用して、ゾーンをビットとして保存します。 2つのボールゾーンのゾーンのANDがゼロを返す場合、テストの終了。
3)私が言ったように、平方根をしないでください。
2Dでの衝突検出と応答に関する情報が記載された素晴らしいページを見つけました。
http://www.metanetsoftware.com/technique.html
彼らはそれがどのように行われるかを学術的な観点から説明しようとします。それらは単純なオブジェクト間の衝突検出から始まり、衝突応答とそれを拡大する方法に進みます。
編集:リンクを更新
これを行うには、2つの簡単な方法があります。ジェイはボールの中心からチェックする正確な方法をカバーしました。
より簡単な方法は、長方形の境界ボックスを使用して、ボックスのサイズをボールのサイズの80%に設定することです。そうすれば、衝突をかなりうまくシミュレートできます。
ボールクラスにメソッドを追加します。
public Rectangle getBoundingRect()
{
int ballHeight = (int)Ball.Height * 0.80f;
int ballWidth = (int)Ball.Width * 0.80f;
int x = Ball.X - ballWidth / 2;
int y = Ball.Y - ballHeight / 2;
return new Rectangle(x,y,ballHeight,ballWidth);
}
次に、ループ内で:
// Checks every ball against every other ball.
// For best results, split it into quadrants like Ryan suggested.
// I didn't do that for simplicity here.
for (int i = 0; i < balls.count; i++)
{
Rectangle r1 = balls[i].getBoundingRect();
for (int k = 0; k < balls.count; k++)
{
if (balls[i] != balls[k])
{
Rectangle r2 = balls[k].getBoundingRect();
if (r1.Intersects(r2))
{
// balls[i] collided with balls[k]
}
}
}
}
この KineticModel
は、Javaでの cited アプローチの実装です。
それはあちこちでほのめかされていますが、オーバーラップの境界ボックスを比較するなど、より高速な計算を最初に実行することもできます。
加算/差分の計算は、半径のすべてのトリガーよりもバウンディングボックスの方がはるかに高速であり、ほとんどの場合、バウンディングボックステストは衝突の可能性を排除します。しかし、その後、trigで再テストすると、求めている正確な結果が得られます。
はい、2つのテストですが、全体的に高速になります。
HTML Canvas要素を使用してJavaScriptでこのコードを実装し、1秒あたり60フレームで素晴らしいシミュレーションを生成しました。ランダムな位置と速度で多数のボールを集めてシミュレーションを開始しました。高速では、小さなボールとはるかに大きなボールとのかすかな衝突により、小さなボールが大きなボールのエッジにSTICKのように見え、最大で約90に移動することがわかりました。分離する前の大きなボールの周囲の角度。 (誰かがこの動作を観察したのだろうか?)
計算の記録から、これらの場合の最小平行移動距離は、次のタイムステップで同じボールが衝突するのを防ぐほど大きくないことが示されました。いくつかの実験を行ったところ、相対速度に基づいてMTDを拡大することでこの問題を解決できることがわかりました。
dot_velocity = ball_1.velocity.dot(ball_2.velocity);
mtd_factor = 1. + 0.5 * Math.abs(dot_velocity * Math.sin(collision_angle));
mtd.multplyScalar(mtd_factor);
この修正の前後に、衝突ごとに総運動エネルギーが保存されることを確認しました。 mtd_factorの0.5の値は、衝突後にボールが常に分離することがわかっているおおよその最小値です。
この修正により、システムの正確な物理にわずかな誤差が生じますが、トレードオフは、非常に高速なボールをタイムステップサイズを減少させずにブラウザーでシミュレートできることです。