How do I calculate the exit vectors of colliding projectiles?

I'm trying to create projectiles which bounce/ricochet off one another when they collide in mid-air.

All projectiles are spherical and have identical size, mass, and speed. Each has two vectors: one representing its current location and the other its direction and speed. (Vectors, here, basically being <x,y,z> tuples.) The distance/direction between the two projectiles can easily be calculated as a fifth vector.

In my head, I envision the ricochet as reflecting each projectile's velocity off of a plane perpendicular to the line between the two projectiles, but Googling for "vector reflection" and the like just gets me pages full of math I don't know how to translate into code. :-)

How can I model this collision? I'm not worried about real-world factors like drag, friction, or gravity — simple reflection is enough.

static void ricochet(Projectile projectile1, Projectile projectile2) {
Vector position1 = projectile1.position;
Vector position2 = projectile2.position;

Vector direction1 = projectile1.direction;
Vector direction2 = projectile2.direction;

Vector distance = new Vector(position1).subtract(position2);

Vector reflection1 = new Vector();
Vector reflection2 = new Vector();

// MAGIC!

projectile1.direction = reflection1;
projectile2.direction = reflection2;
}

Here's some code from Gamasutra which calculate the velocity vector of two colliding circles (edited to exclude mass):

// First, find the normalized vector n from the center of
// circle1 to the center of circle2
Vector n = circle1.center - circle2.center;
n.normalize();
// Find the length of the component of each of the movement
// vectors along n.
// a1 = v1 . n
// a2 = v2 . n
float a1 = v1.dot(n);
float a2 = v2.dot(n);

// Using the optimized version,
// optimizedP =  2(a1 - a2)
float optimizedP = (2.0 * (a1 - a2)) / 2;

// Calculate v1', the new movement vector of circle1
// v1' = v1 - optimizedP * n
Vector v1' = v1 - optimizedP *  n;

// Calculate v1', the new movement vector of circle1
// v2' = v2 + optimizedP * n
Vector v2' = v2 + optimizedP * n;

circle1.setMovementVector(v1');
circle2.setMovementVector(v2');

Update:

Here's a flash demo I created to illustrate the concept: • I've adapted your Flash demo to use this type of collision, but the particles change speeds rather dramatically when they bounce. If you have a chance, perhaps you could take a peek and tell me what I did wrong? :-) Aug 13 '11 at 1:59
• Sorry, my bad. When I edited the code to exclude mass, I excluded a bit too much mass and left out a scale factor in calculating optimizedP. Moral of the story - don't answer SO at 3:00am. I'll update my code on wonderfl to double check it's right this time. Aug 13 '11 at 9:30
• This looks perfect! Thanks so much for your help. I didn't manage to solve the "velocity theft" problem (which is much more visible with a smaller particle size), but as I know what the projectile's velocity should be, I can simply normalize and re-scale the vectors after the fact. It's cheating, but it looks good. :-) Aug 13 '11 at 16:09
• That's due to the naive collision detection in the demo which just checks overlap using distance, causing problems when the distance travelled between frames is large compared to the objects size, resulting in a collision over multiple frames. To be more accurate, it needs to calculate the penetration vector, then rewind the collision a little to the exact point of intersection. This is also described in the Gamasutra article. I didn't realise the example would be used literally otherwise I would have made it more comprehensive, but then again simpler is usually better. Aug 13 '11 at 17:06
• No worries, that's probably a lot more detail than I need. ^.^ Aug 13 '11 at 17:34

A little correction to lukevanin's answer, the balls can end up sticking to one another by colliding perpetually, this problem can be resolved by preventing collisions between balls moving away from one another, this phenomena happens to coincide with optimizedP being negative, so here is a simple fix:

private function collideMoving(pA:Point, vA:Point, pB:Point, vB:Point):void
{
// excerpt from http://www.gamasutra.com/view/feature/3015/pool_hall_lessons_fast_accurate_.php?page=3

// First, find the normalized vector n from the center of
// circle1 to the center of circle2
var normal:Point = pA.subtract(pB);
normal.normalize(1);

// Find the length of the component of each of the movement
// vectors along n.
// a1 = v1 . n
// a2 = v2 . n
var tA:Number = dotproduct(vA, normal);
var tB:Number = dotproduct(vB, normal);

// Using the optimized version,
// optimizedP =  2(a1 - a2)
//              -----------
//                m1 + m2
// Math.min added to prevent collisions between balls moving away
// from one another, thus preventing "entanglement".
var optimizedP:Number = Math.min((2.0 * (tA - tB)) / 2, 0);

// Calculate v1', the new movement vector of circle1
// v1' = v1 - optimizedP * m2 * n
vA.x = vA.x - (optimizedP * normal.x);
vA.y = vA.y - (optimizedP * normal.y);

// Calculate v1', the new movement vector of circle1
// v2' = v2 + optimizedP * m1 * n
vB.x = vB.x + (optimizedP * normal.x);
vB.y = vB.y + (optimizedP * normal.y);
}

http://wonderfl.net/c/qZbk

• @eBusiness — Your fix doesn't resolve the "velocity theft" problem (try it with smaller particles, it's much more visible) and prevents particles from colliding in some situations where they should. Aug 13 '11 at 16:09
• @Ben Blank, I don't get what you are talking about. There is an inherent lack of exactness due to the update rate, and given small enough particles it does mean that they may appear to pass clean through one another, but I don't get why you would describe that problem as "velocity theft". Aug 13 '11 at 17:58

Reflection is unnecessarily complex, the problem is much simpler than that.

Consider two projectiles (A and B), where A is moving and B is stationery. At the point of collision all the force in A will be passed onto B, resulting in A coming to a complete stop, and B shooting off at the speed that A was travelling at when it hit. That's exactly how a newtons cradle works.

What actually happens is that A's velocity is added to B. We know that every action has an equal and opposite reaction, so the same velocity is subtracted from A. The result is A's velocity vector is transferred to B, and vice-versa.

Now consider if A and B are flying directly towards each other. At the point of collision we add A's velocity to B, and subtract the same value from A. We apply the same calculation to B since it is also moving. The code would look something like this:

This can be simplified by removing the unnecessary arithmetic:

Vector newVelocityA = velocityB;
Vector newVelocityB = velocityA;

As you can see it basically boils down to swapping the vectors of the two particles.

The reason why such a simple system works is that we're assuming perfect elastic collision and projectiles with identical masses. In a more complex system with different masses you would need to base the calculations on the force rather than the velocity, but is based on the same principles.

Update:

Here's an example in Flash which I put together to illustrate the concept. • This works and is valid so long as the mass of the object doesn't matter. In effect a bullet hitting a person would send the person flying off at the speed the bullet was traveling. :D Aug 12 '11 at 19:42
• This doesn't seem right. If the two projectiles (which have identical size, mass, and speed — I'll update the question) are traveling in opposite directions, but don't collide head-on, shouldn't they ricochet at an angle? I'm picturing how billiard balls collide. :-) Aug 12 '11 at 20:00
• Assuming mass is the same and inelastic collisions you can break the velocity vectors into orthogonal components. Those of opposite sign swap those of the same sign remain with their object. Av = (1,1,1) and Bv = (-2,-2,2) then the moment after the collision the resulting velocity vectors are Av = (-2,-2,1) and Bv = (1,1,2) Aug 12 '11 at 20:29
• Ignore the above comment...they swap orthogonal components. Aug 12 '11 at 20:46
• @ben-blank The case of the bodies travelling in opposite directions was a generalisation. The principle applies at any relative angle. Just keep in mind that this example only applies point particles (ie, size is ignored) - if you actually want to model billiard balls then you will also need to take into account the relative centres of each ball and integrate rotation. It becomes considerably more complex (even more so than reflection). Aug 12 '11 at 22:12