我写了一个简单的物理建模器,它允许我在屏幕周围弹跳球。您可以单击并拖动以发射球,或者您可以一次生成数百个球并观察它们之间的互动。
alt text http://i41.tinypic.com/2zr0oic.png
[Link to larger version]
这是一个有趣的小程序,如果可以的话,我想进一步研究它。我知道他们说预成熟优化是所有邪恶的根源,但我开始遇到实际的性能障碍,我想知道是否有任何有游戏/模拟器开发经验的人可以帮助我。
问题:
现在,如果你添加太多球(我的机器上似乎无法处理超过800个),我的程序就会窒息。如果这样做,模拟不再现实,所有球在底部相互重叠。
问题在于碰撞检测。在最天真的情况下,碰撞检测是O(N ^ 2)问题。每个球都会检查每一个球。这很快就会导致性能不佳(甚至在100个球之后,你会在每个循环周期进行10k次碰撞检查)。
如果你看这里,你可以看到我添加了几百个球的截图。模拟器无法跟上,它们开始相互重叠。
alt text http://i41.tinypic.com/2nsnuqd.png
[Link to larger version]
目前,我通过寻找重叠球来检测碰撞。如果我找到两个重叠的球,我将它们按最小平移距离(MTD)分开,或将它们分开。然后,我使用一个简单的物理方程来调整它们的脉冲矢量,然后在碰撞后它们向不同方向移动。
效果很好,除非球太多,最小平移距离变得明显。它们开始大量重叠并且不断地在底部互相挤压。我越是“增加”引力就越糟糕。它们上的压力增加,它们被压缩/相互重叠的量增加。
同样,在我打出相当数量的N球之前我没有任何问题。
当前优化方法:
碰撞检测 -
Sweep and prune - (又名。排序和扫描)
我在我的球上使用插入排序,每个循环沿x轴循环。由于插入排序的性质,我可以利用我的模拟器的temporal coherence。框架到框架,球的位置只是稍微改变,因此排序没有太多工作要做。这使得线性分类分摊运行时间为O(N)或线性而不是其平均运行时间O(N ^ 2)。
由于球被分类,我在检查碰撞之前在第二个循环中进行了几次预检查。现在我只需要检查彼此附近的球(因为它们沿着x轴排序),并且当我检查球与另一个xmin大于当前球的xmax的球时,我会突破第二个循环。所以它会跳过数以千计的支票。
当我实施此功能时,它带来了巨大的速度提升。但是,我仍然希望能够处理超过600-800个球。我已经阅读过物理引擎,可以轻松地同时处理10k个物体之间的碰撞检测,所以我想我可以通过一点点工作达到1-2k。
运行 profiler 之后,碰撞检测占用了大约55%的时间,而渲染正在消耗大约45%。所以,这是我最昂贵的两个成本。
问题:
你能想到任何能让我的模拟器能够处理更多球的更好的算法或技术吗?
相关守则:
整个项目:
svn checkout http://simucal-projects.googlecode.com/svn/ballbounce/trunk/
或,点击here在浏览器中手动浏览文件。
感兴趣的部分:
答案 0 :(得分:13)
简单的方法是不测试Object vs Object碰撞,用每个球的中心点填充一个数组。然后,从每个中心扫描一个以该点为中心的4 *半径的正方形(您可以通过不使用正方形来优化这一点,代价是使代码更复杂)。如果这个方块中有另一个中心,那么你才能检查它们是否在彼此的2 *半径范围内(因此发生碰撞)。你可以通过降低分辨率和圆球位置来进一步优化这一点,减少你需要做的检查次数。
另一种方法是将空间划分为网格,并将对象存储在网格区域中。您只需要检查相邻网格中对象之间的碰撞。
答案 1 :(得分:9)
跟踪附近的球 -
就像插入排序是最佳的,因为每帧的变化最小,你可以使用相同的属性来跟踪球'邻居'
每个球最多只能与其他6个球相互作用。您可以每隔10帧左右运行一次算法,以确定每个球所具有的邻居,然后将该信息用于接下来的10帧以计算实际碰撞。
您还可以跟踪每个球的矢量,绘制虚线,并查看在接下来的3-5帧中哪些线交叉并且仅计算可能发生的碰撞(即使很少会因时间而发生)。
正如您按x轴排序一样,您可以在主窗口内的细分中“分组”球。当球在细分内至少一个球直径时,它只需要在同一个细分中查看球。如果它更靠近一个或两个边界,你需要查看一个或两个其他细分,但它应该是更少的计算。
此外,这些细分可以动态定位,因此平均细分只有100个球 - 不需要使用不同数量的球进行相同大小的细分。
根据细分的拥挤程度(每平方单位球数),你可能会尝试不同的算法 - 稀疏填充的盒子只需要矢量计算来预测碰撞,而密集的细分可能只需要某种优化的六边形计算。对于许多帧,稀疏框可能不需要碰撞检测(因为重叠将不会像在人口密集的框中那样被注意到)。
可以确定给定盒子的能量密度 - 具有低能量球的非常稀疏的盒子比具有高能量的稀疏盒子需要更少的碰撞计算。
- 亚当
答案 2 :(得分:4)
硬核物理引擎使用浮点数的矢量化,如果幸运的话,可以在当前硬件上提供x16提升,而在专用硬件上提供更多。例如Larrabee可以在数学处理中处理1024个同时x1024增强的同时计算(但它需要这个,因为它也是GPU)
尚未查看代码,但您使用的是数字优化,如快速平方根和二进制绝对值,还是按位符号翻转?这些东西本身并没有帮助,但是当你大量数据时,这些技术会将一些数学重新路由到主CPU,从而释放FPU,基本上可以为你提供更大的吞吐量。
GCC的SIMD代码生成也很糟糕,我已经看到使用VC或IntelCompiler增加了16倍,这意味着,如果你注意了,GCC根本没有使用任何SIMD指令!
那些被称为10k碰撞的人并不像你所说的那么近,所以它不能直接比较。
答案 3 :(得分:4)
您的主要问题是您的碰撞解决算法 - 您可以加快绘图和碰撞检测,但这不会阻止您的球相互坍塌。你遇到的问题要比现在更难以解决;但是,它可以做得很好。
在你的情况下,在失败的配置(大bin-o'-balls)你的对象应该真正相互滑动,而不是弹跳。滑动是一种连续约束,它与您的实现所处理的基于脉冲的约束类型不同。
你可以做的最简单的事情就是重新检查所有碰撞对象,确保它们在进行碰撞处理后没有穿透 - 可能会根据需要重复多次,直到 none 违反了约束。这当然需要更长的时间 - 可能更长(甚至提高了无限循环的可能性,但可能不是在那种特殊情况下......)。
有很好的算法可以使你的模拟行为,但它们比基于脉冲的系统更复杂。通常,它们涉及考虑相互作用的物体(如箱中的球)作为一个整体,并调整它们的集体配置以满足它们的相互制约。
您应该查找有关多体模拟的作品(书籍,论文,网站)。我不能说哪个对你的目的最有用;如果我是你,我会先参观一个好的大学图书馆,然后查看他们对这个主题的所有书籍。你应该为一些严肃的数学做好准备;如果像“拉格朗日乘数”这样的术语让你在荨麻疹中爆发,请注意8 ^)。基本上,如果你走这条路,你可能会学到很多数学,而且学习的物理量也很少。
答案 4 :(得分:3)
一旦球完全被其他球包围,请考虑进行碰撞检测。只是看你的截图,似乎只应该考虑“表面”球。为什么要检查球6个深,没有任何东西可能碰撞?这将大大减少潜在的碰撞次数。
答案 5 :(得分:2)
在这一点上它开始听起来几乎像一个引力场中的轻微可压缩流体。使用欧拉观点的计算流体动力学思想可能会有所帮助。如果您创建一个具有刻度的网格,使得每次只有一个球可以占据每个单元格,您可以为每个单元格编写质量守恒,动量和能量平衡,并跟踪球的运动。
答案 6 :(得分:2)
自从Chipmunk开始以来,我一直在关注this的开发,根据我的理解,优化的答案在数据结构中。 (不是一直吗?)......
Chipmunk用于实现Spacial Hash的数据结构是{{3}}。
答案 7 :(得分:1)
也许问题在于,当球“堆积”时,有如此多的互动?如果我这样做,我会尝试以下方法:
答案 8 :(得分:1)
试试这个:
将矩形划分为N * M个正方形,使得正方形略宽于球的半径。让正方形与矩形的边缘重叠,而不是整齐地适合它,这可能是一个好主意。
创建一个BitSet数组。不要使用Bitset [M] [N],只需要新的Bitset [M * N] - 稍微增加就不会伤到你。
用数字识别每个球。当您将球放置在某个位置时,请将该方块的位置设置为该方块的8位方块(为了使这更容易,扩展您的方块阵列,使它们延伸超出矩形边缘 - 这样您就可以了不必剪辑。)
穿过广场。对于每个方形标记中的每对球,其配对为潜在的碰撞。要做到这一点,创建一个bitset并且 - 假设你有H球和A和B占据相同的方块 - 设置位A + B H和A H + B.
现在很容易通过潜在的冲突进行处理,因为BitSet包含了一个方法,即“在设置此项之后找到我的下一位”。请记住,每个位都是双重计数,因此当检测到位Q置位时,请务必取消设置位(Q%H)*H + (Q/H)
- 这是该位的另一位。
或者:你可以很容易地折叠那一系列的碰撞。 A和B之间的碰撞 - 给出A>可以通过设置位A * (A-1) / 2 + B
来标记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();
}
};
}
}
这将很好地跟踪您的潜在碰撞。然后是psudeocode
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;
}
答案 9 :(得分:1)
Simucal。我意识到我回答的时间已经晚了一年半,但我还是想写下自己的想法。
我最近写了一个关于same problem as your balls overlapping的问题,它实际上使用了你使用的相同算法。我提交时没有看到你的问题,所以我很糟糕。
首先,
优化: 我使用一个简单的网格分区系统,整个屏幕被划分为单元格,这些单元格是最大球体直径的宽度和高度。网格跟踪每个球所在的单元格,因此当需要进行碰撞检查时,我使用了nearBy()函数,该函数填充了与我正在检查的单元格相邻的单元格中每个球的ID列表。
网格方法效果很好,在落后之前我最多可以有2000个球。删除和添加球到网格有点痛苦,但这只是我实现它的方式(网格主要基于球列表和每个球的索引位置)。
将来,我想研究其他分区和优化碰撞检查的方法。
重叠:这里和其他地方的很多答案都表明你每帧都会递归地纠正碰撞。这在某种程度上对我有用。通过足够好的优化,你可以在每帧中进行2或3次检查,这似乎可以防止一些重叠的混乱。但它并不完美。我怀疑提高准确性(使用插值和更好的集成等奇特的单词)可以帮助抖动和重叠。
我考虑过根据优先级排序碰撞检查(最高的是触摸墙壁的那些,然后触摸那些是优先级列表上的一步,等等),并将其分解为最小的平移距离。 MyPhysicsLab谈到处理多个同时发生的碰撞,但我还没有考虑过。
如果您发现任何问题,请更新!我正在研究完全相同的问题,并且球模拟器似乎非常受欢迎。如果我们要进入刚体物理学,这种经验可以派上用场。
感谢。
答案 10 :(得分:0)
我认为是时候衡量一下性能,以确定瓶颈究竟在哪里。您之前不需要进行测量,因为存在明显的算法问题。现在仍然有改进算法的空间,但你确定这是最大的问题吗?测量你现在每个球做多少次比较。它有点小吗?如果是这样,那么算法变化可能不是最好的下一步。
比较确定每个球位置所需的时间与实际绘制它们所需的时间。它可能是后来现在的瓶颈,你应该把精力集中在渲染代码而不是物理引擎上。
答案 11 :(得分:0)
我在iPhone上做了非常类似的事情,它使用加速度计让你可以倾斜球,并使用触摸屏添加和删除球。在它开始明显陷入困境之前,它可以处理至少30个球。
我早期做的一个优化是内联数学。最初我有一个单独的“矢量”类,在变成幻灯片之前它只能处理10-12个球。分析显示它花费了大量时间来分配和释放载体。
另外,一旦击球我就不会将球分开,我只是反弹向量。它们似乎没有重叠,当它们这样做时很明显,因为它们都被卡在底部。
我还没有准备好发布代码,我还有一些要做的事,然后我会把它放在商店里。
答案 12 :(得分:0)
除非我错过了什么,否则球重叠是错误的结果,而不是低效的代码。效率低下的代码只会导致动画运行缓慢。
我建议这个问题是由球到球碰撞检测的迭代方法造成的。目前看起来你只是在考虑球到球碰撞的结果。当多个球碰到一个球时,结果似乎是不确定的,并且随着重力或球数的增加,这种情况发生的可能性会增加。