【问题标题】:How can I optimize my basic physics simulator?如何优化我的基本物理模拟器?
【发布时间】:2009-02-24 00:41:38
【问题描述】:

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


[Link to larger version]

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

问题:

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

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

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


[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结账http://simucal-projects.googlecode.com/svn/ballbounce/trunk/

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

感兴趣的部分:


【问题讨论】:

  • 画球用了多少功?
  • @gs,大约 45% 基于我所做的一些分析。很多。
  • 我不是在写 .Net 代码,所以我帮不上什么忙。但肯定有许多改进的可能。抗锯齿关闭/同时绘制所有圆圈……
  • @gs,我正在使用双缓冲......并且应该在我的 render() 方法中同时绘制球。我会将我的渲染方法添加到感兴趣的部分
  • 没有什么有用的建议可以提供,但你有我的尊重和钦佩。看起来像一些非常好的工作。抱歉,我没有更多可提供的。

标签: optimization physics collision-detection


【解决方案1】:

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

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

【讨论】:

【解决方案2】:

跟踪附近的球 -

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

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

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

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

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

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

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

-亚当

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

    • 最新的 GCC 似乎有一个 -ftree-vectorize 选项(由 -O3 暗示)和一个相关的 -ftree-vectorizer-verbose=n 选项,以便在它不起作用时提供更多信息。或者您可以使用内置编译器显式调用 SIMD 功能(谷歌第一次点击“gcc simd”)
    【解决方案4】:

    一旦一个球完全被其他球包围,就停止考虑将其用于碰撞检测。仅从您的屏幕截图来看,似乎应该只考虑“表面”球。为什么要检查没有任何东西可能碰撞的6个深的球?这将大大减少潜在冲突的数量。

    【讨论】:

      【解决方案5】:

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

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

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

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

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

      【讨论】:

      • 是的,我同意你的说法。我研究了各种流行的物理引擎是如何处理它的,它会变得相当复杂。在查看 Box2D 中的连续碰撞检测代码后,我可以大致了解他们在做什么,但我需要时间来完成数学运算
      • 感谢您的意见,以及对 SO 的第一个很好的回答! +1
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        我一直在关注Chipmunk 的发展,据我了解,优化的答案在于数据结构。 (不是一直这样吗?)...

        Chipmunk 用来实现this 的数据结构是Spacial Hash

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

            【解决方案9】:

            试试这个:

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

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

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

            穿过广场。对于每个方形标记中的每对球,这对球都是潜在的碰撞。为此,请创建一个位集 - 假设您有 H 个球并且球 A 和 B 占据同一个方格 - 设置位 A+BH 和 AH+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;
            }
            

            【讨论】:

            • 实际上 - 您可以通过获取一个迭代器来摆脱分配和释放“Pair”对象,该迭代器使用对可变 Pair 对象的引用进行实例化。它的工作是填写它所指向的对的 x 和 y。
            【解决方案10】:

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

            我最近写了一个关于same problem as your balls overlapping 的问题,它实际上使用了您使用的相同算法。我提交的时候没有看到你的问题,所以我不好。

            首先,

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

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

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

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

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

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

            谢谢。

            【讨论】:

              【解决方案11】:

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

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

              【讨论】:

              • 我已经对其进行了分析,我应该将其包含在我的问题中。碰撞检测大约占我时间的 55%,渲染大约占 45%。大约。
              【解决方案12】:

              我在 iPhone 上做过非常类似的事情,它使用加速度计让您可以倾斜球,并使用触摸屏添加和删除球。它可以处理至少 30 个球,然后才开始明显陷入困境。

              我早期进行的一项优化是内联数学。最初我有一个单独的“矢量”类,它只能处理 10-12 个球,然后变成幻灯片。分析表明它花费了大量时间分配和释放向量。

              此外,一旦球击中,我不会将它们分开,我只是反弹矢量。它们似乎从来没有重叠,当它们重叠时,很明显这是因为它们都被卡在了底部。

              我还没有准备好发布代码,我还有一些润色工作要做,然后我会把它放在商店里。

              【讨论】:

                【解决方案13】:

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

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

                【讨论】:

                • 这不是错误。只是有不同的方法。一种是您使用固定增量时间步长,并且在该时间步长期间,您可能会超过该点,球重叠。如果你想使用一些更高级的微积分,你可以计算一个球碰撞的确切时刻并从那里开始。
                • 尽管如此,更快的代码执行仍然不可能改变结果。或者您是否打算在您的代码更优化后减少固定增量时间?
                猜你喜欢
                • 2012-04-24
                • 2013-03-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-09-05
                • 2019-04-03
                • 1970-01-01
                • 2011-10-06
                相关资源
                最近更新 更多