35

画面上でボールを跳ね返せる単純な物理モデラーを作成しました。クリック アンド ドラッグしてボールを発射したり、一度に数百個のボールを生成して相互に作用する様子を観察したりできます。

代替テキスト
[拡大版へのリンク]

取り組むのは楽しい小さなプログラムでした。できればさらに進めたいと思っています。時期尚早の最適化がすべての悪の根源であると彼らが言うことは知っていますが、実際のパフォーマンスの障壁にぶつかり始めています。ゲーム/シミュレーター開発の経験がある人が助けてくれるかどうか知りたいです.

問題:

現時点では、ボールを追加しすぎるとプログラムが停止します (私のマシンでは 800 個を超えるボールを処理できないようです)。実行すると、シミュレーションが現実的ではなくなり、すべてのボールが下部で重なり合ってしまいます。

問題は衝突検出にあります。最も素朴なケースでは、衝突検出は O(N^2) 問題です。各ボールは、他のすべてのボールをチェックします。これにより、すぐにパフォーマンスが低下します (100 個のボールの後でも、ループ サイクルごとに 10,000 の衝突チェックを行うことになります)。

ここを見ると、数百個のボールを追加したスクリーンショットを見ることができます。シミュレーターが追いつかなくなり、お互いに重なり始めます。

代替テキスト
[拡大版へのリンク]

現在、重なっているボールを探すことで衝突を検出しています。重なっている 2 つのボールを見つけた場合は、それらを最小移動距離 (MTD) で離すか、押し離します。次に、単純な物理方程式を使用してインパルス ベクトルを調整し、衝突後に異なる方向に進みます。

ボールが多すぎる場合に最小移動距離が顕著になることを除いて、これはうまく機能します。それらは大量に重なり始め、常に底で互いに押し合います。「重力」を増やせば増やすほど悪くなる。それらへの圧力が増加し、それらが互いに圧縮/重なり合う量が増加します。

繰り返しますが、かなりの N 個のボールを打つまで問題はありません。

現在の最適化方法:

衝突検出 -
スイープとプルーニング- (別名、ソートとスイープ)

x軸に沿ってループサイクルごとにボールに挿入ソートを使用します。挿入ソートの性質により、シミュレーターの一時的な一貫性を活用できます。フレームごとに、ボールの位置がわずかに変化するだけなので、並べ替える作業はそれほど多くありません。これにより、線形ソートの償却された実行時間が O(N^2) の平均実行時間ではなく、O(N) または線形になります。

ボールがソートされているので、衝突をチェックする前に、2 番目のループでいくつかの事前チェックを行います。これで、互いに近くにあるボールをチェックするだけで済み (x 軸に沿ってソートされているため)、xmin が現在のボールの xmax より大きい別のボールに対してボールをチェックするたびに、2 番目のループから抜け出します。 . したがって、何千ものチェックをスキップします。

これを実装したところ、速度が大幅に向上しました。それでも600~800球以上は扱えるようにしたいです。10,000 個のオブジェクト間の衝突検出を同時に簡単に処理できる物理エンジンについて読んだことがあります。

プロファイラーを実行したところ、衝突検出に約 55% の時間が費やされ、レンダリングに約 45% の時間が費やされていることがわかりました。したがって、これらは私の 2 つの最も高価なコストです。


質問:

シミュレーターがより多くのボールを処理できるようにするための、より優れたアルゴリズムまたは手法を思いつきますか?


関連コード:

プロジェクト全体:

svn チェックアウトhttp://simucal-projects.googlecode.com/svn/ballbounce/trunk/

または、ここをクリックて、ブラウザでファイルを手動で参照します。

関心のあるセクション:


4

13 に答える 13

14

簡単な方法は、オブジェクト対オブジェクトの衝突をテストせず、各ボールの中心点を配列に入力することです。次に、各中心から、その点を中心とするサイズ 4*radius の正方形をスキャンします (コードをより複雑にすることを犠牲にして、正方形を使用しないことでこれを少し最適化できます)。この正方形に別の中心がある場合にのみ、それらが互いに半径 2 倍以内にあるかどうか (したがって衝突しているかどうか) を確認します。解像度を下げてボールの位置を丸めることで、これをさらに最適化し、実行する必要のあるチェックの数を減らすことができます。

もう 1 つの方法は、スペースをグリッドに分割し、オブジェクトをグリッド領域に格納することです。隣接するグリッド内のオブジェクト間の衝突を確認するだけで済みます。

于 2009-02-24T02:01:11.167 に答える
10

近くのボールを追跡する-

フレームごとの変更が最小限であるため挿入ソートが最適であるのと同じように、同じプロパティを使用してトラックボールの「隣接」を維持できます。

各ボールは、最大で6つの他のボールとしか相互作用できません。おそらく、10フレームごとにアルゴリズムを実行して、各ボールがどの隣接ボールを持っているかを把握し、その情報を次の10フレームで使用して、実際の衝突を計算できます。

また、各ボールのベクトルをたどり、仮想線を描画し、次の3〜5フレームでどの線が交差するかを確認し、発生する可能性のある衝突のみを計算することもできます(時間の関係で発生することはほとんどありません)。

x軸で並べ替えたのと同じように、メインウィンドウ内のサブディビジョンにボールを「グループ化」できます。ボールが少なくとも1つのボールの直径でサブディビジョン内にある場合、同じサブディビジョン内のボールを見るだけで済みます。境界線または2つに近い場合は、他の1つまたは2つのサブディビジョンを調べる必要がありますが、計算は少なくなるはずです。

さらに、これらのサブディビジョンは動的に配置できるため、平均的なサブディビジョンには100個のボールしかありません。同じサイズで、ボールの数が異なるサブディビジョンを持つ必要はありません。

サブディビジョンの混雑度(1平方単位あたりのボール数)に応じて、さまざまなアルゴリズムを試すことができます。まばらに塗りつぶされたボックスは、衝突を予測するためにベクトル計算のみを必要としますが、密集したサブディビジョンは、ある種の最適化されたヘキサグラフィック計算のみを必要とします。スパースボックスは、多くのフレームで衝突検出を必要としない場合があります(密集したボックスほどオーバーラップが認識されないため)。

特定のボックスのエネルギー密度を決定できます。低エネルギーのボールを含む非常にスパースなボックスは、高エネルギーのスパースボックスよりも衝突計算が少なくて済みます。

-アダム

于 2009-02-24T00:53:43.263 に答える
4

主な問題は、衝突解決アルゴリズムです。描画と衝突検出を高速化できますが、ボールが互いに崩壊するのを止めることはできません。あなたが抱えている問題は、見た目よりもきれいに解決するのがはるかに困難です。ただし、正しく行うことは可能です。

あなたの場合、失敗する構成(大きなビン・オ・ボール)では、オブジェクトは実際には跳ね返るのではなく、互いにスライドするはずです。スライドは継続的な制約であり、実装で処理するインパルス ベースの制約とは異なります。

崩壊を防ぐためにできる最も簡単な方法は、衝突処理を行った後に、衝突しているすべてのオブジェクトを再チェックして、相互貫通していないことを確認することです。制約に違反するオブジェクトがなくなるまで、必要な回数だけ反復する可能性があります。もちろん、これにはもっと時間がかかります-おそらくもっと長くなります(そして、その特定の状況ではそうではないかもしれませんが、無限ループの可能性さえ生じます...)。

シミュレーションを動作させる優れたアルゴリズムはありますが、インパルス ベースのシステムよりも複雑です。一般に、相互に作用する物体 (ビン内のボールなど) を全体として考慮し、相互の制約を満たすように全体的な構成を調整する必要があります。

マルチボディ シミュレーションに関する著作 (書籍、論文、Web サイト) を探す必要があります。どちらがあなたの目的に最も役立つとは言えません。もし私があなたなら、良い大学の図書館に行って、そこにある本を調べることから始めます。真面目な数学の準備をしておく必要があります。「ラグランジュ乗数」のような用語で蕁麻疹が出る場合は、注意してください 8^)。基本的に、このルートに進むと、多くの数学を学び、少なからず物理学を学ぶことができます。

于 2009-03-21T04:39:21.577 に答える
4

ボールが他のボールに完全に囲まれると、衝突検出の考慮を停止します。スクリーンショットを見るだけで、「表面」のボールだけを考慮する必要があるようです。何も衝突しないボールを 6 ボールの深さでチェックするのはなぜですか? これにより、潜在的な衝突の数が大幅に減少します。

于 2009-02-24T02:58:44.960 に答える
4

ハードコアの物理エンジンは float のベクトル化を使用します。これにより、運が良ければ現在のハードウェアで 16 倍のブーストが得られ、特殊なハードウェアではさらに多くのブーストが得られます。たとえば、Larrabee は、数学処理を 1024 倍に高速化するために 1024 の同時計算を処理できます (ただし、GPU でもあるため、これが必要です)。

コードはまだ見ていませんが、高速な平方根やバイナリ絶対値、またはビットごとの符号反転などの数学の最適化を使用していますか? これらのことだけでは役に立ちませんが、大量のデータを大量に処理する場合、これらの手法によって一部の計算がメイン CPU に転送され、FPU が解放され、基本的にスループットが向上します。

また、GCC の SIMD コード生成は文字通り最悪です。VC または IntelCompiler を使用することで最大 16 倍の増加が見られました。つまり、注意を払っていれば、GCC は SIMD 命令をまったく使用していないということです。

また、疑惑の 10,000 回の衝突は、あなたがシミュレートしたほど接近していないため、直接比較することはできません。

于 2009-02-24T01:05:44.937 に答える
2

私はChipmunkの開発を開始してから追跡してきましたが、私の理解から、最適化への答えはデータ構造にあります。(いつもじゃないですか?)..。

Chipmunkがこれを実現するために使用するデータ構造はSpacialHashです。

于 2009-03-20T20:54:21.867 に答える
2

この時点で、重力場でわずかに圧縮可能な流体のように聞こえ始めます。オイラーの観点を使用した計算流体力学のアイデアが役立つかもしれません。一度に 1 つのボールだけが各セルを占めることができるようなスケールでグリッドを作成すると、各セルの質量、運動量、およびエネルギー収支の保存を記述し、その方法でボールの動きを追跡できます。

于 2009-02-24T01:58:22.563 に答える
1

同時。返事が1年半遅れてしまいましたが、それでも感想を書き留めたいと思います。

私は最近、実際にあなたが使用したのと同じアルゴリズムを使用した、あなたのボールが重なっているのと同じ問題に関する質問を書きました。送信したときにあなたの質問が表示されなかったので、残念です。

初めに、

最適化: 画面全体を最大のボールの直径の幅と高さのセルに分割する単純なグリッド分割システムを使用します。グリッドは各ボールがどのセルにあるかを追跡するため、衝突チェックが必要な場合は、チェックしているセルに隣接するセルにあるすべてのボールの ID のリストを埋める nearBy() 関数を使用します。 .

グリッド方式はうまく機能し、遅れる前に最大 2000 個のボールを持つことができます。グリッドにボールを削除したり追加したりするのは少し面倒ですが、それが私が実装した方法です (グリッドはボール リストとすべてのボールのインデックス位置に大きく基づいています)。

将来的には、パーティション分割と衝突チェックの最適化の他の方法を検討したいと思います。

オーバーラップ: ここや他の場所の多くの回答は、フレームごとに衝突を再帰的に修正することを示唆しています。これは実際にはある程度うまくいきました。最適化が十分に行われていれば、フレームごとに 2 ~ 3 回のチェックを実行することで、重なり合う混乱を防ぐことができます。しかし、それは完璧ではありません。精度を向上させると (補間やより良い統合などの凝った言葉を使用して)、ジッタリングとオーバーラップが改善されるのではないかと思います。

衝突チェックを優先度に基づいて順序付けすることを考えました (壁に接触しているものが最も高く、壁に接触しているものは優先度リストの 1 つ下など)、それを最小移動距離に考慮します。MyPhysicsLabは複数の同時衝突の処理について述べていますが、私はまだそれを調べていません。

何か分かったら更新お願いします!私はまったく同じ問題に取り組んでおり、ボール シミュレーターはかなり人気があるようです。この種の経験は、剛体物理学に進む場合に役立ちます。

ありがとう。

于 2010-12-30T22:09:26.623 に答える
1

問題は、ボールが「積み重なる」ほど多くの相互作用があることでしょうか? 私がこれを行っていた場合、次のことを試します。

  • 多くの衝突が同時に発生しないように、重力を 0 にします。
  • 衝突検出アルゴリズムのプロファイリング - 並べ替えられたボールの配列を使用していて、最も近い 6 または 7 のみを分析している場合、問題はないはずです ... 800 個のボールを想定すると、1 サイクルあたりわずか 8000 回の衝突チェックです。あまり多くない
于 2009-02-24T01:10:59.837 に答える
1

これを試して:

正方形がボールの半径よりもわずかに広くなるように、長方形を N*M の正方形に分割します。四角形が四角形の中にきちんと収まるのではなく、四角形の端に重なるようにすることをお勧めします。

BitSet の配列を作成します。Bitset[M][N] を使用しないでください。新しい Bitset[M*N] だけを使用してください。多少の乗算は問題ありません。

各ボールを番号で識別します。ある場所にボールを配置するときは、その正方形とその周囲の 8 つの正方形のビットセットにビットを設定します (これを簡単にするために、正方形の配列を拡張して、長方形の端を 1 つ超えて拡張します。クリップする必要はありません。)

正方形を実行します。各正方形のボールのペアごとに、そのペアが衝突の可能性があるものとしてマークします。これを行うには、ビットセットを作成し、H 個のボールがあり、ボール A と B が同じ正方形を占めていると仮定して、ビット A+B H と A H+B を設定します。

BitSet には「設定されているビットの次のビットを見つけてください」というメソッドが含まれているため、衝突の可能性を簡単に回避できます。各ビットは二重にカウントされるため、ビット Q がセットされていることが検出されたら(Q%H)*H + (Q/H)、ペアのもう一方のビットであるビットを必ずアンセットしてください。

あるいは、その衝突の配列を非常に簡単に折りたたむことができます。A と B の間の衝突 -ビットを設定することで A > Bをマークできる場合A * (A-1) / 2 + B。これには、ボールの総数を気にする必要がないという利点があります。

実際:忘れてください。演習として書いたこのクラスを使用してください。

import java.util.BitSet;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class PairSet extends BitSet implements
    Iterable<PairSet.Pair> {
  public static class Pair implements Comparable<Pair> {
    public final int a;
    public final int b;

    private Pair(int a, int b) {
      if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
          "Pair(" + a + "," + b + ")"); }
      if (a > b) {
        this.a = a;
        this.b = b;
      } else {
        this.a = b;
        this.b = a;
      }
    }

    public String toString() {
      return "Pair(" + a + "," + b + ")";
    }

    public int hashCode() {
      return a * (a - 1) / 2 + b;
    }

    public boolean equals(Object o) {
      return o instanceof Pair
          && hashCode() == ((Pair) o).hashCode();
    }

    public int compareTo(Pair o) {
      return hashCode() - o.hashCode();
    }

  }

  PairSet() {}

  PairSet(BitSet z) {
    or(z);
  }

  PairSet(Iterable<Pair> z) {
    for (Pair p : z)
      set(p);
  }

  public void set(Pair p) {
    set(p.a, p.b);
  }

  public void clear(Pair p) {
    clear(p.a, p.b);
  }

  public void set(int a, int b) {
    if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
        "add(" + a + "," + b + ")"); }
    if (a > b) {
      set(a * (a - 1) / 2 + b);
    } else {
      set(b * (b - 1) / 2 + a);
    }
  }

  public void clear(int a, int b) {
    if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
        "add(" + a + "," + b + ")"); }
    if (a > b) {
      clear(a * (a - 1) / 2 + b);
    } else {
      clear(b * (b - 1) / 2 + a);
    }
  }

  public Iterator<Pair> iterator() {
    return new Iterator<Pair>() {
      int at       = -1;
      int triangle = 0;
      int a        = 0;

      public boolean hasNext() {
        return nextSetBit(at + 1) != -1;
      }

      public Pair next() {
        int nextat = nextSetBit(at + 1);
        if (nextat == -1) { throw new NoSuchElementException(); }
        at = nextat;
        while (triangle <= at) {
          triangle += a++;
        }
        return new Pair(a - 1, at - (triangle - a) - 1);

      }

      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }
}

そして、それはあなたの潜在的な衝突をうまく追跡します. 疑似コードは次のとおりです。

SW = width of rectangle
SH = height of rectangle
R = radius of balls + 1 // +1 is a fudge factor.

XS = number of squares across = SW/R + 4; // the +4 adds some slop
YS = number of squares hight = SH/R + 4; // the +4 adds some slop

int sx(Point2D.Float p) // the square into which you put a ball at x
   // never returns a number < 1
 := (int)((p.x-R/2)/R) + 2;

int sy(Point2D.Float p) // the square into which you put a ball at y
   // never returns a number < 1
 := (int)((p.y-R/2)/R) + 2;

Bitset[] buckets = new BitSet[XS*YS];
{for(int i: 0; i<buckets.length; i++) bukets[i] = new BitSet();}

BitSet bucket(int x, int y) {return bucket[y*XS + x]}
BitSet bucket(Point2D.Float p) {return bucket(sy(p),sx(p));}

void move(int ball, Point2D.Float from, Point2D.Float to) {
  if bucket(from) == bucket(to) return;
  int x,y;
  x = sx(from); y=sy(from);
  for(int xx==-1;xx<=1; xx++)
  for(int yy==-1;yy<=1; yy++)
  bucket(sx+xx, sy+yy).clear(ball);
  x = sx(to); y=sy(to);
  for(int xx==-1;xx<=1; xx++)
  for(int yy==-1;yy<=1; yy++)
  bucket(sx+xx, sy+yy).set(ball);
} 

PointSet findCollisions() {
    PointSet pp = new PointSet();
    for(BitSet bb: buckets) {
    int a;
    int prev_a;
    for(prev_a = -1; (a = bb.nextSetBit(prev_a+1))!=-1; prev_a=a) {
      int b;
      int prev_b;
      for(prev_b = a; (b = bb.nextSetBit(prev_b+1))!=-1; prev_b=b) {
        pp.add(a,b);
      }
    }
    return pp;
}
于 2009-03-21T03:14:08.677 に答える
0

パフォーマンスを測定して、ボトルネックが実際にどこにあるかを確認するときが来たと思います。明らかなアルゴリズムの問​​題があったため、以前に測定を行う必要はありませんでした。現在、アルゴリズムを改善する余地はまだありますが、それが最大の問題であると確信していますか?現在、ボールごとに行う比較の数を測定します。小さいものですか?もしそうなら、アルゴリズムの変更は最良の次のステップではないかもしれません。

すべてのボールの位置を決定するのにかかる時間と、実際にボールを描くのにかかる時間を比較します。後者がボトルネックになっている可能性があります。物理エンジンではなく、レンダリングコードに注力する必要があります。

于 2009-02-24T00:58:55.733 に答える
0

何かを見逃していない限り、ボールが重なっているのはバグの結果であり、非効率的なコードではありません。非効率的なコードは、アニメーションの実行速度を低下させるだけです。

問題は、ボールからボールへの衝突検出の反復アプローチによるものだと思います。現在、ボール同士の衝突の結果のみを考慮しているようです。複数のボールが 1 つのボールに接触している場合、結果は定義されていないように見えます。これが発生する可能性は、重力が大きいほど、またはボールの数が多いほど高くなります。

于 2009-06-11T13:26:38.090 に答える