Simple rendering

Every object starts off as a shape (nmlorg.shape.Shape). A shape contains geometry, color information, etc.  Each shape exists on its own, independent of a canvas, world, etc., and does not have a position or physics model.

  var shape = new nmlorg.shape.Shape(
      ['position', 'color'],
      'triangles',
      [// [position ] [color       ]
          x1, y1, z1, r1, g1, b1, 1,
          x2, y2, z2, r2, g2, b2, 1,
          x3, y3, z3, r3, g3, b3, 1,
      ]);

Shapes are coupled with a 3D position inside a world to form a position object (nmlorg.world.PositionObject). A single shape can be added to a world multiple times, creating a new position object each time. When a world is rendered, every shape is sent to the graphics card once, then it is repeatedly painted for every position object.

  var positionObject1 = world.addObject(shape);
  var positionObject2 = world.addObject(shape);

  positionObject1.translate(x1, y1, z1);
  positionObject2.translate(x2, y2, z2);

Each world is a collection of objects and canvases (nmlorg.gl.Canvas). When a shape is added to a world, its geometry, color, etc. information (as vertex buffers) is added to each canvas. When the world is rendered, each object is rendered to each canvas.

  var canvas1 = new nmlorg.gl.Canvas(width1, height1);
  var canvas2 = new nmlorg.gl.Canvas(width2, height2);
  var world = new nmlorg.world.World([canvas1, canvas2]);

The world can be rendered once by calling world.draw(). The world can be set to render itself repeatedly (using the browser's requestAnimationFrame) using world.start(). Custom code can be run once for each animation frame by setting the world's eachFrame.

  world.eachFrame = function(timeStep) {
    positionObject1.translate(0, 0, -1);
    positionObject2.translate(2, 0, 0);
  };

A custom animation function can be called for each position object by passing it to the constructor (directly or as the optional second argument to world.addObject). The position object will be available as this and any arguments passed after the animation function will be passed back into the function, followed by a value containing the amount of time (in seconds) since the previous frame began rendering.

  function animateObject(x, timeStep) {
    this.translate(x[0], x[1], x[2]);
  }

  var positionObject1 = world.addObject(shape, animateObject, [0, 0, -1]);
  var positionObject2 = world.addObject(shape, animateObject, [2, 0, 0]);

Simple physics

physics.js provides an object-management system completely independent from the renderer. Typically, a physical object (nmlorg.physics.Object) will be created at the same time as a naive position object (nmlorg.world.PositionObject, via world.addObject), then the position object's animate callback will tell the physical object to calculate its motion and then copy the physical object's current position to the position object.

  function animateObject(obj, timeStep) {
    obj.compound(timeStep);
    this.setPosition(obj.position[0], obj.position[1], obj.position[2]);
  }

  for (var i = 0; i < 2; i++) {
    var physicalObject = new nmlorg.physics.Object(x, y, z);
    var positionObject = world.addObject(shape, animateObject, physicalObject);
  }

Each physical object maintains its current position in 3D space along with its current velocity (measured in pixels per second) and a list of zero or more forces that apply to the object. The compound call is responsible for first adjusting the object's velocity based on the active forces, and then updating the object's position based on its velocity. Forces may either be indefinite (such as the simplified force of gravity) or pre-set for a fixed duration (such as the force from a rocket thruster with a fixed amount of fuel). [[At present, there is no way to add a force and have it disabled programmatically. To effect that you would need to re-schedule the force before every call to compound with a fixed duration set to the timeStep.]] If a fixed-duration force expires during the current frame (if its remaining duration is less than timeStep), the frame is divided into a subframe lasting until the force expires, then the remaining time left in the frame, with the correct average velocity applied to the position for each subframe. If two forces expire during the current frame (at different times), the frame is divided into [(0, firstExpiration), (firstExpiration, secondExpiration), (secondExpiration, timeStep)]; and so on.

Forces are registered using addForce(xAccel, yAccel, zAccel, optional duration). To roughly simulate Earth gravity (at the scale of 1 meter = 10 pixels), you can call addEarthGravity.

  function animateObject(obj, timeStep) {
    obj.compound(timeStep);
    this.setPosition(obj.position[0], obj.position[1], obj.position[2]);
  }

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

Collision detection and handling

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.

Collision subframing (compound)

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

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

Collision detection (Shape)

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.Shape subclasses (nmlorg.collision.Sphere).

  var collisionWorld = new nmlorg.collision.World();
  var collisionShape = new nmlorg.collision.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, ...);
  }

The math

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

Constant velocity

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:

  1.   sqrt((pos.x + vel.x * t)2 + (pos.y + vel.y * t)2 + (pos.z + vel.z * t)2) = obj1.radius + obj2.radius
    
  2.         (pos.x + vel.x * t)2 + (pos.y + vel.y * t)2 + (pos.z + vel.z * t)2 = (obj1.radius + obj2.radius)2
    
  3.                              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
    
  4.                               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
    
  5. Factor out t2:
                                               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
    
  6. Factor out t:
                                               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
    
  7.                                            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.quadratic.solve.

Constant acceleration

Assuming constant acceleration, the equation for the position of an object at a given time is:
  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:

  1.   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
    
  2.         (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
    
  3.   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
    
  4.   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
    
  5. Factor out t4:
      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
    
  6. Factor out t3:
      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
    
  7. Factor out t2:
      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
    
  8. Factor out t:
      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.quartic.solve. The full detection algorithm is implemented as nmlorg.collision.timeToOrigin.

Collision response (Material)

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.Material.

  var collisionWorld = new nmlorg.collision.World();
  var collisionShape = new nmlorg.collision.Sphere(radius);
  var bouncyBall = new nmlorg.collision.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).

A note about the collision dimension

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). Any component vector tangent to the line of collision is left unchanged. After the objects' final velocities (along the line of collision) are computed, they are recombined with the original non-colliding component vectors to return the velocities to 3D world space.

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.

3D

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.transformVelocity and nmlorg.collision.restoreVelocity. The full response algorithm is implemented as material.handleCollision.