1
votes

I am currently using the following to calculate two spheres bouncing off of each other. This is being used in a 2D pong game using 3D objects (trying to wrap my head around 3D). Most everything works correctly, but sometimes (usually when the X or Y velocities are moving in the same direction, just one faster than the other) the physics do weird things.

The returned float is just a difference in mass that I use to alter the sounds played when balls collide. Can anyone see any mistakes in my calculations:

internal float ResolveCollision(Ball otherBall)
{
    if (otherBall == this) { return 0f; }
    if (this.GetBoundingSphere().Intersects(otherBall.GetBoundingSphere()))
    {
        // Attempt to step the balls back so they are just barely touching
        Vector3 dd = Position - otherBall.Position;
        dd.Normalize();
        Position += dd / 2;
        otherBall.Position -= dd / 2;

        ///http://williamecraver.wix.com/elastic-equations

        Vector3 V1 = Velocity;
        Vector3 P1 = Position;
        float M1 = Mass;
        float A1 = getMovementAngle(V1.X, V1.Y);

        Vector3 V2 = otherBall.Velocity;
        Vector3 P2 = otherBall.Position;
        float M2 = otherBall.Mass;
        float A2 = getMovementAngle(V2.X, V2.Y);

        float CA = getContactAngle(P1, P2);

        // Recalculate x and y components based of a rotated axis, having the x axis parallel to the contact angle.
        Vector3 V1XR = V1 * (float)Math.Cos(A1 - CA);
        Vector3 V1YR = V1 * (float)Math.Sin(A1 - CA);

        Vector3 V2XR = V2 * (float)Math.Cos(A2 - CA);
        Vector3 V2YR = V2 * (float)Math.Sin(A2 - CA);

        //Now solve the x components of the velocity as if they were in one dimension using the equation;
        Vector3 V1f = (V1 * (M1 - M2) + 2 * M2 * V2) / (M1 + M2);
        Vector3 V2f = (V2 * (M2 - M1) + 2 * M1 * V1) / (M1 + M2);

        Vector3 V1fXR = (V1 * (float)Math.Cos(A1 - CA) * (M1 - M2) + 2 * M2 * V2 * (float)Math.Cos(A2 - CA)) / (M1 + M2);
        Vector3 V2fXR = (V2 * (float)Math.Cos(A2 - CA) * (M2 - M1) + 2 * M1 * V1 * (float)Math.Cos(A1 - CA)) / (M1 + M2);

        //Now find the x and y values for the un-rotated axis by equating for the values when the axis are rotated back.
        Vector3 V1fX = V1fXR * (float)Math.Cos(CA) + V1YR * (float)Math.Cos(CA + MathHelper.PiOver2);
        Vector3 V1fY = V1fXR * (float)Math.Sin(CA) + V1YR * (float)Math.Sin(CA + MathHelper.PiOver2);
        Vector3 V2fX = V2fXR * (float)Math.Cos(CA) + V2YR * (float)Math.Cos(CA + MathHelper.PiOver2);
        Vector3 V2fY = V2fXR * (float)Math.Sin(CA) + V2YR * (float)Math.Sin(CA + MathHelper.PiOver2);

        // Add it all up
        Vector3 nV1 = V1fX + V1fY;
        Vector3 nV2 = V2fX + V2fY;

        ///////////////////////////////////////////
        // Correct Velocity & Move apart
        //////////////////////////////////////////
        Velocity = v3check(nV1, MAXSPEED, -MAXSPEED);
        otherBall.Velocity = v3check(nV2, MAXSPEED, -MAXSPEED);

        // Step the balls forward (by there Velocity) just a bit so they are no longer touching
        Position += Velocity * _lastDT * .25f;
        otherBall.Position += otherBall.Velocity * otherBall._lastDT * .25f;

        return BMDMath.toFloat(Mass - otherBall.Mass);
    }

    return 0f;
}

I have the following helper methods to convert some angles (this could be where the problem is:

private static float getMovementAngle(double vx, double vy)
{
    return MathHelper.ToDegrees((float)Math.Atan2(vy, vx));
}


private static float getContactAngle(Vector3 o1, Vector3 o2)
{
    Vector3 d = o1 - o2;
    return MathHelper.ToDegrees((float)Math.Atan2(d.Y, d.X));
}
2
Remember that in XNA the Y axis is reversed, so if you're using a reference system with the "classic" upwards Y axis you have to change your Atan2 method to Math.Atan2(-vy, vx) - pinckerman
@pinckerman -- thanks, that corrected most of the issues I have been seeing! - Anthony Nichols

2 Answers

3
votes

Using angles should be avoided whenever it's possible. In fact, calculating the collision with angles is just awful. There is so much wonderful vector math that help you with the calculation.

So let's start by calculating the collision plane. Actually, we do not need the whole plane, but only its normal. In the case for two spheres, this is just the vector that connects both centers:

collision

var collisionNormal = Position - otherBall.Position;
collisionNormal.Normalize();
//The direction of the collision plane, perpendicular to the normal
var collisionDirection = new Vector3(-collisionNormal.Y, collisionNormal.X, 0);

Now split both velocities in two parts. One part is parallel to the normal, the other is perpendicular. That's because the perpendicular part is not affected by the collision. The split for V2 looks as follows:

collision

var v1Parallel = Vector3.Dot(collisionNormal, V1) * collisionNormal;
var v1Ortho    = Vector3.Dot(collisionDirection, V1) * collisionDirection;
var v2Parallel = Vector3.Dot(collisionNormal, V2) * collisionNormal;
var v2Ortho    = Vector3.Dot(collisionDirection, V2) * collisionDirection;

We could reconstruct the original veclocities by adding its components:

v1 = v1Parallel + v1Ortho;
v2 = v2Parallel + v2Ortho;

As already mentiond, the orthogonal component is not affected by the collision. Now we can apply some physics to the parallel components:

var v1Length = v1Parallel.Length;
var v2Length = v2Parallel.Length;
var commonVelocity = 2 * (this.Mass * v1Length + otherBall.Mass * v2Length) / (this.Mass + otherBall.Mass);
var v1LengthAfterCollision = commonVelocity - v1Length;
var v2LengthAfterCollision = commonVelocity - v2Length;
v1Parallel = v1Parallel * (v1LengthAfterCollision / v1Length);
v2Parallel = v2Parallel * (v2LengthAfterCollision / v2Length);

Now we can recombine the components and:

this.Velocity = v1Parallel + v1Ortho;
otherBall.Velocity = v2Parallel + v2Ortho;
1
votes

The answer by Nico Shertler does a great job of setting up the problem in terms of vectors and has great diagrams. However, the physics portion of the example code does not work if the velocity of either ball is zero--it generates a divide-by-zero error because v1Length or v2Length will be 0 when V1 or V2 are zero, respectively.

Instead, the physics portion can be replaced with the formulas from the Wikipedia article on Elastic Collision (http://en.wikipedia.org/wiki/Elastic_collision#One-dimensional_Newtonian), thusly:

var totalMass = this.Mass + otherBall.Mass;
var v1ParallelNew = (v1Parallel * (this.Mass - otherBall.Mass) + 2*otherBall.Mass * v2Parallel) / totalMass;
var v2ParallelNew = (v2Parallel * (otherBall.Mass - this.Mass) + 2*this.Mass * v1Parallel) / totalMass;
v1Parallel = v1ParallelNew;
v2Parallel = v2ParallelNew;

Then the parallel and orthogonal components can be recombined as in the remainder of his example code.

There is one other important consideration. If you only change the velocities and the balls are not moved out of collision at the time that collision is detected, then they may still be overlapping (collided) in the next frame even though their velocities are now moving them away from each other. This cause them to stick together as the code reverses there velocities towards each other. To avoid this condition, only change the velocities if at least on ball is actually moving towards the other ball in the "parallel" direction. E.g.:

if (Vector.Dot(collisionNormal, V1) > 0 || Vector.Dot(collisionNormal, V2) < 0)
{
    // do the physics code here
}

This conditional check will allow the balls to continue moving away from each other when they are still overlapping after the initial collision resolution is applied to their velocities. It also allows balls that are spawned in an overlapping state to separate.