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

你点击并拖动鼠标来发射一个球。它会弹来弹去,最终停在“地板”上。

我想添加的下一个重要功能是球与球的碰撞。球的运动被分解成x和y速度向量。有重力(每一步y向量都有小幅度的减小),有摩擦力(每一次与墙碰撞两个向量都有小幅度的减小)。这些球以一种令人惊讶的真实方式移动。

我想我的问题有两部分:

检测球与球碰撞的最佳方法是什么? 我只是有一个O(n²)循环,遍历每个球,并检查每个球的半径是否重叠? 我用什么方程来处理球与球的碰撞?101年物理 它如何影响两个球的速度x/y向量?这两个球最终的方向是什么?我怎么把它应用到每个球上呢?

处理“墙壁”的碰撞检测和由此产生的矢量变化很容易,但我发现球-球碰撞更复杂。对于墙,我只需要取适当的x或y向量的负数,它就会朝着正确的方向移动。我可不这么认为。

一些快速的澄清:为了简单起见,我现在对完美弹性碰撞没有问题,而且我所有的球现在都有相同的质量,但未来我可能会改变这一点。


编辑:我发现有用的资源

带矢量的二维球物理:不含三角函数的二维碰撞。pdf 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碰撞。此外,它跳过检查与自身的碰撞)。

然后,在我的球类中,我有我的collision()和resolveccollision()方法:

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));

}

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

如果有人对如何改进这个基本物理模拟器有一些建议,请告诉我!我还需要补充的一件事是角动量,这样球就会滚动得更真实。还有其他建议吗?请留下评论!


减少碰撞检查数量的一个好方法是将屏幕分割成不同的部分。然后只将每个球与同一区域的球进行比较。


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

仔细阅读 二进制空间分区 而且 四叉树


To detect whether two balls collide, just check whether the distance between their centers is less than two times the radius. To do a perfectly elastic collision between the balls, you only need to worry about the component of the velocity that is in the direction of the collision. The other component (tangent to the collision) will stay the same for both balls. You can get the collision components by creating a unit vector pointing in the direction from one ball to the other, then taking the dot product with the velocity vectors of the balls. You can then plug these components into a 1D perfectly elastic collision equation.

维基百科对整个过程有一个很好的总结。对于任何质量的球,新的速度可以用以下公式计算(其中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是对的,您应该考虑将区域划分为部分,然后在每个部分中进行碰撞检测。请记住,球可以在一个部分的边界上与其他球碰撞,所以这可能会使您的代码更加复杂。不过,在你有几百个球之前,效率可能并不重要。为了获得额外的奖励,你可以在不同的核心上运行每个部分,或者在每个部分中拆分碰撞处理。


为了澄清Ryan Fox的建议,将屏幕分割成区域,并且只检查区域内的冲突……

例如,将游戏区域分割成一个方格网格(即每边有一个单位长度),并检查每个方格网格之间的冲突。

这绝对是正确的解决方案。唯一的问题(正如另一个帖子指出的那样)是跨边界的碰撞是一个问题。

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

然后,任何跨越第一个网格边界的碰撞(因此不会被检测到)将在第二个网格的方格内。只要跟踪已经处理过的碰撞(因为可能会有一些重叠),就不必担心处理边缘情况。所有的碰撞都将发生在其中一个网格上的方格内。


有两种简单的方法。杰伊已经介绍了从球的中心起跳的准确方法。

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

我在这里看到了一个需要优化的东西。

虽然我同意球的撞击距离是它们的半径之和,但实际上不应该计算这个距离!更确切地说,计算它是方形的,并以这种方式处理它。没有理由进行昂贵的平方根运算。

Also, once you have found a collision you have to continue to evaluate collisions until no more remain. The problem is that the first one might cause others that have to be resolved before you get an accurate picture. Consider what happens if the ball hits a ball at the edge? The second ball hits the edge and immediately rebounds into the first ball. If you bang into a pile of balls in the corner you could have quite a few collisions that have to be resolved before you can iterate the next cycle.

至于O(n²),你所能做的就是尽量减少拒绝错过的代价:

1)不动的球打不到任何东西。如果有一个合理数量的球躺在地板上,这可以节省很多测试。(注意,你仍然必须检查是否有东西击中了固定球。)

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

3)正如我提到的,不要做平方根。


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

http://www.metanetsoftware.com/technique.html (web.archive.org)

他们试图从学术的角度解释这是如何做到的。他们从简单的对象对对象碰撞检测开始,然后转向碰撞响应以及如何扩展它。

编辑:更新链接


多年前,我做了一个像你在这里展示的节目。 有一个隐藏的问题(或许多,取决于观点):

如果球的速度太 高,你可以错过碰撞。

而且,几乎在100%的情况下,你的新速度是错误的。不是速度,而是位置。你必须在正确的位置精确地计算新的速度。否则,你只是在一个小的“错误”量上移动球,这可以从前面的离散步骤中得到。

解决方案是显而易见的:你必须分割时间步长,首先移动到正确的位置,然后碰撞,然后在剩下的时间内移动。


我看到它在这里和那里暗示,但你也可以先做一个更快的计算,比如,比较边界框的重叠,然后做一个基于半径的重叠,如果第一个测试通过。

边界盒的加法/差分数学运算要比半径的所有三角运算快得多,而且大多数情况下,边界盒测试会排除碰撞的可能性。但是如果你用三角函数重新测试,你会得到你想要的准确结果。

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


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


I implemented this code in JavaScript using the HTML Canvas element, and it produced wonderful simulations at 60 frames per second. I started the simulation off with a collection of a dozen balls at random positions and velocities. I found that at higher velocities, a glancing collision between a small ball and a much larger one caused the small ball to appear to STICK to the edge of the larger ball, and moved up to around 90 degrees around the larger ball before separating. (I wonder if anyone else observed this behavior.)

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

dot_velocity = ball_1.velocity.dot(ball_2.velocity);
mtd_factor = 1. + 0.5 * Math.abs(dot_velocity * Math.sin(collision_angle));
mtd.multplyScalar(mtd_factor);

我验证了在修正之前和之后,每次碰撞的总动能都是守恒的。mtd_factor中的0.5值大约是在碰撞后总是导致球分离的最小值。

虽然这个修复在系统的精确物理中引入了少量的错误,但代价是现在可以在浏览器中模拟非常快的球,而不减少时间步长。


改进问题中给出的圆碰撞检测检测圆的解决方案:

float dx = circle1.x - circle2.x,
      dy = circle1.y - circle2.y,
       r = circle1.r + circle2.r;
return (dx * dx + dy * dy <= r * r);

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


如果你有很多球,我会考虑使用四叉树。为了确定弹跳的方向,只需使用基于碰撞法向的简单能量守恒公式。弹性,重量和速度会让它更真实一些。


经过一些试验和错误,我使用了本文的2D碰撞方法:https://www.vobarian.com/collisions/2dcollisions2.pdf (那个OP链接到)

我在一个使用p5js的JavaScript程序中应用了这一点,它工作得很完美。我之前曾尝试使用三角方程,虽然它们确实适用于特定的碰撞,但我无法找到一个适用于每一次碰撞的方程,无论它发生的角度是多少。

本文档中解释的方法没有使用任何三角函数,它只是简单的向量操作,我将此推荐给任何试图实现球与球碰撞的人,以我的经验,三角函数很难推广。我请我大学里的一位物理学家教我怎么做,他告诉我不要用三角函数来麻烦,并向我展示了一个类似于文档中链接的方法。

注:我的质量都是相等的,但这可以推广到不同的质量使用在文件中提出的方程。

下面是我计算碰撞后速度矢量的代码:

    //you just need a ball object with a speed and position vector.
    class TBall {
        constructor(x, y, vx, vy) {
            this.r = [x, y];
            this.v = [0, 0];
        }
    }

    //throw two balls into this function and it'll update their speed vectors
    //if they collide, you need to call this in your main loop for every pair of 
    //balls.
    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);
    }


这是一个支持质量的简单例子。

private void CollideBalls(Transform ball1, Transform ball2, ref Vector3 vel1, ref Vector3 vel2, float radius1, float radius2)
{
    var vec = ball1.position - ball2.position;
    float dis = vec.magnitude;
    if (dis < radius1 + radius2)
    {
        var n = vec.normalized;
        ReflectVelocity(ref vel1, ref vel2, ballMass1, ballMass2, n);

        var c = Vector3.Lerp(ball1.position, ball2.position, radius1 / (radius1 + radius2));
        ball1.position = c + (n * radius1);
        ball2.position = c - (n * radius2);
    }
}

public static void ReflectVelocity(ref Vector3 vel1, ref Vector3 vel2, float mass1, float mass2, Vector3 intersectionNormal)
{
    float velImpact1 = Vector3.Dot(vel1, intersectionNormal);
    float velImpact2 = Vector3.Dot(vel2, intersectionNormal);

    float totalMass = mass1 + mass2;
    float massTransfure1 = mass1 / totalMass;
    float massTransfure2 = mass2 / totalMass;

    vel1 += ((velImpact2 * massTransfure2) - (velImpact1 * massTransfure2)) * intersectionNormal;
    vel2 += ((velImpact1 * massTransfure1) - (velImpact2 * massTransfure1)) * intersectionNormal;
}