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

279

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

alt text

你可以通过点击和拖动鼠标来发射球。球会在周围弹跳并最终停在“地板”上。

我想要添加的下一个重要功能是球与球之间的碰撞。球的运动被分解为x和y速度向量。我有重力(每步减小y向量的大小),我有摩擦(每次与墙壁碰撞时,两个向量都会稍微减小)。球实际上以一种令人惊讶的逼真方式移动。

我想问的问题有两个部分:

  1. 检测球与球之间碰撞的最佳方法是什么?
    我是否只需要使用O(n^2)循环,迭代每个球并检查每个其他球是否重叠半径?
  2. 我应该使用哪些方程式来处理球与球之间的碰撞?物理学101
    它如何影响两个球的速度x/y向量?两个球朝着什么方向飞出去?我如何将其应用于每个球?

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

}

源代码:球与球碰撞完整源代码。

如果有人有关于如何改进这个基本的物理模拟器的建议,请让我知道!我还未添加的一件事是角动量,这样球就会更真实地滚动。其他建议呢?请留言!


19
我认为这个算法不够好,因为如果你的球移动得太快(比如每帧超过两倍半径的速度),则一个球可以从另一个球中穿过而没有任何碰撞。 - Benji Mizrahi
1
这是我最后一次开发的BallBounce版本链接:http://dl.dropbox.com/u/638285/ballbounce.rar - mmcdole
各位贡献者:你们能否请给予一些指导,将这个引擎转换为3D。这个优秀的引擎如何在Java3D中运行。 - static void main
2
代码相关内容翻译:第Vector2d impulse = mtd.multiply(i);行应该是i乘以标准化的mtd向量。类似这样:Vector2d impulse = mtd.normalize().multiply(i); - klenwell
据我所见,这里没有提到更好的实现方式。事件驱动分子动力学 我会推荐你阅读Boris D. Lubachevsky的"How to Simulate Billiards and Similar Systems",该文可在arxiv上获取:http://arxiv.org/abs/cond-mat/0503627。附图是我打算在完成后开源的程序截图。即使在早期阶段,它也可以平稳地运行5000个球体。希望它能做得更好,尽管我不想实现分区,我想保持代码简单。 - Adrian Roman
显示剩余4条评论
15个回答

2
我使用HTML Canvas元素在JavaScript中实现了这段代码,并以每秒60帧的速度产生了精美的模拟效果。我开始模拟时,随机放置了一打小球,并给它们赋予了随机速度。我发现,在更高的速度下,一个小球与一个大得多的球之间的擦碰会导致小球似乎“粘”在大球的边缘上,并沿着大球移动约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值是经过多次测试后发现的最小值,可以确保球在碰撞后分离。

尽管这种修复引入了一定量的误差到系统的精确物理运算中,但它的优势在于现在非常快速的球可以在浏览器中模拟,而不需要降低时间步长。


1
sin(..)不是一个廉价的函数。 - PaulHK
球速确切代表了什么? - B''H Bi'ezras -- Boruch Hashem

2

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

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

它避免了不必要的“if和两个返回”,以及使用比必要更多的变量。

1
经过一些尝试,我使用了这篇文档中的方法来进行2D碰撞: https://www.vobarian.com/collisions/2dcollisions2.pdf (OP提供的链接)。
我在JavaScript程序中应用了这个方法,使用p5js,它完美地运行。我之前尝试过使用三角函数方程,虽然它们对于特定的碰撞是有效的,但我找不到一个适用于每种碰撞的解决方案,无论发生碰撞的角度如何。
这个文档中解释的方法根本不使用三角函数,只是普通的向量操作,我建议任何试图实现球与球碰撞的人使用这个方法,因为我个人的经验是三角函数很难概括。我曾经问过我学校的物理学家如何做到这一点,他告诉我不要费心去想三角函数,并向我展示了一种类似于文档中链接的方法。
注:我的质量都相等,但可以使用文档中提供的公式将其推广到不同的质量。
这是计算碰撞后速度向量的代码:
    //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);
    }


2
使用所有那些单个字母缩写和没有注释是很难阅读的。通常情况下,除非你在编写SIMD或GLSL,否则不要缩写。这样做没有进行优化,使得任何人都很难理解正在发生的事情。 - Alexander
@Alexander,你说得对,我添加了一些注释,希望这样能更清楚。这里的重点是,你不需要理解函数中发生了什么,所有内容都在我提供的文档中有解释,这只是一个实现该文档的代码而已。 - gordon_freeman

0
如果您有大量的球,我建议使用四叉树。为了决定反弹方向,只需根据碰撞法线使用简单的能量守恒公式即可。弹性、重量和速度会使其更加真实。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接