Elastic Rebounding Between Balls of Unequal Radii and Mass

COMP 310    Java Resources  Eclipse Resources

[Back to Ballworld]

Here we will attempt a more rigorous treatment of the rebound position between two balls of unequal mass (below, mass is determined by the area of the ball), to exactly calculate the proper position of the rebounded balls without the need for a "nudging" hack.

The basic calculation strategy is backtrack in time wit the pre-collision velocities to the actual collision point and then to forward track in time with the post-collision velocities to the balls proper locations back at the current time.

  1. Calculate the time when the two balls first collided.   Here is the derivation of the collision time for two balls of unequal radius.
  2. For each ball,
    1. Use their current velocity and the negative collision time to find the vector displacement to back to the location of the balls at the moment of the first contact between the balls.    It is important to use floating point double values here to maintain accuracy. The location of a ball at the time of the first contact is its current location plus its velocity times the collision time (a negative value, i.e. backwards in time). Use a double-precision point (e.g. Point2D.Double) for this location to maintain accuracy.
    2. Using the impulse, calculated as normal, EXCEPT that the impulse needs to be calculated at the balls' locations when they are first contacting, i.e. at the locations from calculation above. The method to calculate the impulse must therefore be modified to take the double precision location points. The impulse can then be used to calculate the change in velocity of the ball after the collision as usual.
    3. Encapsulate the actual velocity and location changes in a command to be executed after both balls have completed their interactions.
      • Location change:
        1. Using the negative of the collision time (i.e. go forward in time) and the post-collision velocity to add the vector displacement forward in time (back to the current time)  to the already calculated backtrack displacement.    This will give us the net displacement from the ball's current location to where it should be.  
        2. Translate the balls current location by the calculated net displacement to the proper location.   The ball will now be located properly and have the proper velocity.
      • Velocity change:
        1. Use the impulse and the ball's mass to calculate the change in velocity and then add that change to the current velocity.
  3. Note that the negation of the impulse for the second ball depends on whether or not the second ball calculates its own impulse. That is, the impulse is correct for the ball for which it was calculated.
  4. Run the commands to update the velocities and positions of each ball after they have both finished any calculations.
  5. Don't forget to let the balls run any other interaction behaviors they have -- this may or may not be automatic, depending on whether or not the code separates the interaction criteria and interaction behavior.

  For best results, have your system update the state of the balls after they have moved in a straight line and bounced off the walls. 

Here is the code for this more exact calculation.   Note that the impulse() and updateCollision() methods below are not the same as that used when nudging because there is no nudging taking place during the impulse calculation, instead the position is updated at the same time that the velocity is updated.

/**
 * A physically correct elastic two-body collision model. 
 * @author Stephen Wong
 *
 */
public class CollideExactStrategy implements IUpdateStrategy {


	@Override
	public void updateState(final Ball context, Dispatcher dispatcher) {
		// TODO: To be completed by student	
		// The only difference between the code needed here and the code used for the nudging technique is 
		// the conditional to test if the balls have collided within the last timer tick. 
	}

	/**
	 * Returns the vector that points from point p1 to point p2 
	 * @param p1   The first point
	 * @param p2 The second point
	 * @return The difference vector between p1 and p2
	 */
	private Point deltaP(Point p1, Point p2) {
		return new Point(p2.x - p1.x, p2.y-p1.y);
	}

	/**
	 * Calculates the time of the first contact between two balls. A negative returned time means that the balls 
	 * collided in the past.  A positive time value means that either the balls will collide in the future,
	 * or if equal to Double.MAX_VALUE, they will never collide, i.e. are traveling parallel to each other. 
	 * The contact time is returned in units of timer ticks.
	 * 
	 * IMPORTANT NOTE:  Only negative time values should be used for collision detection because negative times
	 * indicate that the balls collided in the past.   However, any time less than -1.0 means that the collision 
	 * occurred during the previous tick cycle and thus should be ignored.  
	 * 
	 * @param x1    The location of the first ball
	 * @param x2    The location of the second ball
	 * @param v1    The velocity of the first ball
	 * @param v2    The velocity of the second ball
	 * @param minSeparation  The minimum separation, i.e. contact distance between the balls.
	 * @return  The first possible contact time between the two balls.
	 */
	private double collisionTime(Point x1, Point x2, Point v1, Point v2, double minSeparation) {
		Point deltaX = deltaP(x1, x2);
		Point deltaV = deltaP(v1, v2);
		double deltaX2 = deltaX.distanceSq(0.0, 0.0);
		double deltaV2 = deltaV.distanceSq(0.0, 0.0);
		double minSeparation2 = minSeparation*minSeparation;
		double dvdx = deltaV.x*deltaX.x + deltaV.y*deltaX.y;
		
		double root2 = dvdx*dvdx - deltaV2*(deltaX2-minSeparation2);
		
		if (root2 < 0.0) {
			return Double.MAX_VALUE;   // no solution for t
		}
		
		double t = (-dvdx - Math.sqrt(root2))/deltaV2;   // want most negative time solution, i.e. first contact
					
		return t;
	}

	/**
	 * Returns the reduced mass of the two balls (m1*m2)/(m1+m2) Gives correct
	 * result if one of the balls has infinite mass.
	 * 
	 * @param mSource
	 *            Mass of the source ball
	 * @param mTarget
	 *            Mass of the target ball
	 */
	protected double reducedMass(double mSource, double mTarget) {
		if (mSource == Double.POSITIVE_INFINITY)
			return mTarget;
		if (mTarget == Double.POSITIVE_INFINITY)
			return mSource;
		else
			return (mSource * mTarget) / (mSource + mTarget);
	}


	/**
	 * Calculates the impulse (change in momentum) of the collision in the
	 * direction from the source to the target This method calculates the
	 * impulse on the source ball. The impulse on the target ball is the
	 * negative of the result. 
	 * 
	 * @param lSource
	 *            Location of the source ball
	 * @param vSource
	 *            Velocity of the source ball
	 * @param lTarget
	 *            Location of the target ball
	 * @param vTarget
	 *            Velocity of the target ball
	 * @param reducedMass
	 *            Reduced mass of the two balls
	 * @param distance
	 *            Distance between the two balls.
	 * @return  The impulse value of the collision
	 */
	protected Point2D.Double impulse(Point2D.Double lSource, Point vSource,
				Point2D.Double lTarget, Point vTarget, double reducedMass, double distance) {
		// Calculate the normal vector, from source to target
		double nx = (lTarget.x - lSource.x) / distance;
		double ny = (lTarget.y - lSource.y) / distance;
			
		// delta velocity (speed, actually) in normal direction, source to
		// target
		double dvn = (vTarget.x - vSource.x) * nx + (vTarget.y - vSource.y)
				* ny;


		return new Point2D.Double(2.0 * reducedMass * dvn * nx, 2.0
				* reducedMass * dvn * ny);

	}

	/**
	 * Creates a command to update the velocity and position of the source ball (context), given an impulse, then uses the
	 * context's interactWith method to determine the post collision behavior, from the context
	 * ball's perspective. The change in velocity is the impulse divided by the (source) ball's mass. To change
	 * the velocity of the target ball, switch the source and target input
	 * parameters and negate the impulse values.  
	 * 
	 * @param context
	 *            The ball to update
	 * @param mass 
	 *            The mass of the ball 
	 * @param impX
	 *            x-coordinate of the impulse
	 * @param impY
	 *            y-coordinate of the impulse
	 * @param tContact The first ball contact time in ticks.  Should be a negative number.
	 * @return A command that will update the velocity and location of the ball as per the given impulse and first contact time.
	 */
	protected IBallCmd updateCollision(Ball context, double mass, double impX,
			double impY, double tContact) {

		double dVx = impX / mass;
		double dVy = impY / mass;
		double dx = -dVx*tContact; // Current location already takes into account original velocity over tContact   //-(dVx+context.getVelocity().getX())*tContact;
		double dy = -dVy*tContact; // Current location already takes into account original velocity over tContact   //-(dVy+context.getVelocity().getY())*tContact;

		return new IBallCmd() {

			@Override
			public void apply(Ball context, IDispatcher<IBallCmd> disp) {
				context.getVelocity().translate((int) Math.round(dVx),(int) Math.round(dVy));
				context.getLocation().translate((int)Math.round(dx), (int)Math.round(dy));
			}
		};
	}

			
	@Override
	public void init(Ball context) {		
	}

}

Here's an interesting article talking about exact collisions of balls, particularly the issues when multiple balls collide within a single time slice: http://garethrees.org/2009/02/17/physics/

 

© 2020 by Stephen Wong