The physics object-management system can be thought of as keeping track of the positions and velocities of the centers of objects in the physical model. An independent collision object-management system (provided by collision/detect.js, collision/response.js, and collision/world.js) extends this model to include a simplified shape and material behavior for each object. (The collision shape is intentionally kept separate from the geometry defined by the world renderer's own object manager to simplify the math. In many cases, individual objects will be modeled as spheres to calculate collisions, even if they are cuboid or some other complex shape when drawn to the screen.)
The two functions of the collision system are 1. to detect (predict) when two objects' surfaces are touching such that, without adjusting their velocities, they will pass through each other, and 2. to adjust the velocities of one or both objects at the time of collision to prevent them from passing through each other.
If the world contains two objects, they will either collide in the current frame or they won't. If
they do, the frame needs to be subdivided into [(0, timeToCollision), (timeToCollision,
timeStep)]
with an adjustment to one or both object's velocities at the collision point.
If the world contains three objects, there can be 1. no collisions, 2. a single collision between
obj1 and obj2, 3. a single collision between obj1 and obj3, 4. a single collision between obj2 and obj3,
5. a single collision between obj1, obj2, and obj3, 6. a single collision between obj1 and obj2 followed
by a single collision between obj1 and obj3, 7. a single collision between obj1 and obj3 followed by a
single collision between obj1 and obj2, 8. a single collision obetween obj1 and obj2 followed by a
single collision between obj1 and obj3 followed by a single collision between obj1 and obj2, and so on.
In the case of three colinear objects, with obj1 moving toward both obj2 and obj3 but elastically
striking obj2 first, at the start of the frame a collision between obj1 and obj3 will be predicted that
never occurs. In the case of three colinear objects, with obj1 moving toward obj2 and away from obj3,
after an elastic collision with the immovable obj2, obj1 will begin moving toward and eventually collide
with obj3, which could not have been predicted based on the objects' velocities at the start of the
frame. Therefore, the world needs to predict the earliest collision first, adjust all colliding objects'
velocities, and then completely restart the collision detection process, until there are no more
collisions before the time remaining in the current frame. This is similar to the physical object's compound
, which subdivides the current
frame to calculate the object's changes to velocity and position with variable acceleration; however, to
perform accurate collision detection, all objects must be checked at once.
function animateObject(obj, timeStep) {obj.compound(timeStep);this.setPosition(obj.position[0], obj.position[1], obj.position[2]); } var collisionWorld = new nmlorg.collision.world.World(); for (var i = 0; i < 2; i++) { var physicalObject = new nmlorg.physics.Object(x, y, z); var positionObject = world.addObject(shape, animateObject, physicalObject); physicalObject.addEarthGravity(); collisionWorld.addObject(physicalObject, ...); } world.eachFrame = function(timeStep) { collisionWorld.compound(timeStep); };
To predict when two objects are colliding, the collision system needs to know both the positions of
both objects' centers (nmlorg.physics.Object
) and their shapes. To
simplify the math of collision detection, all objects are modeled as either spheres or 2D
surfaces via nmlorg.collision.detect.Shape
subclasses (nmlorg.collision.detect.Sphere
).
var collisionWorld = new nmlorg.collision.world.World(); var collisionShape = new nmlorg.collision.detect.Sphere(radius); for (var i = 0; i < 2; i++) { var physicalObject = new nmlorg.physics.Object(x, y, z); var positionObject = world.addObject(shape, animateObject, physicalObject); physicalObject.addEarthGravity(); collisionWorld.addObject(physicalObject, collisionShape, ...); }
Two objects are "colliding" when their surfaces are touching, and two spherical objects are colliding when the distance between their centers is equal to the sum of their radii:
sqrt((obj1.pos[t].x - obj2.pos[t].x)2 + (obj1.pos[t].y - obj2.pos[t].y)2 + (obj1.pos[t].z - obj2.pos[t].z)2) = obj1.radius + obj2.radius
Assuming constant velocity, the equation for the position of an object at a given time is:
obj.pos[t] = obj.pos[0] + obj.vel * t
To simplify the math, assume our frame of reference is the second object. (That is, when the camera
is fixed over the origin, if object 1 is moving to the right and object 2 is moving down, we can
calculate the same motion if the camera is fixed over the second object, with object 1 moving to the
right and up.) The second object will have a relative position and velocity of 0, and the first object
will have a relative position of obj1.pos - obj2.pos
and velocity of obj1.vel -
obj2.vel
.
pos = obj1.pos - obj2.pos vel = obj1.vel - obj2.vel
Performing the substitutions and solving for t:
sqrt((pos.x + vel.x * t)2 + (pos.y + vel.y * t)2 + (pos.z + vel.z * t)2) = obj1.radius + obj2.radius
(pos.x + vel.x * t)2 + (pos.y + vel.y * t)2 + (pos.z + vel.z * t)2 = (obj1.radius + obj2.radius)2
pos.x2 + 2 * pos.x * vel.x * t + (vel.x * t)2 + pos.y2 + 2 * pos.y * vel.y * t + (vel.y * t)2 + pos.z2 + 2 * pos.z * vel.z * t + (vel.z * t)2 = (obj1.radius + obj2.radius)2
pos.x2 + 2 * pos.x * vel.x * t + vel.x2 * t2 + pos.y2 + 2 * pos.y * vel.y * t + vel.y2 * t2 + pos.z2 + 2 * pos.z * vel.z * t + vel.z2 * t2 = (obj1.radius + obj2.radius)2
t2 * (vel.x2 + vel.y2 + vel.y2) + pos.x2 + 2 * pos.x * vel.x * t + pos.y2 + 2 * pos.y * vel.y * t + pos.z2 + 2 * pos.z * vel.z * t = (obj1.radius + obj2.radius)2
t2 * (vel.x2 + vel.y2 + vel.y2) + t * (2 * pos.x * vel.x + 2 * pos.y * vel.y + 2 * pos.z * vel.z) + pos.x2 + pos.y2 + pos.z2 = (obj1.radius + obj2.radius)2
t2 * (vel.x2 + vel.y2 + vel.y2) + t * (2 * pos.x * vel.x + 2 * pos.y * vel.y + 2 * pos.z * vel.z) + pos.x2 + pos.y2 + pos.z2 - (obj1.radius + obj2.radius)2 = 0
This is now a quadratic equation with:
a = vel.x2 + vel.y2 + vel.y2 b = 2 * pos.x * vel.x + 2 * pos.y * vel.y + 2 * pos.z * vel.z c = pos.x2 + pos.y2 + pos.z2 - (obj1.radius + obj2.radius)2
A quadratic equation solver is available as nmlorg.math.quadratic.solve
.
obj.pos[t] = obj.pos[0] + obj.vel[0] * t + (1/2) * obj.accel * t2
Similar to the constant velocity case:
pos = obj1.pos - obj2.pos vel = obj1.vel - obj2.vel accel = obj1.accel - obj2.accel
Performing the substitutions and solving for t:
sqrt((pos.x + vel.x * t + accel.x * t2 / 2)2 + (pos.y + vel.y * t + accel.y * t2 / 2)2 + (pos.z + vel.z * t + accel.z * t2 / 2)2) = obj1.radius + obj2.radius
(pos.x + vel.x * t + accel.x * t2 / 2)2 + (pos.y + vel.y * t + accel.y * t2 / 2)2 + (pos.z + vel.z * t + accel.z * t2 / 2)2 = (obj1.radius + obj2.radius)2
pos.x2 + 2 * pos.x * vel.x * t + pos.x * accel.x * t2 + (vel.x * t)2 + vel.x * accel.x * t3 + (accel.x * t2)2 + pos.y2 + 2 * pos.y * vel.y * t + pos.y * accel.y * t2 + (vel.y * t)2 + vel.y * accel.y * t3 + (accel.y * t2)2 + pos.z2 + 2 * pos.z * vel.z * t + pos.z * accel.z * t2 + (vel.z * t)2 + vel.z * accel.z * t3 + (accel.z * t2)2 = (obj1.radius + obj2.radius)2
pos.x2 + 2 * pos.x * vel.x * t + pos.x * accel.x * t2 + vel.x2 * t2 + vel.x * accel.x * t3 + accel.x2 * t4 + pos.y2 + 2 * pos.y * vel.y * t + pos.y * accel.y * t2 + vel.y2 * t2 + vel.y * accel.y * t3 + accel.y2 * t4 + pos.z2 + 2 * pos.z * vel.z * t + pos.z * accel.z * t2 + vel.z2 * t2 + vel.z * accel.z * t3 + accel.z2 * t4 = (obj1.radius + obj2.radius)2
t4 * (accel.x2 + accel.y2 + accel.z2) + pos.x2 + 2 * pos.x * vel.x * t + pos.x * accel.x * t2 + vel.x2 * t2 + vel.x * accel.x * t3 + pos.y2 + 2 * pos.y * vel.y * t + pos.y * accel.y * t2 + vel.y2 * t2 + vel.y * accel.y * t3 + pos.z2 + 2 * pos.z * vel.z * t + pos.z * accel.z * t2 + vel.z2 * t2 + vel.z * accel.z * t3 = (obj1.radius + obj2.radius)2
t4 * (accel.x2 + accel.y2 + accel.z2) + t3 * (accel.x * vel.x + accel.y * vel.y + accel.z * vel.z) + pos.x2 + 2 * pos.x * vel.x * t + pos.x * accel.x * t2 + vel.x2 * t2 + pos.y2 + 2 * pos.y * vel.y * t + pos.y * accel.y * t2 + vel.y2 * t2 + pos.z2 + 2 * pos.z * vel.z * t + pos.z * accel.y * t2 + vel.z2 * t2 = (obj1.radius + obj2.radius)2
t4 * (accel.x2 + accel.y2 + accel.z2) + t3 * (accel.x * vel.x + accel.y * vel.y + accel.z * vel.z) + t2 * (pos.x * accel.x + vel.x2 + pos.y * accel.y + vel.y2 + pos.z * accel.z + vel.z2) + pos.x2 + 2 * pos.x * vel.x * t + pos.y2 + 2 * pos.y * vel.y * t + pos.z2 + 2 * pos.z * vel.z * t = (obj1.radius + obj2.radius)2
t4 * (accel.x2 + accel.y2 + accel.z2) + t3 * (accel.x * vel.x + accel.y * vel.y + accel.z * vel.z) + t2 * (pos.x * accel.x + vel.x2 + pos.y * accel.y + vel.y2 + pos.z * accel.z + vel.z2) + t * 2 * (pos.x * vel.x + pos.y * vel.y + pos.z * vel.z) + pos.x2 + pos.y2 + pos.z2 = (obj1.radius + obj2.radius)2
This is now a quartic equation with:
a = accel.x2 + accel.y2 + accel.z2 b = accel.x * vel.x + accel.y * vel.y + accel.z * vel.z c = pos.x * accel.x + vel.x2 + pos.y * accel.y + vel.y2 + pos.z * accel.z + vel.z2 d = 2 * (pos.x * vel.x + pos.y * vel.y + pos.z * vel.z) e = pos.x2 + pos.y2 + pos.z2 - (obj1.radius + obj2.radius)2
A quartic equation solver is available as nmlorg.math.quartic.solve
. The full
detection algorithm is implemented as nmlorg.collision.detect.timeToOrigin
.
Strictly speaking, two objects are in collision not just when they are touching, but when they are touching and their velocities are such that they will pass through each other after the point of collision. To prevent objects from passing through each other, their velocities must be adjusted at the point of collision in some way. The simplest thing to visualize is letting a rubber ball roll out of your hand: After it is initially released, it will move both toward the floor and away from you. Eventually it will come in contact with the floor, at which point its velocity changes so it is moving back up at some fraction of the speed it was moving down just prior to the collision, and still away from you at roughly the same speed. Eventually, after bouncing against the floor several times, it will stop moving up at all but continue moving away from you, simply rolling along the floor.
Now visualize dropping an object the same size, shape, and mass of the rubber ball, but made out of
concrete. After the first collision with the floor, it will simply start rolling, rather than bouncing.
The difference between the two collisions can be modeled as a difference in the objects' material, via
nmlorg.collision.response.Material
.
var collisionWorld = new nmlorg.collision.world.World(); var collisionShape = new nmlorg.collision.detect.Sphere(radius); var bouncyBall = new nmlorg.collision.response.Material(elastic, inelastic, heat); for (var i = 0; i < 2; i++) { var physicalObject = new nmlorg.physics.Object(x, y, z); var positionObject = world.addObject(shape, animateObject, physicalObject); physicalObject.addEarthGravity(); collisionWorld.addObject(physicalObject, collisionShape, bouncyBall); }
The three material characteristics elastic
, inelastic
, and
heat
are used to calculate how the object's momentum is affected by a collision. A high
value for elastic
means the object will transfer more of its energy to the other object.
(Visualize (elastic=1, inelastic=0, heat=0)
as a Newton's cradle that never stops.) A high value
for inelastic
means the object will attempt to equalize more of its energy with the other
object. (Visualize (elastic=0, inelastic=1, heat=0)
as a piece of wet
clay hitting a skateboard at an angle.) In both elastic and inelastic ideal collisions, the total
momentum of the two objects as a system is preserved; a high value for heat
means the
object will dissipate more of its energy into the atmosphere. (Visualize (elastic=0, inelastic=0,
heat=1)
as a glass figurine shattering against the floor.) The values given are normalized with
each other, so (1, 2, 3)
== (.01, .02, .03)
== (.17, .33,
.5)
.
With spherical objects, the point of collision is always on the same line as the centers of both objects at the time of impact. This line is the "line of collision".
When two objects collide "head on", where the centers of both objects are moving directly toward each other (i.e. the relative velocity of the colliding objects is parallel to the line of collision), the collision can be resolved as a one-dimensional collision along the line of collision (only the magnitudes of the final velocities need to be calculated, the directionality will be preserved by dividing the original vectors by their magnitudes and then multiplying the final magnitude).
For example, if object 1 at [-100, 0] of radius 30 is moving at [20, 0] and object 2 at [0, 0] of radius 30 is at rest, the line of collision follows the x axis (running through [-60, 0] and [0, 0]) as does the relative velocity ([20, 0]). After an inelastic (clay-on-skateboard) collision, both objects will continue moving along the x axis at velocity [10, 0]:
After an elastic (Newton's cradle) collision, object 1 will be at rest and object 2 will be moving along the x axis at [20, 0]:
However, when two objects collide obliquely, where the relative velocity of the colliding objects is
not parallel to the line of collision, their relative motion must be broken up into colliding and
non-colliding components. The component vector along the line of collision is the relative velocity fed
into the collision response formula (such as m1v1 +
m2v2 = (m1 + m2)v
If object 1 at [-100, 0] of radius 30 is moving at [20, 0] and object 2 at [0, 20] of radius 30 is moving at [0, -10], they will collide when object 1 is at [-60, 0] and object 2 is at [0, 0]. After either type of collision, object 1's y-direction velocity will remain 0, and object 2's y-direction velocity will remain -10; this component was tangent to the line of collision (which was along the x axis) and so no energy transfer takes place.
Inelastic:
Elastic:
If object 1 at [-96, 18] of radius 30 is moving at [20, 0] and object 2 at [0, 20] of radius 30 is moving at [0, -10], they will collide when object 1 is at [-56, 18] and object 2 is at [0, 0]:
This time, the line of collision does not follow the same axes as the component vectors, so it is simplest to combine them into a single relative velocity (for example, object 1 moving at [20, 10] and object 2 at rest):
Then break the relative velocity into components in a new two-dimensional coordinate system with the line of collision as the x axis. First calculate the angle of the line of collision from the original x axis: Form a right triangle with a hypotenuse running between the centers of both objects and a point at the y position of the center of object 1 and the x position of the center of object 2. Calculate the angle using SOHCAHTOA:
tan(angleLOC) = (obj2.pos.y - obj1.pos.y) / (obj2.pos.x - obj1.pos.x) angleLOC = atan2(obj2.pos.y - obj1.pos.y, obj2.pos.x - obj1.pos.x) angleLOC = atan2(0 - 18, 0 - -56) angleLOC = atan2(-18, 56) angleLOC = -17.8189°
Next calculate the angle of the relative velocity vector from the original x axis: Form a right triangle with the origin as one point, the relative velocity vector as a second, and a point at the velocity vector's x component along the x axis for the third. Calculate the angle using SOHCAHTOA:
tan(anglevel) = vel.y / vel.x anglevel = atan2(vel.y, vel.x) [atan(vel.y / vel.x)] anglevel = atan2(10, 20) anglevel = 26.5651°
To move into the alternate coordinate system, subtract angleLOC from anglevel.
Use this new angle to form a right triangle with a hypotenuse equal in length to the magnitude of the
relative velocity vector (sqrt(vel.x2 + vel.y2)
), and solve for the
other sides; these are your x (colliding) and y (non-colliding) components.
mag = sqrt(vel.x2 + vel.y2) mag = sqrt(202 + 102) mag = sqrt(400 + 100) mag = 22.3607 sin(anglevel - angleLOC) = non-colliding / mag sin(26.5651 - -17.8189) = non-colliding / 22.3607 22.3607 * sin(44.3840) = non-colliding non-colliding = 15.6405 cos(anglevel - angleLOC) = colliding / mag cos(26.5651 - -17.8189) = colliding / 22.36 22.3607 * cos(44.3840) = colliding colliding = 15.9805
Apply the objects' materials' collision response formula to the x (colliding) component, then add the original y (non-colliding) component back into the result, reverse the coordinate system shift, and re-separate the final relative velocity into object 1's and object 2's.
When obj1.pos.z != obj2.pos.z, the simple angle subtraction separation won't quite cut it. To fully
support arbitrary 3D position and velocity differences, collision/response.js separates out the
colliding component using a rotation matrix (shifting [dx, dy, dz] first to the plane defined by the x
and z axes, then to the x axis), available as nmlorg.collision.response.transformVelocity
and nmlorg.collision.response.restoreVelocity
.
The full response algorithm is implemented as material.handleCollision
.