Skip to content

Commit

Permalink
Optimized SAT.collides method by precomputing vertex projections
Browse files Browse the repository at this point in the history
  • Loading branch information
bchevalier committed Jan 6, 2018
1 parent f42e1fd commit 17862de
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 112 deletions.
81 changes: 52 additions & 29 deletions src/body/Body.js
Expand Up @@ -19,6 +19,7 @@ var Render = require('../render/Render');
var Common = require('../core/Common');
var Bounds = require('../geometry/Bounds');
var Axes = require('../geometry/Axes');
var Projections = require('../geometry/Projections');


(function() {
Expand Down Expand Up @@ -97,6 +98,7 @@ var Axes = require('../geometry/Axes');
composite: null,

axes: null,
projections: null,
area: 0,
mass: 0,
inertia: 0,
Expand All @@ -108,6 +110,8 @@ var Axes = require('../geometry/Axes');

_initProperties(body, options);

Projections.verticesOntoAxes(body.projections, body.vertices, body.axes);

return body;
};

Expand Down Expand Up @@ -164,12 +168,13 @@ var Axes = require('../geometry/Axes');
Bounds.update(body.bounds, body.vertices, body.velocity);

// allow options to override the automatically calculated properties
Body.set(body, {
axes: options.axes || body.axes,
area: options.area || body.area,
mass: options.mass || body.mass,
inertia: options.inertia || body.inertia
});
var properties = {};
if (options.axes) { properties.axes = options.axes; }
if (options.area) { properties.area = options.area; }
if (options.mass) { properties.mass = options.mass; }
if (options.inertia) { properties.inertia = options.inertia; }

Body.set(body, properties);

// render properties
var defaultFillStyle = (body.isStatic ? '#2e2b44' : Common.choose(['#006BA6', '#0496FF', '#FFBC42', '#D81159', '#8F2D56'])),
Expand Down Expand Up @@ -214,6 +219,9 @@ var Axes = require('../geometry/Axes');
case 'inertia':
Body.setInertia(body, value);
break;
case 'axes':
Body.setAxes(body, value);
break;
case 'vertices':
Body.setVertices(body, value);
break;
Expand Down Expand Up @@ -347,7 +355,6 @@ var Axes = require('../geometry/Axes');
}

// update properties
body.axes = Axes.fromVertices(body.vertices);
body.area = Vertices.area(body.vertices);
Body.setMass(body, body.density * body.area);

Expand All @@ -360,9 +367,15 @@ var Axes = require('../geometry/Axes');

// update geometry
Vertices.translate(body.vertices, body.position);
Body.setAxes(body, Axes.fromVertices(body.vertices));
Bounds.update(body.bounds, body.vertices, body.velocity);
};

Body.setAxes = function (body, axes) {
body.axes = axes;
body.projections = Projections.create(axes.length);
};

/**
* Sets the parts of the `body` and updates mass, inertia and centroid.
* Each part will have its parent set to `body`.
Expand Down Expand Up @@ -604,49 +617,59 @@ var Axes = require('../geometry/Axes');
Body.update = function(body, deltaTime, timeScale, correction) {
var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2);

var position = body.position,
previousPosition = body.positionPrev,
velocity = body.velocity,
force = body.force;

// from the previous step
var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale,
velocityPrevX = body.position.x - body.positionPrev.x,
velocityPrevY = body.position.y - body.positionPrev.y;
velocityPrevX = position.x - previousPosition.x,
velocityPrevY = position.y - previousPosition.y;

// update velocity with Verlet integration
body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared;
body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared;
velocity.x = (velocityPrevX * frictionAir * correction) + (force.x / body.mass) * deltaTimeSquared;
velocity.y = (velocityPrevY * frictionAir * correction) + (force.y / body.mass) * deltaTimeSquared;

body.positionPrev.x = body.position.x;
body.positionPrev.y = body.position.y;
body.position.x += body.velocity.x;
body.position.y += body.velocity.y;
previousPosition.x = position.x;
previousPosition.y = position.y;
position.x += velocity.x;
position.y += velocity.y;

// update angular velocity with Verlet integration
body.angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared;
var angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared;
body.angularVelocity = angularVelocity;
body.anglePrev = body.angle;
body.angle += body.angularVelocity;
body.angle += angularVelocity;

// track speed and acceleration
body.speed = Vector.magnitude(body.velocity);
body.angularSpeed = Math.abs(body.angularVelocity);
body.speed = Vector.magnitude(velocity);
body.angularSpeed = Math.abs(angularVelocity);

// transform the body geometry
for (var i = 0; i < body.parts.length; i++) {
var part = body.parts[i];
var parts = body.parts;
for (var i = 0; i < parts.length; i++) {
var part = parts[i],
partVertices = part.vertices,
partPosition = part.position;

Vertices.translate(part.vertices, body.velocity);
Vertices.translate(partVertices, velocity);

if (i > 0) {
part.position.x += body.velocity.x;
part.position.y += body.velocity.y;
partPosition.x += velocity.x;
partPosition.y += velocity.y;
}

if (body.angularVelocity !== 0) {
Vertices.rotate(part.vertices, body.angularVelocity, body.position);
Axes.rotate(part.axes, body.angularVelocity);
if (angularVelocity !== 0) {
Vertices.rotate(partVertices, angularVelocity, position);
Axes.rotate(part.axes, angularVelocity);
if (i > 0) {
Vector.rotateAbout(part.position, body.angularVelocity, body.position, part.position);
Vector.rotateAbout(partPosition, angularVelocity, position, partPosition);
}
}

Bounds.update(part.bounds, part.vertices, body.velocity);
Projections.verticesOntoAxes(body.projections, body.vertices, body.axes);
Bounds.update(part.bounds, partVertices, velocity);
}
};

Expand Down
131 changes: 53 additions & 78 deletions src/collision/SAT.js
Expand Up @@ -11,10 +11,11 @@ var SAT = {};
module.exports = SAT;

var Vertices = require('../geometry/Vertices');
var Vector = require('../geometry/Vector');

(function() {

SAT._temp = [{ depth: 0, axes: null }, { depth: 0, axes: null }];

/**
* Detect collision between two bodies using the Separating Axis Theorem.
* @method collides
Expand All @@ -23,17 +24,17 @@ var Vector = require('../geometry/Vector');
* @return {collision} collision
*/
SAT.collides = function(bodyA, bodyB) {
var overlapAB = SAT._overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes);
if (overlapAB.overlap <= 0) {
var overlapAB = SAT._temp[0];
if (!SAT._overlapAxes(bodyA, bodyB.vertices, overlapAB)) {
return null;
}

var overlapBA = SAT._overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes);
if (overlapBA.overlap <= 0) {
var overlapBA = SAT._temp[1];
if (!SAT._overlapAxes(bodyB, bodyA.vertices, overlapBA)) {
return null;
}

var minOverlap = (overlapAB.overlap < overlapBA.overlap) ? overlapAB : overlapBA;
var minOverlap = (overlapAB.depth < overlapBA.depth) ? overlapAB : overlapBA;

// ensure normal is facing away from bodyA
var positionA = bodyA.position,
Expand All @@ -56,7 +57,7 @@ var Vector = require('../geometry/Vector');
// find support points, there is always either exactly one or two
var verticesA = bodyA.vertices,
verticesB = bodyB.vertices,
potentialSupportsB = SAT._findSupports(bodyA, verticesB, normal),
potentialSupportsB = SAT._findSupports(bodyA, verticesB, -normal.x, -normal.y),
supportCount = 0,
supports = new Array(2);

Expand All @@ -70,7 +71,7 @@ var Vector = require('../geometry/Vector');

// find the supports from bodyA that are inside bodyB
if (supportCount < 2) {
var potentialSupportsA = SAT._findSupports(bodyB, verticesA, Vector.neg(normal));
var potentialSupportsA = SAT._findSupports(bodyB, verticesA, normal.x, normal.y);

if (Vertices.contains(verticesB, potentialSupportsA[0]))
supports[supportCount++] = potentialSupportsA[0];
Expand All @@ -90,7 +91,7 @@ var Vector = require('../geometry/Vector');
}
}

var depth = minOverlap.overlap;
var depth = minOverlap.depth;
var parentA = bodyA.parent;
var parentB = bodyB.parent;
return {
Expand Down Expand Up @@ -122,69 +123,53 @@ var Vector = require('../geometry/Vector');
};

/**
* Find the overlap between two sets of vertices.
* Find the overlap between a body and a set of vertices
* @method _overlapAxes
* @private
* @param {} verticesA
* @param {} verticesB
* @param {} body
* @param {} vertices
* @param {} axes
* @return result
*/
SAT._overlapAxes = function(verticesA, verticesB, axes) {
var projectionA = Vector._temp[0],
projectionB = Vector._temp[1],
result = { overlap: Number.MAX_VALUE },
overlap,
axis;
SAT._overlapAxes = function(body, vertices, overlap) {
var projections = body.projections,
axes = body.axes;

overlap.depth = Number.MAX_VALUE;
for (var i = 0; i < axes.length; i++) {
axis = axes[i];

SAT._projectToAxis(projectionA, verticesA, axis);
SAT._projectToAxis(projectionB, verticesB, axis);

overlap = Math.min(projectionA.max - projectionB.min, projectionB.max - projectionA.min);

if (overlap <= 0) {
result.overlap = overlap;
return result;
var projection = projections[i],
axis = axes[i],
axisX = axis.x,
axisY = axis.y,
vertex = vertices[0],
min = vertex.x * axisX + vertex.y * axisY,
max = min;

for (var j = 1; j < vertices.length; j += 1) {
vertex = vertices[j];

var dot = vertex.x * axisX + vertex.y * axisY;
if (dot > max) {
max = dot;
} else if (dot < min) {
min = dot;
}
}

if (overlap < result.overlap) {
result.overlap = overlap;
result.axis = axis;
var depth = Math.min(projection.max - min, max - projection.min);
if (depth <= 0) {
return;
}
}

return result;
};

/**
* Projects vertices on an axis and returns an interval.
* @method _projectToAxis
* @private
* @param {} projection
* @param {} vertices
* @param {} axis
*/
SAT._projectToAxis = function(projection, vertices, axis) {
var min = Vector.dot(vertices[0], axis),
max = min;

for (var i = 1; i < vertices.length; i += 1) {
var dot = Vector.dot(vertices[i], axis);

if (dot > max) {
max = dot;
} else if (dot < min) {
min = dot;
if (depth < overlap.depth) {
overlap.depth = depth;
overlap.axis = axis;
}
}

projection.min = min;
projection.max = max;
return overlap;
};

/**
* Finds supporting vertices given a body and a set of vertices along a given direction using hill-climbing.
* @method _findSupports
Expand All @@ -194,8 +179,9 @@ var Vector = require('../geometry/Vector');
* @param {} normal
* @return [vector]
*/
SAT._findSupports = function(body, vertices, normal) {
SAT._findSupports = function(body, vertices, normalX, normalY) {
var nearestDistance = Number.MAX_VALUE,
secondNearestDistance = nearestDistance,
vertexToBodyX,
vertexToBodyY,
position = body.position,
Expand All @@ -206,36 +192,25 @@ var Vector = require('../geometry/Vector');
vertexA,
vertexB;

// find closest vertex
// find two closest vertices
for (var i = 0; i < vertices.length; i++) {
vertex = vertices[i];
vertexToBodyX = vertex.x - positionX;
vertexToBodyY = vertex.y - positionY;
distance = -(normal.x * vertexToBodyX + normal.y * vertexToBodyY);
distance = normalX * vertexToBodyX + normalY * vertexToBodyY;

if (distance < nearestDistance) {
secondNearestDistance = nearestDistance;
vertexB = vertexA;

nearestDistance = distance;
vertexA = vertex;
} else if (distance < secondNearestDistance) {
secondNearestDistance = distance;
vertexB = vertex;
}
}

// find next closest vertex using the two connected to it
var prevIndex = vertexA.index - 1 >= 0 ? vertexA.index - 1 : vertices.length - 1;
vertex = vertices[prevIndex];
vertexToBodyX = vertex.x - positionX;
vertexToBodyY = vertex.y - positionY;
nearestDistance = -(normal.x * vertexToBodyX + normal.y * vertexToBodyY);
vertexB = vertex;

var nextIndex = (vertexA.index + 1) % vertices.length;
vertex = vertices[nextIndex];
vertexToBodyX = vertex.x - positionX;
vertexToBodyY = vertex.y - positionY;
distance = -(normal.x * vertexToBodyX + normal.y * vertexToBodyY);
if (distance < nearestDistance) {
vertexB = vertex;
}

return [vertexA, vertexB];
};

Expand Down

0 comments on commit 17862de

Please sign in to comment.