球与球的碰撞 - 检测和处理

时间:2008-12-06 03:24:00

标签: graphics language-agnostic collision-detection physics

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

alt text

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

我想要添加的下一个重要功能是球与球的碰撞。球的运动被分解为x和y速度矢量。我有重力(每一步的y矢量小减少),我有摩擦(每次与墙碰撞的两个矢量的小减少)。球真诚地以令人惊讶的逼真方式移动。

我想我的问题有两部分:

  1. 检测球与球碰撞的最佳方法是什么?
    我是否只有一个O(n ^ 2)循环遍历每个球并检查每个其他球是否重叠半径?
  2. 我用什么方程来处理球到球碰撞?物理101
    它如何影响两个球的速度x / y向量?两个球进入的最终方向是什么?我如何将其应用于每个球?
  3. alt text

    处理“墙壁”的碰撞检测以及由此产生的矢量变化很容易,但我发现球 - 球碰撞的并发症更多。对于墙壁,我只需要采取适当的x或y向量的负数,然后关闭它将朝正确的方向前进。对于球我不认为是这样。

    一些快速澄清:为了简单起见,我现在可以完全弹性碰撞了,现在我的所有球都具有相同的质量,但我将来可能会改变它。


    编辑:我发现有用的资源

    2d球物理学与向量:2-Dimensional Collisions Without Trigonometry.pdf
    2d球碰撞检测示例:Adding Collision Detection


    成功!

    我的球碰撞检测和响应非常有效!

    相关代码:

    碰撞检测:

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

    源代码:Complete source for ball to ball collider.

    如果有人对如何改进这个基本的物理模拟器有一些建议,请告诉我!我还有一点要补充的是角动量,所以球会更逼真地滚动。还有其他建议吗?发表评论!

15 个答案:

答案 0 :(得分:112)

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

维基百科非常好summary of the whole process。对于任何质量的球,可以使用方程计算新的速度(其中v1和v2是碰撞后的速度,u1,u2来自之前):

v_{1} = \frac{u_{1}(m_{1}-m_{2})+2m_{2}u_{2}}{m_{1}+m_{2}}

v_{2} = \frac{u_{2}(m_{2}-m_{1})+2m_{1}u_{1}}{m_{1}+m_{2}}

如果球的质量相同,则只需切换速度即可。这里是我编写的一些类似的代码:

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是对的,您应该考虑将区域分成几个部分,然后在每个部分内进行碰撞检测。请记住,球可能会与部分边界上的其他球发生碰撞,因此这可能会使您的代码更加复杂。效率可能无关紧要,直到你有几百个球。对于奖励积分,您可以在不同核心上运行每个部分,或者拆分每个部分内的碰撞处理。

答案 1 :(得分:48)

好吧,多年前我制作了像你这样的节目。
有一个隐藏的问题(或许多,取决于观点):

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

而且,几乎在100%的情况下,你的新速度都是错误的。好吧,不是速度,而是位置。您必须在正确的位置计算精确的新速度。否则你只需要在一些小的“错误”数量上移动球,这可以从之前的离散步骤中获得。

解决方案很明显:你必须将时间步长分开,首先你转移到正确的位置,然后碰撞,然后在剩下的时间内转移。

答案 2 :(得分:20)

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

阅读 Binary Space PartitioningQuadtrees

答案 3 :(得分:13)

作为对Ryan Fox建议将屏幕划分为区域并仅检查区域内的碰撞的建议的澄清......

e.g。将游戏区域划分为一个正方形网格(将任意说每边有1个单位长度),并检查每个网格方格内的碰撞。

这绝对是正确的解决方案。它的唯一问题(正如另一张海报所指出的)是跨越边界的碰撞是一个问题。

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

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

答案 4 :(得分:10)

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

答案 5 :(得分:7)

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

虽然我确实认为当距离是它们的半径之和时球会击中,但实际上不应该计算这个距离!相反,计算它的正方形并以这种方式使用它。这种昂贵的平方根操作没有理由。

此外,一旦发现碰撞,您必须继续评估碰撞,直到不再存在。问题是,在获得准确的图片之前,第一个可能会导致其他人必须解决。想想如果球在边缘击球会发生什么?第二球击中边缘并立即反弹到第一球。如果你撞到角落里的一堆球,你可能会有很多碰撞需要解决,然后再迭代下一个循环。

至于O(n ^ 2),你所能做的只是最大限度地减少拒绝错过​​的费用:

1)没有移动的球不能击中任何东西。如果在地板上有合理数量的球,这可以节省大量的测试。 (请注意,您仍然必须检查是否有东西击中固定球。)

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

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

答案 6 :(得分:6)

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

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

他们试图从学术角度解释它是如何完成的。它们从简单的物体到物体碰撞检测开始,然后继续进行碰撞响应以及如何扩大它。

修改:更新了链接

答案 7 :(得分:3)

您有两种简单的方法可以做到这一点。杰伊已经从球的中心覆盖了准确的检查方式。

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

答案 8 :(得分:3)

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

对于边界框,加法/差异数学比半径的所有三角形快得多,并且大多数情况下,边界框测试将忽略碰撞的可能性。但是如果你再用trig重新测试,那么你就可以得到准确的结果。

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

答案 9 :(得分:3)

这个KineticModel是Java中cited方法的实现。

答案 10 :(得分:2)

我使用HTML Canvas元素在JavaScript中实现了此代码,并以每秒60帧的速度生成了精彩的模拟。我开始模拟了随机位置和速度的十几个球的集合。我发现在较高的速度下,小球与较大球之间的掠射碰撞导致小球看起来 STICK 到较大球的边缘,并向上移动到大约90度左右分离前的较大球。 (我想知道是否有其他人观察到这种行为。)

计算的一些记录显示,在这些情况下,最小平移距离不足以防止相同的球在下一个时间步骤中发生碰撞。我做了一些实验,发现我可以通过根据相对速度放大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值是大约发现的最小值,总是导致球在碰撞后分离。

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

答案 11 :(得分:1)

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

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

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

答案 12 :(得分:0)

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

答案 13 :(得分:0)

经过反复试验,我将本文档的方法用于2D碰撞:https://www.vobarian.com/collisions/2dcollisions2.pdf (该OP链接到)

我在使用p5js的JavaScript程序中应用了此方法,并且效果很好。我以前曾尝试使用三角方程,虽然它们确实适用于特定的碰撞,但是无论发生的角度如何,我都找不到一个适用于每次碰撞的方程。

本文档中介绍的方法完全不使用三角函数,它只是普通的矢量运算,我向尝试实现球对球碰撞的任何人推荐此方法,根据我的经验,三角函数很难一概而论。我请我大学的物理学家给我示范如何做,他告诉我不要担心三角函数,并给我介绍一种类似于文档中链接的方法。

注意:我的质量都是相等的,但是可以使用文档中提供的方程将其推广到不同的质量。

这是我的代码,用于计算碰撞后的速度矢量:

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

答案 14 :(得分:0)

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

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