在Stack Overflow社区的帮助下,我编写了一个非常基本但有趣的物理模拟器。



单击并拖动鼠标以启动球。它会反弹并最终停在“地板”上。

我要添加的下一个主要功能是球与球之间的碰撞。球的运动分为x和y速度向量。我有重力(每个步骤y向量的小减小),我有摩擦力(每个与壁碰撞的两个向量的小减小)。球正以令人惊讶的逼真的方式运动。

我想我的问题分为两部分:



什么是最好的检测方法球与球之间的碰撞?
我是否有一个O(n ^ 2)循环遍历每个球并检查其他每个球以查看其半径是否重叠?

我应该使用哪些方程用来处理球与球的碰撞?物理101
如何影响两个球的速度x / y向量?两个球朝哪个方向前进?如何将其应用到每个球上?



处理“墙”的碰撞检测和所产生的矢量变化很容易,但是我发现球的复杂性更高。球碰撞。对于墙壁,我只需要取适当的x或y向量的负值,然后取走正确的方向即可。关于球,我认为不是那样。

一些快速的说明:为简单起见,我现在可以完全弹性碰撞,现在我所有的球都具有相同的质量,但是我将来可能会改变它。 br /> 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));

}


源代码:球对撞机的完整源代码。

如果有人对如何改进此基本物理模拟器有任何建议,请告诉我!我要补充的一件事是角动量,这样球将更真实地滚动。还有其他建议吗?发表评论!

评论

我认为这种算法不够好,因为如果您的球移动得太快(例如:每帧速度快于2 *半径,则一个球可以通过另一个球而不会发生任何碰撞。

@Simulcal您能否再次上传源代码(所有的filedropper.com链接似乎都损坏了)。您还可以放置从[geocities.com/vobarian/2dcollisions/2dcollisions.pdf]中获得的pdf文件,因为geocities最近已离线

这是我工作的BallBounce的最新版本的链接:dl.dropbox.com/u/638285/ballbounce.rar

@致所有贡献者:能否请您说明一下将此引擎转换为3D的方法。这个出色的引擎如何在Java3D中工作。

Line Vector2d脉冲= mtd.multiply(i);应该是我*归一化的mtd向量。类似于:Vector2d脉冲= mtd.normalize()。multiply(i);

#1 楼

要检测两个球是否发生碰撞,只需检查两个球的中心之间的距离是否小于半径的两倍即可。要在球之间进行完美的弹性碰撞,您只需要担心碰撞方向上的速度分量。两个球的其他分量(与碰撞相切)将保持不变。通过创建指向从一个球到另一个球的方向的单位向量,然后将点乘积与球的速度向量相乘,可以得到碰撞分量。然后,您可以将这些组件插入一维完全弹性碰撞方程中。

维基百科对整个过程做了很好的总结。对于任何质量的球,可以使用以下公式计算新的速度(其中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是正确的,您应该考虑将区域划​​分为多个部分,然后进行碰撞检测在每个部分中。请记住,球可能会与节的边界上的其他球碰撞,因此这可能会使您的代码复杂得多。效率可能并不重要,直到您拥有数百个球为止。为了获得加分,您可以在不同的内核上运行每个部分,或在每个部分内拆分碰撞处理。

评论


可以说两个球的质量不相等。球之间的矢量变化如何影响?

– mmcdole
08年12月6日,下午3:53

从12年级开始已经有一段时间了,但是我认为他们得到的动量比例与质量比例相对应。

–瑞安·福克斯(Ryan Fox)
08年12月6日在3:59

@杰伊,只是要指出..您添加的一个方程式图像是针对一维碰撞,而不是二维碰撞。

– mmcdole
08年12月6日下午5:44

@simucal。不正确... u和v是该方程式中的向量。也就是说,它们具有x,y(和z)分量。

–安德鲁·罗尔斯(Andrew Rollings)
08年12月6日下午14:34

@Simucal,您是对的,它们适用于一维情况。要获得更大的尺寸,只需使用与碰撞一致的速度分量(代码中的aci,bci)。其他组件与碰撞正交,不会改变,因此您无需担心它们。

–杰伊·康罗德(Jay Conrod)
08年12月6日15:40

#2 楼

好吧,几年前,我像这里介绍的那样制作了程序。
有一个隐藏的问题(或很多,取决于角度):


球太高
,您可能会错过碰撞。

而且,几乎在100%的情况下,您的新速度是错误的。好吧,不是速度,而是位置。您必须在正确的位置精确计算新速度。否则,您只需要转移一些小的“错误”量即可,这在上一个离散步骤中是可用的。

解决方案很明显:您必须拆分时间步,因此,首先必须转移到正确的位置,然后发生碰撞,然后在剩下的时间里移动。

评论


如果在时间帧长度*速度/ 2上移动位置,则位置将在统计上固定。

– Nakilon
2011年10月7日7:02

@Nakilon:不,它仅在某些情况下有帮助,但通常可能会错过碰撞。错过碰撞的可能性随时间长度的增加而增加。顺便说一句,看来Aleph展示了正确的解决方案(尽管我只是略过了)。

–avp
2011年10月11日在21:43



@avp,我不是在说球的速度太快,您可能会错过碰撞。但是关于您的新位置将是错误的。由于检测到碰撞的时间要晚于实际碰撞的时间,因此如果从该位置减去timeframelength * speed / 2,则精度将提高两倍。

– Nakilon
2011年10月12日,0:05

#3 楼

您应该使用空间分区来解决此问题。

阅读
二进制空间分区

四叉树

评论


代替空间分区,清除和修剪算法不会更好地工作吗?球的移动速度很快,因此任何分区都必须经常更新,从而产生开销。扫描和修剪可以找到O(n log n)中的所有冲突对,而无需任何瞬态数据结构。这是一个很好的基础教程

–HugoRune
2012年6月14日22:45

#4 楼

为了澄清莱恩·福克斯(Ryan Fox)的建议,即将屏幕分成多个区域,并且仅检查区域内的碰撞...

例如将游戏区域划分为正方形网格(每边将任意指定长度为1个单位),并检查每个网格正方形内是否存在碰撞。唯一的问题(正如另一位海报指出的那样)是跨越边界的碰撞是一个问题。

解决方案是将第二个网格以与第一个网格垂直和水平偏移0.5个单位的方式覆盖

然后,任何会跨越第一个网格的边界(因此无法检测到)的碰撞都将在第二个网格的网格正方形内。只要您跟踪已经处理的冲突(可能存在一些重叠),就不必担心处理极端情况。所有碰撞都将在其中一个网格的网格正方形内。

评论


+1可提供更准确的解决方案,并应对怯down的下车手

–史蒂文·劳(Steven A. Lowe)
08年12月6日在6:29

多数民众赞成在一个好主意。我这样做一次,然后检查了当前单元格和所有相邻单元格,但是您的方法更有效。我刚刚想到的另一种方法是检查当前单元格,然后检查它是否与当前单元格边界相交,如果是,则检查该相邻单元格中的对象。

–LoveMeSomeCode
09年7月27日在22:24

#5 楼

减少冲突检查次数的一个好方法是将屏幕分成不同的部分。然后,您只将每个球与同一部分中的球进行比较。

评论


更正:您需要检查与相同和相邻部分的碰撞

– rint
2016年9月14日14:16在

#6 楼

我在这里看到要优化的一件事。

虽然我确实同意,当距离为半径的总和时,击中的球永远不应该实际计算该距离!相反,计算它的平方并以这种方式使用它。没有理由进行如此昂贵的平方根运算。

此外,一旦发现了碰撞,就必须继续评估碰撞,直到不再存在碰撞为止。问题在于,第一个可能导致其他人必须得到解决,才能获得准确的图像。考虑如果球在边缘击球会发生什么?第二个球撞到边缘,立即反弹到第一个球。如果在拐角处撞成一堆球,那么可能会有很多冲突需要解决,然后才能迭代下一个循环。

对于O(n ^ 2),所有您可以做的是最大程度地降低拒绝错过的成本:

1)静止不动的球不会击中任何东西。如果地板上有足够数量的球,这可以节省很多测试。 (请注意,您仍然必须检查是否有物体撞到固定的球上。)

2)可能值得做的事情:将屏幕划分为多个区域,但线条应模糊-球在区域的边缘被列为位于所有相关的区域(可能是4个)中。我将使用4x4网格,将区域存储为位。如果两个球区域的AND之间的零返回零,则测试结束。

3)正如我提到的,不要进行平方根运算。

评论


感谢您提供有关平方根技巧的信息。与广场相比,不知道其昂贵的性质。

– mmcdole
08年12月6日在6:02

另一个优化方法是找到距离其他任何球都不远的球。仅当球的速度受到约束时,这才可靠地起作用。

–布拉德·吉尔伯特(Brad Gilbert)
08年12月6日15:50

我不同意寻找孤立的球。这与检测碰撞一样昂贵。要改善事情,您需要的球小于O(n)。

–Loren Pechtel
08年12月7日在2:25

#7 楼

我找到了一个很棒的页面,其中包含有关2D碰撞检测和响应的信息。

http://www.metanetsoftware.com/technique.html

他们试图解释它的原理从学术角度来看。他们从简单的对象到对象碰撞检测开始,然后继续进行碰撞响应以及如何进行放大。

编辑:更新了链接

#8 楼

您有两种简单的方法可以做到这一点。周杰伦介绍了从球中心进行检查的准确方法。

更简单的方法是使用矩形边界框,将框的大小设置为球的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]
            }
        }
    }
}


评论


这将使球在水平和垂直碰撞中相互进入20%。最好使用圆形边界框,因为效率差异可以忽略不计。同样,(x-width)/ 2应该是x-width / 2。

–马库斯·贾德罗(Markus Jarderot)
08年12月6日在13:05

优先输入错字。您会发现大多数2d游戏都使用非矩形形状的矩形边界框,因为它很快,而且用户几乎从不注意。

–FlySwat
08年12月6日在15:38

您可以执行矩形边界框,然后单击选中圆形边界框。

–布拉德·吉尔伯特(Brad Gilbert)
08年12月6日在15:51

@Jonathan Holland,您的内部循环应该为for(int k = i + 1; ...)这将消除所有多余的检查。 (即检查自身的碰撞并检查Ball1与Ball2的碰撞,然后Ball2与Ball1的碰撞)。

– mmcdole
08年12月6日在16:39

实际上,与圆形边界框相比,正方形边界框在性能方面可能会更差(假设您已经对平方根进行了优化)

– Ponkadoodle
2010年7月6日,凌晨3:01

#9 楼

我在这里和那里都看到了提示,但是您也可以先进行更快的计算,例如比较重叠的边界框,然后在第一次测试通过时再进行基于半径的重叠。

边界框的加/差运算比半径的所有trig运算要快得多,而且大多数情况下,边界框测试将消除发生碰撞的可能性。但是,如果您随后使用Trig进行重新测试,则会获得所需的准确结果。

是的,这是两个测试,但是总体上会更快。

评论


您不需要触发。 bool is_overlapping(int x1,int y1,int r1,int x2,int y2,int r2){return(x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)<(r1 + r2)*(r1 + r2); }

– Ponkadoodle
2010年7月6日,下午3:05

#10 楼

KineticModel是Java中引用的方法的实现。

#11 楼

我使用HTML Canvas元素在JavaScript中实现了此代码,并且以每秒60帧的速度产生了出色的模拟效果。我从随机位置和速度的十几个球的集合开始模拟。我发现,在较高的速度下,一个小球与一个更大的球之间的掠影碰撞会导致该小球看上去粘在大球的边缘,并在分离之前围绕大球向上移动大约90度。 (我想知道是否还有其他人观察到这种现象。)

一些计算记录表明,在这些情况下的最小平移距离不足以防止同一球在接下来的时间步中碰撞。我做了一些实验,发现我可以通过根据相对速度扩展MTD来解决此问题:

每次碰撞都保留了总动能。 mtd_factor中的0.5值大约是在碰撞后总是导致球分离的最小值。

尽管此修复在系统的精确物理过程中引入了少量误差,但权衡是现在可以在浏览器中模拟非常快的球而不会减小时间步长。

评论


sin(..)不是便宜的函数

–PaulHK
17年6月21日在13:03

#12 楼

改进了问题中给出的使用圆碰撞检测来检测圆的解决方案:
float dx = circle1.x - circle2.x,
      dy = circle1.y - circle2.y,
       r = circle1.r + circle2.r;
return (dx * dx + dy * dy <= r * r);

避免了不必要的“如果有两个返回”,并且避免使用不必要的变量。

#13 楼

如果您有很多球,我会考虑使用四叉树。要确定反弹的方向,只需使用基于碰撞法线的简单能量公式守恒即可。弹性,重量和速度会使它更加逼真。

#14 楼

经过一番尝试和错误之后,我将本文档的方法用于2D碰撞:https://www.vobarian.com/collisions/2dcollisions2.pdf
(OP链接到)
我在JavaScript中应用了此方法使用p5js的程序,并且效果很好。我以前曾尝试使用三角方程,尽管它们确实适用于特定的碰撞,但是无论发生碰撞的角度如何,我都找不到一个能对每次碰撞都有效的方程。
本文档中介绍的方法不使用三角方程函数,这只是简单的矢量运算,我向所有尝试实现球到球碰撞的人推荐此方法,根据我的经验,三角函数很难一概而论。我请我大学的物理学家告诉我如何做,他告诉我不要打扰三角函数,并给我演示一种类似于文档中链接的方法。相等,但是可以使用文档中提供的方程式将其推广到不同的质量。
这是我的代码,用于计算碰撞后所得的速度矢量:



 class TBall {
    constructor(x, y, vx, vy) {
        this.r = [x, y];
        this.v = [0, 0];
    }
}

function collision(ball1, ball2) {
    n = [ (ball1.r)[0] - (ball2.r)[0], (ball1.r)[1] - (ball2.r)[1] ];
    un = [n[0] /  vecNorm(n), n[1] / vecNorm(n) ] ;
    ut = [ -un[1], un[0] ];   
    v1n = dotProd(un, (ball1.v));
    v1t = dotProd(ut, (ball1.v) );
    v2n = dotProd(un, (ball2.v) );
    v2t = dotProd(ut, (ball2.v) );
    v1t_p = v1t; v2t_p = v2t;
    v1n_p = v2n; v2n_p = v1n;
    v1n_pvec = [v1n_p * un[0], v1n_p * un[1] ]; 
    v1t_pvec = [v1t_p * ut[0], v1t_p * ut[1] ]; 
    v2n_pvec = [v2n_p * un[0], v2n_p * un[1] ]; 
    v2t_pvec = [v2t_p * ut[0], v2t_p * ut[1] ];
    ball1.v = vecSum(v1n_pvec, v1t_pvec); ball2.v = vecSum(v2n_pvec, v2t_pvec);
}