Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ported Tomas Akenine-Moller's triangle-AABB implementation to javascript #115

Merged
merged 5 commits into from
Jun 29, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions lib/triBoxOverlap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
'use strict';

/**
* Function and helpers for computing the intersection between a static triangle and a static AABB.
* Adapted from Tomas Akenine-Möller's public domain implementation: http://www.cs.lth.se/home/Tomas_Akenine_Moller/code/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still add this to LICENSE.md as public domain so all third-party code is referenced from there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the third party code section is pretty out of date, should I open an issue for that or just update it here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating it as part of this PR would be fine and appreciated.

*/

var Cesium = require('cesium');
var Cartesian3 = Cesium.Cartesian3;

module.exports = triBoxOverlap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use abbreviations in public APIs so make this triangleBoxOverlap.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually triangleAxisAlignedBoundingBoxOverlap is a more precise name.


var vMin = new Cartesian3();
var vMax = new Cartesian3();
// Perform a fast plane/box overlap test using the separating axis theorem.
// Basically, this checks the plane normal against the two vertices whose normals which are most closely aligned with it.
function planeBoxOverlap(normal, vertex, maxBox) {
// Pick the most closely aligned vertices, vMin and vMax
var v = vertex.x;
var sign = (normal.x > 0.0) ? -1.0 : 1.0;
vMin.x = sign * maxBox.x - v;
vMax.x = (-sign) * maxBox.x - v;

v = vertex.y;
sign = (normal.y > 0.0) ? -1.0 : 1.0;
vMin.y = sign * maxBox.y - v;
vMax.y = (-sign) * maxBox.y - v;

v = vertex.z;
sign = (normal.z > 0.0) ? -1.0 : 1.0;
vMin.z = sign * maxBox.z - v;
vMax.z = (-sign) * maxBox.z - v;

// Project each vertex as a vector from the origin onto the plane normal
if (Cartesian3.dot(normal, vMin) > 0.0) {
return false;
}
else if (Cartesian3.dot(normal, vMax) >= 0.0) {
return true;
}
return false;
}

////// X-tests //////

function axisTestX01(a, b, fa, fb, boxHalfSize, v0, v2) {
var p0 = a * v0.y - b * v0.z;
var p2 = a * v2.y - b * v2.z;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace.

var min = p2;
var max = p0;
if (p0 < p2) {
min = p0;
max = p2;
}
var rad = fa * boxHalfSize.y + fb * boxHalfSize.z;
return (min > rad || max < -rad);
}

function axisTestX2(a, b, fa, fb, boxHalfSize, v0, v1) {
var p0 = a * v0.y - b * v0.z;
var p1 = a * v1.y - b * v1.z;
var min = p1;
var max = p0;
if (p0 < p1) {
min = p0;
max = p1;
}
var rad = fa * boxHalfSize.y + fb * boxHalfSize.z;
return (min > rad || max < -rad);
}

////// Y-tests //////

function axisTestY02(a, b, fa, fb, boxHalfSize, v0, v2) {
var p0 = -a * v0.x + b * v0.z;
var p2 = -a * v2.x + b * v2.z;
var min = p2;
var max = p0;
if (p0 < p2) {
min = p0;
max = p2;
}
var rad = fa * boxHalfSize.x + fb * boxHalfSize.z;
return (min > rad || max < -rad);
}

function axisTestY1(a, b, fa, fb, boxHalfSize, v0, v1) {
var p0 = -a * v0.x + b * v0.z;
var p1 = -a * v1.x + b * v1.z;
var min = p1;
var max = p0;
if (p0 < p1) {
min = p0;
max = p1;
}
var rad = fa * boxHalfSize.x + fb * boxHalfSize.z;
return (min > rad || max < -rad);
}

////// Z-tests //////

function axisTestZ12(a, b, fa, fb, boxHalfSize, v1, v2) {
var p1 = a * v1.x - b * v1.y;
var p2 = a * v2.x - b * v2.y;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment.

var min = p1;
var max = p2;
if (p2 < p1) {
min = p2;
max = p1;
}
var rad = fa * boxHalfSize.x + fb * boxHalfSize.y;
return (min > rad || max < -rad);
}

function axisTestZ0(a, b, fa, fb, boxHalfSize, v0, v1) {
var p0 = a * v0.x - b * v0.y;
var p1 = a * v1.x - b * v1.y;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

var min = p1;
var max = p0;
if (p0 < p1) {
min = p0;
max = p1;
}
var rad = fa * boxHalfSize.x + fb * boxHalfSize.y;
return (min > rad || max < -rad);
}

var triangleNormal = new Cartesian3();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below, use Scratch suffix so it is clear these are scratch variables.


var edges = [];
edges.push(new Cartesian3());
edges.push(new Cartesian3());
edges.push(new Cartesian3());

var v0 = new Cartesian3();
var v1 = new Cartesian3();
var v2 = new Cartesian3();

function triBoxOverlap(boxCenter, boxHalfSize, triangle) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this and the scratch variables at the top of the file and the helper function below. This will read better. It relies on "hoisting" the helper functions, but that is OK. Never rely on hoisting variables though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this efficiently use Cesium's AxisAlignedBoundingBox?

// Use the separating axis theorem to test overlap between triangle and box.
// Need to test for overlap in these directions:
// 1) the {x,y,z} directions (testing the AABB of the triangle against the box handles these)
// 2) normal of the triangle
// 3) cross product of edge from triangle with {x, y, z} direction. this is 3x3 = 9 tests.

// Translate the triangle and box so the box is centered at the origin.
Cartesian3.subtract(triangle[0], boxCenter, v0);
Cartesian3.subtract(triangle[1], boxCenter, v1);
Cartesian3.subtract(triangle[2], boxCenter, v2);

// Compute triangle edges
Cartesian3.subtract(v1, v0, edges[0]);
Cartesian3.subtract(v2, v1, edges[1]);
Cartesian3.subtract(v0, v2, edges[2]);

// Bullet 3: Perform edge checks first for early return. Check if each is a separating axis.
var fex = Math.abs(edges[0].x);
var fey = Math.abs(edges[0].y);
var fez = Math.abs(edges[0].z);
if (axisTestX01(edges[0].z, edges[0].y, fez, fey, boxHalfSize, v0, v2)) {
return false;
} else if (axisTestY02(edges[0].z, edges[0].x, fez, fex, boxHalfSize, v0, v2)) {
return false;
} else if (axisTestZ12(edges[0].y, edges[0].x, fey, fex, boxHalfSize, v1, v2)) {
return false;
}

fex = Math.abs(edges[1].x);
fey = Math.abs(edges[1].y);
fez = Math.abs(edges[1].z);
if (axisTestX01(edges[1].z, edges[1].y, fez, fey, boxHalfSize, v0, v2)) {
return false;
} else if (axisTestY02(edges[1].z, edges[1].x, fez, fex, boxHalfSize, v0, v2)) {
return false;
} else if (axisTestZ0(edges[1].y, edges[1].x, fey, fex, boxHalfSize, v0, v1)) {
return false;
}

fex = Math.abs(edges[2].x);
fey = Math.abs(edges[2].y);
fez = Math.abs(edges[2].z);
if (axisTestX2(edges[2].z, edges[2].y, fez, fey, boxHalfSize, v0, v1)) {
return false;
} else if (axisTestY1(edges[2].z, edges[2].x, fez, fex, boxHalfSize, v0, v1)) {
return false;
} else if (axisTestZ12(edges[2].y, edges[2].x, fey, fex, boxHalfSize, v1, v2)) {
return false;
}

// Bullet 1: Perform an AABB test between the triangle's minimum AABB and the box
var triangleAABBmin = Math.min(v0.x, v1.x, v2.x);
var triangleAABBmax = Math.max(v0.x, v1.x, v2.x);
if (triangleAABBmin > boxHalfSize.x || triangleAABBmax < -boxHalfSize.x) {
return false;
}

triangleAABBmin = Math.min(v0.y, v1.y, v2.y);
triangleAABBmax = Math.max(v0.y, v1.y, v2.y);
if (triangleAABBmin > boxHalfSize.y || triangleAABBmax < -boxHalfSize.y) {
return false;
}

triangleAABBmin = Math.min(v0.z, v1.z, v2.z);
triangleAABBmax = Math.max(v0.z, v1.z, v2.z);
if (triangleAABBmin > boxHalfSize.z || triangleAABBmax < -boxHalfSize.z) {
return false;
}

// Bullet 2: Test if the box intersects the plane of the triangle.
triangleNormal = Cartesian3.cross(edges[0], edges[1], triangleNormal);
return planeBoxOverlap(triangleNormal, v0, boxHalfSize);
}
70 changes: 70 additions & 0 deletions specs/lib/triBoxOverlapSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';
var Cesium = require('cesium');
var Cartesian3 = Cesium.Cartesian3;
var triBoxOverlap = require('../../lib/triBoxOverlap');

describe('triBoxOverlap', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these my original tests? How is test coverage? It is important that we test all the case and combinations since we are going to build on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are new, and they don't cover every possible return path although they cover all the helper functions. Do you have comprehensive tests? Otherwise I could write them, but figuring out the geometry for each case could take some time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that code coverage is 100% even if the cyclomatic complexity is not. Also test representative, but perhaps not exhaustive cases, e.g.,

  • Completely inside box
  • Completely outside box in each of the possible 26 voxels (or less)
  • Intersecting parallel with the x plane, y plane, and z plane
  • Diangle intersects
  • Etc

We want to be very confident in this code so when we build on it, we don't have to chase down bugs in a more complex system.

var triangle = [
new Cartesian3(0.0, 0.0, 0.0),
new Cartesian3(1.0, 0.0, 0.0),
new Cartesian3(1.0, 1.0, 0.0)
];

var boxCenter = new Cartesian3();
var halfSize = new Cartesian3();

it('correctly detects no intersection for a completely separate triangle and box', function() {
boxCenter.x = 2.0;
boxCenter.y = 2.0;
boxCenter.z = 2.0;
halfSize.x = 0.5;
halfSize.y = 0.5;
halfSize.z = 0.5;

expect(triBoxOverlap(boxCenter, halfSize, triangle)).toEqual(false);
});

it('correctly detects an intersection when the triangle is entirely inside the box', function() {
boxCenter.x = 0.5;
boxCenter.y = 0.5;
boxCenter.z = 0.0;
halfSize.x = 1.0;
halfSize.y = 1.0;
halfSize.z = 1.0;

expect(triBoxOverlap(boxCenter, halfSize, triangle)).toEqual(true);
});

it('correctly detects an intersection when the box is entirely in the triangle plane', function() {
boxCenter.x = 0.5;
boxCenter.y = 0.5;
boxCenter.z = 0.0;
halfSize.x = 0.1;
halfSize.y = 0.1;
halfSize.z = 0.1;

expect(triBoxOverlap(boxCenter, halfSize, triangle)).toEqual(true);
});

it('correctly detects an intersection when a triangle vertex is in the box', function() {
boxCenter.x = 1.0;
boxCenter.y = 1.0;
boxCenter.z = 0.0;
halfSize.x = 0.5;
halfSize.y = 0.5;
halfSize.z = 0.5;

expect(triBoxOverlap(boxCenter, halfSize, triangle)).toEqual(true);
});

it('correctly detects an intersection when the intersection is on an edge of the triangle', function() {
boxCenter.x = 0.0;
boxCenter.y = 1.0;
boxCenter.z = 0.0;
halfSize.x = 1.1;
halfSize.y = 1.1;
halfSize.z = 1.1;

expect(triBoxOverlap(boxCenter, halfSize, triangle)).toEqual(true);
});
});