35

我编写了一个简单的物理建模器,可以让我在屏幕上弹跳球。您可以单击并拖动来发射一个球,或者您可以一次生成数百个球并观察它们之间的相互作用。

替代文字
[链接到更大的版本]

这是一个有趣的小程序,如果可以的话,我想更进一步。我知道他们说过早的优化是万恶之源,但我开始遇到实际的性能障碍,我想知道是否有经验丰富的游戏/模拟器开发人员可以帮助我。

问题:

现在,如果你添加太多球,我的程序就会窒息(在我的机器上它似乎无法处理超过 800 个)。如果这样做,模拟将不再真实,并且所有球在底部相互重叠。

问题在于碰撞检测。在最简单的情况下,碰撞检测是一个 O(N^2) 问题。每个球检查每个其他球。这很快就会变得很糟糕(即使在 100 个球之后,每个循环周期你也要进行 10k 次碰撞检查)。

如果你看这里,你可以看到我添加了数百个球的屏幕截图。模拟器跟不上,它们开始相互重叠。

替代文字
[链接到更大的版本]

目前,我通过寻找重叠的球来检测碰撞。如果我找到两个重叠的球,我会用它们的最小平移距离 (MTD) 将它们分开,或者将它们推开。然后我使用一个简单的物理方程来调整它们的冲量矢量,然后它们在碰撞后会朝不同的方向移动。

它工作得很好,除非球太多,最小平移距离变得明显。它们开始大量重叠,并在底部不断地相互推挤。我增加“重力”越多,情况就越糟。对它们施加的压力增加并且它们彼此压缩/重叠的量增加。

同样,在我击中相当数量的 N 个球之前,我没有任何问题。

当前优化方法

碰撞检测 -
扫描和修剪- (又名排序和扫描)

我在我的球上使用插入排序,每个循环沿着它们的 x 轴循环。由于插入排序的性质,我可以利用模拟器的时间连贯性。逐帧,球的位置变化很小,所以排序没有太多工作要做。这将线性排序摊销运行时间带到 O(N) 或线性,而不是 O(N^2) 的平均运行时间。

由于球已排序,因此在检查碰撞之前,我在第二个循环中进行了几次预检查。现在我只需要检查彼此靠近的球(因为它们是沿 x 轴排序的),并且每当我检查一个球与另一个 xmin 大于当前球的 xmax 的球时,我都会跳出第二个循环. 所以它跳过了成千上万的检查。

当我实现这个时,它带来了巨大的速度提升。但是,我仍然希望能够处理超过 600-800 个球。我读过物理引擎可以轻松同时处理 10k 个对象之间的碰撞检测,所以我想我可以通过一些工作达到 1-2k。

运行分析器后,碰撞检测占用了我大约 55% 的时间,而渲染占用了大约 45% 的时间。所以,这是我最昂贵的两项费用。


问题:

你能想到任何更好的算法或技术让我的模拟器能够处理更多的球吗?


相关代码:

整个项目:

svn 结帐http://simucal-projects.googlecode.com/svn/ballbounce/trunk/

或者,单击此处在浏览器中手动浏览文件。

感兴趣的部分:


4

13 回答 13

14

简单的方法是不要测试对象与对象的碰撞,用每个球的中心点填充一个数组。然后,从每个中心扫描一个以该点为中心的大小为 4*radius 的正方形(您可以通过不使用正方形对其进行一些优化,但代价是使代码更复杂)。如果这个正方形中还有另一个中心,那么您才检查它们是否在彼此的 2* 半径范围内(因此会发生碰撞)。您可以通过降低分辨率和四舍五入球的位置来进一步优化这一点,从而减少您需要执行的检查次数。

另一种方法是将您的空间划分为网格,并将对象存储在网格区域中。您只需要检查相邻网格中对象之间的碰撞。

于 2009-02-24T02:01:11.167 回答
10

跟踪附近的球 -

就像插入排序由于每帧的最小变化是最佳的一样,您可以使用相同的属性来保持跟踪球“邻居”

每个球最多只能与可能的 6 个其他球互动。您可能每 10 帧左右运行一次算法,以确定每个球有哪些邻居,然后在接下来的 10 帧中使用该信息来计算实际碰撞。

您还可以跟踪每个球的矢量,绘制虚拟线,并查看在接下来的 3-5 帧中哪些线交叉,并仅计算可能发生的碰撞(尽管由于时间的原因很少发生)。

正如您按 x 轴排序一样,您可以在主窗口内的细分中“分组”球。当一个球在至少一个球直径的细分内时,它只需要查看同一细分中的球。如果它更接近一两个边界,则需要查看一两个其他细分,但计算量应该更少。

此外,这些细分可以动态定位,因此平均细分只有 100 个球 - 不需要具有不同数量的球的相同大小的细分。

根据细分的拥挤程度(每平方单位的球数),您可以尝试不同的算法 - 稀疏填充的框只需要矢量计算来预测碰撞,而密集的细分可能只需要某种优化的六边形计算。稀疏框可能不需要对许多帧进行碰撞检测(因为不会像在密集框中那样注意到重叠)。

可以确定给定盒子的能量密度——一个非常稀疏的低能量球盒子比一个高能量的稀疏盒子需要更少的碰撞计算。

-亚当

于 2009-02-24T00:53:43.263 回答
4

Once a ball is completely surrounded by other balls stop considering it for collision detection. Just from looking at your screenshot it seems that only the "surface" balls should be considered. Why check the ball 6 balls deep that nothing can possibly collide with? This would greatly reduce the number of potential collisions.

于 2009-02-24T02:58:44.960 回答
4

你的主要问题是你的碰撞解决算法——你可以加快绘图和碰撞检测,但这不会阻止你的球相互碰撞。你遇到的问题比看起来更难解决;不过,有可能做对。

在您的情况下,在失败的配置(大 bin-o'-balls)中,您的对象应该真的相互滑动,而不是弹跳。滑动是一个连续的约束,它不同于你的实现处理的基于脉冲的约束。

防止崩溃的最简单方法是重新检查所有碰撞对象,以确保它们在进行碰撞处理不会相互穿透——可能会根据需要进行多次迭代,直到它们都没有违反约束。这当然会花费更长的时间 - 可能更长(甚至会增加无限循环的可能性,尽管可能不是在那种特定情况下......)。

有一些很好的算法可以让你的模拟运行起来,但它们比你的基于脉冲的系统更复杂。通常,它们涉及将相互作用的物体(如垃圾箱中的球)视为一个整体,并调整它们的集体配置以满足它们的相互约束。

您应该寻找有关多体模拟的作品(书籍、论文、网站)。我不能说哪个可能对您的目的最有用;如果我是你,我会先去一个好的大学图书馆,然后翻阅他们关于这个主题的所有书籍。你应该为一些严肃的数学做好准备;如果诸如“拉格朗日乘数”之类的术语让您陷入荨麻疹,请注意 8^)。基本上,如果你走这条路,你很可能会学到很多数学知识,而且会学到不少物理知识。

于 2009-03-21T04:39:21.577 回答
4

硬核物理引擎使用浮点矢量化,如果幸运的话,它可以在当前硬件上提供 x16 的提升,而在专用硬件上则更多。例如,Larrabee 可以同时处理 1024 次计算,以实现数学处理中的 totcal x1024 提升(但它需要这个,因为它也是 GPU)

尚未查看代码,但您是否在使用数学优化,如快速平方根和二进制绝对值,或按位符号翻转?这些东西本身并没有帮助,但是当你搅动大量数据时,这些技术将一些数学重新路由到主 CPU,释放 FPU,基本上给你更大的吞吐量。

GCC 的 SIMD 代码生成也很糟糕,我看到使用 VC 或 IntelCompiler 增加了 16 倍,这意味着,如果你注意的话,GCC 根本没有使用任何 SIMD 指令!

此外,那些所谓的 10k 碰撞并不像您模拟的那样近距离,因此无法直接比较。

于 2009-02-24T01:05:44.937 回答
2

I've been following the development of Chipmunk since it began and from my understanding the answer to optimizing is in the data structures. (isn't it always?)...

The data structure Chipmunk uses to achieve this is a Spacial Hash.

于 2009-03-20T20:54:21.867 回答
2

在这一点上,它开始听起来几乎像引力场中的一种轻微可压缩的流体。使用欧拉观点的计算流体动力学思想可能会有所帮助。如果您创建一个网格,使得一次只有一个球可以占据每个单元格,您可以为每个单元格编写质量守恒、动量和能量平衡,并以这种方式跟踪球的运动。

于 2009-02-24T01:58:22.563 回答
1

同时。我意识到我回答晚了大约一年半,但我仍然想把我的想法写下来。

我最近写了一个关于与你的球重叠问题相同的问题,它实际上使用了你使用的相同算法。我提交的时候没有看到你的问题,所以我不好。

首先,

优化:我使用了一个简单的网格划分系统,整个屏幕被划分为单元格,这些单元格是最大球直径的宽度和高度。网格跟踪每个球所在的单元格,因此当需要进行碰撞检查时,我使用 nearBy() 函数填充与我正在检查的单元格相邻的单元格中的每个球的 ID 列表.

网格方法效果很好,在滞后之前我最多可以有 2000 个球。虽然在网格中删除和添加球有点痛苦,但这正是我实现它的方式(网格主要基于球列表和每个球的索引位置)。

将来,我想研究其他分区和优化碰撞检查的方法。

重叠:这里和其他地方的很多答案都建议您递归地纠正每一帧的碰撞。这实际上在一定程度上对我有用。通过足够好的优化,您可以每帧进行 2 或 3 次检查,这似乎可以防止一些重叠的混乱。虽然它并不完美。我怀疑提高准确性(使用插值和更好的集成等花哨的词)可以帮助解决抖动和重叠问题。

我考虑过根据优先级排序碰撞检查(最高的是接触墙壁的碰撞检查,然后是接触墙壁的碰撞检查是优先级列表中的一步,等等),并将其考虑到最小平移距离中。MyPhysicsLab谈到处理多个同时发生的碰撞,但我还没有研究过。

如果你发现了什么,请更新!我正在研究完全相同的问题,并且球模拟器似乎很受欢迎。如果我们要继续研究刚体物理,这种体验会派上用场。

谢谢。

于 2010-12-30T22:09:26.623 回答
1

尝试这个:

将你的矩形分成 N*M 个正方形,使正方形比球的半径稍宽。让正方形与矩形的边缘重叠可能是个好主意,而不是整齐地放在其中。

制作一个 BitSet 数组。不要使用 Bitset[M][N],只需使用 new Bitset[M*N] - 一点乘法不会对您造成伤害。

用数字识别每个球。当您将球定位在某个位置时,为该正方形及其周围的 8 个正方形设置 bitset 中的位(为了使这更容易,扩展您的正方形数组,使它们延伸到矩形边缘之外 - 这样您不必剪辑。)

穿过广场。对于每个方形标记中的每对球,这对球都是潜在的碰撞。为此,请创建一个位组 - 假设您有 H 个球并且球 A 和 B 占据同一个方格 - 设置位 A+B H 和 A H+B。

现在很容易找出潜在的冲突,因为 BitSet 包含一个方法,该方法说“在设置的那个之后找到我的下一个位”。请记住,每个位都是双重计数的,因此当检测到位 Q 被设置时,请务必取消设置位(Q%H)*H + (Q/H)- 这是该对的另一位。

或者:您可以很容易地折叠该冲突数组。A 和 B 之间的冲突 -假设 A > B可以通过设置 bit 来标记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 回答
1

也许问题在于,当球“堆积”起来时,相互作用太多了?如果我这样做,我会尝试以下方法:

  • 将重力设置为 0,这样就不会同时发生许多碰撞
  • 分析您的碰撞检测算法 - 如果您使用排序的球阵列并且只分析最近的 6 或 7 个,那么您应该没有任何问题......假设 800 个球,每个周期只有 8000 次左右的碰撞检查,这不是很多
于 2009-02-24T01:10:59.837 回答
0

我认为是时候测量性能来验证瓶颈到底在哪里了。您不需要提前进行测量,因为存在明显的算法问题。现在还有改进算法的空间,但你确定这是最大的问题吗?衡量你现在每个球做了多少比较。是小东西吗?如果是这样,那么算法更改可能不是最好的下一步。

比较确定每个球的位置所需的时间与实际绘制它们所需的时间。可能是后者现在是瓶颈,您应该将精力集中在渲染代码而不是物理引擎上。

于 2009-02-24T00:58:55.733 回答
0

I've done something very much like this on the iPhone, and it uses the accelerometer to allow you to tilt the balls around, and the touch screen to add and delete balls. It can handle at least 30 balls before it starts to bog down noticeably.

One optimization I did early on is to inline the math. Originally I had a separate "vector" class, and it could only handle 10-12 balls before it turned into a slide show. Profiling showed it was spending a lot of time allocating and deallocating vectors.

Also, I don't separate the balls once they hit, I just bounce the vectors. They don't seem to ever overlap, and when they do it's pretty obvious that it's because they all got jammed together on the bottom.

I'm not quite ready to release the code yet, I've got some polishing to do, then I'll put it on the store.

于 2009-03-20T21:04:34.353 回答
0

除非我错过了什么,否则重叠的球是错误的结果,而不是低效的代码。低效的代码只会导致动画运行缓慢。

我认为问题是由于球对球碰撞检测的迭代方法。目前,您似乎只考虑球与球碰撞的结果。当多个球接触一个球时,结果似乎是不确定的,并且发生这种情况的机会随着更大的重力或球的数量而增加。

于 2009-06-11T13:26:38.090 回答