Skip to content

Instantly share code, notes, and snippets.

@BonsaiDen
Created August 2, 2013 23:18
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BonsaiDen/6144232 to your computer and use it in GitHub Desktop.
Save BonsaiDen/6144232 to your computer and use it in GitHub Desktop.
Really simple AABB only physics engine.
(function(exports) {
// Vector Class -----------------------------------------------------------
// ------------------------------------------------------------------------
function Vector2(x, y) {
this.x = x;
this.y = y;
}
Vector2.prototype = {
add: function(v) {
return new Vector2(this.x + v.x, this.y + v.y);
},
sub: function(v) {
return new Vector2(this.x - v.x, this.y - v.y);
},
dot: function(v) {
return this.x * v.x + this.y * v.y;
},
cross: function(v) {
return this.x * v.y - this.y * v.x;
},
div: function(s) {
this.x /= s;
this.y /= s;
return this;
},
mul: function(s) {
this.x *= s;
this.y *= s;
return this;
},
normalize: function() {
var len = this.length();
if (this.length > 0.0001) {
var invLen = 1.0 / len;
this.x *= invLen;
this.y *= invLen;
}
},
// Length
lengthSqr: function() {
return this.x * this.x + this.y * this.y;
},
length: function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
};
// AABB implementation ----------------------------------------------------
// ------------------------------------------------------------------------
function AABB(x, y, w, h, mass) {
mass = mass !== undefined ? mass : 0;
// Properties
this.id = ++AABB.id;
// The is actually the inverse mass.
// The value 0 is used as a placeholder for infinite mass
this.im = mass === 0 ? 0 : 1 / mass;
// How "bouncy" this box is. The higher the value, the more the box will
// bounce of in case of a collision
this.restitution = 0.1;
this.staticFriction = 1;
this.dynamicFriction = 0.3;
this.position = new Vector2(x, y);
this.velocity = new Vector2(0, 0);
this.size = new Vector2(w, h);
// Internal, temporary values only used during the collision phase
this.force = new Vector2(0, 0);
this.min = new Vector2(0, 0);
this.max = new Vector2(0, 0);
}
AABB.prototype = {
/**
* Quick check to see if all of the axis of two AABBs are overlapping.
*/
isOverlapping: function(other) {
if (this.max.x < other.min.x || this.min.x > other.max.x) {
return false;
} else if (this.max.y < other.min.y || this.min.y > other.max.y) {
return false;
} else {
return true;
}
},
/**
* Update the boundaries of the AABB
*/
updateBounds: function() {
this.min.x = this.position.x - this.size.x;
this.max.x = this.position.x + this.size.x;
this.min.y = this.position.y - this.size.y;
this.max.y = this.position.y + this.size.y;
},
/**
* Integrate force and gravity into the AABB's velocity.
*/
integrateForces: function(gravity) {
if (this.im !== 0) {
this.velocity.x += (this.force.x * this.im + gravity.x) / 2;
this.velocity.y += (this.force.y * this.im + gravity.y) / 2;
}
},
/**
* Integrate the velocity into the AABB's position.
*/
integrateVelocity: function(gravity) {
if (this.im !== 0) {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.integrateForces(gravity);
}
},
applyImpulse: function(x, y) {
this.velocity.x += this.im * x;
this.velocity.y += this.im * y;
},
applyForce: function(x, y) {
this.force.x += x;
this.force.y += y;
},
clearForces: function() {
this.force.x = 0;
this.force.y = 0;
}
};
// Manifold which contains information about a single collision -----------
// ------------------------------------------------------------------------
function Manifold(a, b) {
this.a = a;
this.b = b;
this.e = Math.min(a.restitution, b.restitution);
this.sf = 0;
this.df = 0;
this.normal = new Vector2(0, 0);
this.penetration = 0;
}
Manifold.prototype = {
/**
* Initialize the manifold for the collision phase.
*/
init: function(gravity, epsilon) {
// TODO is this correct?
this.sf = Math.sqrt(this.a.staticFriction * this.b.staticFriction);
this.df = Math.sqrt(this.a.dynamicFriction * this.b.dynamicFriction);
// Figure out whether this is a resting collision, if so do not apply
// any restitution
var rx = this.b.velocity.x - this.a.velocity.x,
ry = this.b.velocity.y - this.a.velocity.y;
if ((rx * rx + ry * ry) < (gravity.x * gravity.x + gravity.y * gravity.y) + epsilon) {
this.e = 0.0;
}
},
/**
* Solve the SAT for two AABBs
*/
solve: function() {
// Vector from A to B
var nx = this.a.position.x - this.b.position.x,
ny = this.a.position.y - this.b.position.y;
// Calculate half extends along x axis
var aex = (this.a.max.x - this.a.min.x) / 2,
bex = (this.b.max.x - this.b.min.x) / 2;
// Overlap on x axis
var xoverlap = aex + bex - Math.abs(nx);
if (xoverlap > 0) {
// Calculate half extends along y axis
var aey = (this.a.max.y - this.a.min.y) / 2,
bey = (this.b.max.y - this.b.min.y) / 2;
// Overlap on x axis
var yoverlap = aey + bey - Math.abs(ny);
if (yoverlap) {
// Find out which axis is the axis of least penetration
if (xoverlap < yoverlap) {
// Point towards B knowing that n points from A to B
this.normal.x = nx < 0 ? 1 : -1;
this.normal.y = 0;
this.penetration = xoverlap;
return true;
} else {
// Point towards B knowing that n points from A to B
this.normal.x = 0;
this.normal.y = ny < 0 ? 1 : -1;
this.penetration = yoverlap;
return true;
}
}
}
return false;
},
/**
* Resolves a collision by applying a impulse to each of the AABB's
* involved.
*/
resolve: function(epsilon) {
var a = this.a,
b = this.b,
// Relative velocity from a to b
rx = b.velocity.x - a.velocity.x,
ry = b.velocity.y - a.velocity.y,
velAlongNormal = rx * this.normal.x + ry * this.normal.y;
// If the velocities are separating do nothing
if (velAlongNormal > 0 ) {
return;
} else {
// Correct penetration
var j = -(1.0 + this.e) * velAlongNormal;
j /= (a.im + b.im);
// Apply the impulse each box gets a impulse based on its mass
// ratio
a.applyImpulse(-j * this.normal.x, -j * this.normal.y);
b.applyImpulse(j * this.normal.x, j * this.normal.y);
// Apply Friction
var tx = rx - (this.normal.x * velAlongNormal),
ty = ry - (this.normal.y * velAlongNormal),
tl = Math.sqrt(tx * tx + ty * ty);
if (tl > epsilon) {
tx /= tl;
ty /= tl;
}
var jt = -(rx * tx + ry * ty);
jt /= (a.im + b.im);
// Don't apply tiny friction impulses
if (Math.abs(jt) < epsilon) {
return;
}
if (Math.abs(jt) < j * this.sf) {
tx = tx * jt;
ty = ty * jt;
} else {
tx = tx * -j * this.df;
ty = ty * -j * this.df;
}
a.applyImpulse(-tx, -ty);
b.applyImpulse(tx, ty);
}
},
/**
* This will prevent objects from sinking into each other when they're
* resting.
*/
positionalCorrection: function() {
var a = this.a,
b = this.b;
var percent = 0.7,
slop = 0.05,
m = Math.max(this.penetration - slop, 0.0) / (a.im + b.im);
// Apply correctional impulse
var cx = m * this.normal.x * percent,
cy = m * this.normal.y * percent;
a.position.x -= cx * a.im;
a.position.y -= cy * a.im;
b.position.x += cx * b.im;
b.position.y += cy * b.im;
}
};
// AABB collision engine --------------------------------------------------
// ------------------------------------------------------------------------
function Engine() {
this.iterations = 10;
this.gravity = new Vector2(0, 1);
this.contacts = [];
this.boxes = [];
this.length = 0;
}
Engine.EPSILON = 0.0001;
Engine.prototype = {
findCollisions: function() {
this.contacts.length = 0;
for(var i = 0; i < this.length; i++) {
var a = this.boxes[i];
for(var j = i + 1; j < this.length; j++) {
var b = this.boxes[j];
// Ignore collisions between objects with infinite mass
if (a.im === 0 && b.im === 0) {
continue;
} else if (a.isOverlapping(b)) {
var c = new Manifold(a, b);
if (c.solve()) {
this.contacts.push(c);
}
}
}
}
},
integrateForces: function() {
for(var i = 0; i < this.length; i++) {
this.boxes[i].integrateForces(this.gravity);
}
},
initializeCollisions: function() {
for(var i = 0, l = this.contacts.length; i < l; i++) {
this.contacts[i].init(this.gravity, Engine.EPSILON);
}
},
solveCollisions: function() {
for(var i = 0; i < this.iterations; i++) {
for(var c = 0, l = this.contacts.length; c < l; c++) {
this.contacts[c].resolve(Engine.EPSILON);
}
}
},
integrateVelocities: function() {
for(var i = 0; i < this.length; i++) {
this.boxes[i].integrateVelocity(this.gravity);
this.boxes[i].clearForces();
this.boxes[i].updateBounds();
}
},
correctPositions: function() {
for(var i = 0, l = this.contacts.length; i < l; i++) {
this.contacts[i].positionalCorrection();
}
},
tick: function() {
this.findCollisions();
this.integrateForces();
this.initializeCollisions();
this.solveCollisions();
this.integrateVelocities();
this.correctPositions();
},
addBox: function(box) {
this.boxes.push(box);
this.length++;
}
};
exports.Engine = Engine;
exports.Vector2 = Vector2;
exports.Box = AABB;
})(typeof module === 'undefined' ? window : module.exports);
var box = exports;
var ground = new box.Box(0, 20, 100, 10);
//ground.restitution = 1;
var object = new box.Box(0, -20, 10, 10, 1);
//var two = new box.Box(20, -40, 10, 10, 1);
//two.restitution = 0.1;
var w = new box.Engine();
w.addBox(ground);
w.addBox(object);
//w.addBox(two);
//object.applyImpulse(4, 0);
setInterval(function() {
//object.applyForce(0.5, 0);
w.tick();
console.log(Math.round(object.position.x), object.position.y);
}, 33);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment