games202-hw/hw4/lib/bvhtree.js

649 lines
20 KiB
JavaScript
Raw Normal View History

/**
* bvh-tree
* A Bounding Volume Hierarchy data structure implementation.
* https://github.com/benraziel/bvh-tree
*
* @author Ben Raziel
*/
var bvhtree = bvhtree || {};
bvhtree.EPSILON = 1e-6;
/**
* A 3D Vector class. Based on three.js Vector3
*/
bvhtree.BVHVector3 = function ( x, y, z ) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
};
bvhtree.BVHVector3.prototype = {
constructor: bvhtree.BVHVector3,
copy: function ( v ) {
this.x = v.x;
this.y = v.y;
this.z = v.z;
return this;
},
set: function ( x, y, z ) {
this.x = x;
this.y = y;
this.z = z;
return this;
},
setFromArray: function(array, firstElementPos) {
this.x = array[firstElementPos];
this.y = array[firstElementPos+1];
this.z = array[firstElementPos+2];
},
add: function ( v ) {
this.x += v.x;
this.y += v.y;
this.z += v.z;
return this;
},
multiplyScalar: function ( scalar ) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
return this;
},
subVectors: function ( a, b ) {
this.x = a.x - b.x;
this.y = a.y - b.y;
this.z = a.z - b.z;
return this;
},
dot: function ( v ) {
return this.x * v.x + this.y * v.y + this.z * v.z;
},
cross: function ( v ) {
var x = this.x, y = this.y, z = this.z;
this.x = y * v.z - z * v.y;
this.y = z * v.x - x * v.z;
this.z = x * v.y - y * v.x;
return this;
},
crossVectors: function ( a, b ) {
var ax = a.x, ay = a.y, az = a.z;
var bx = b.x, by = b.y, bz = b.z;
this.x = ay * bz - az * by;
this.y = az * bx - ax * bz;
this.z = ax * by - ay * bx;
return this;
},
clone: function () {
return new bvhtree.BVHVector3( this.x, this.y, this.z );
}
};
/**
* @typedef {Object} Point A Point in 3D space
* @property {number} x x coordinate of the point
* @property {number} y y coordinate of the point
* @property {number} z z coordinate of the point
*
* @typedef Point[3] Triangle A triangle in 3D space
*/
/**
* Constructs a bounding volume heirarchy from a list of triangles
* @class
* @param {Triangle[]} triangles an array of triangles to index. Each triangle is represented as an array of 3 xyz coordinates: [{x: X0, y: Y0, z: Z0}, {x: X1, y: Y1, z: Z1}, {x: X2, y: Y2, z: Z2}]
* @param {number} [maxTrianglesPerNode=10] the maximum number of triangles in each node of the BVH tree. Once this value is reached, that node is split into two child nodes.
*/
bvhtree.BVH = function(triangles, maxTrianglesPerNode) {
var trianglesArray = [];
trianglesArray.length = triangles.length * 9;
for (var i = 0; i < triangles.length; i++) {
var p0 = triangles[i][0];
var p1 = triangles[i][1];
var p2 = triangles[i][2];
trianglesArray[i*9] = p0.x;
trianglesArray[i*9+1] = p0.y;
trianglesArray[i*9+2] = p0.z;
trianglesArray[i*9+3] = p1.x;
trianglesArray[i*9+4] = p1.y;
trianglesArray[i*9+5] = p1.z;
trianglesArray[i*9+6] = p2.x;
trianglesArray[i*9+7] = p2.y;
trianglesArray[i*9+8] = p2.z;
}
this._trianglesArray = trianglesArray;
this._maxTrianglesPerNode = maxTrianglesPerNode || 10;
this._bboxArray = this.calcBoundingBoxes(trianglesArray);
// clone a helper array
this._bboxHelper = new Float32Array(this._bboxArray.length);
this._bboxHelper.set(this._bboxArray);
// create the root node, add all the triangles to it
var triangleCount = trianglesArray.length / 9;
var extents = this.calcExtents(0, triangleCount, bvhtree.EPSILON);
this._rootNode = new bvhtree.BVHNode(extents[0], extents[1], 0, triangleCount, 0);
this._nodesToSplit = [this._rootNode];
while (this._nodesToSplit.length > 0) {
var node = this._nodesToSplit.pop();
this.splitNode(node);
}
};
/**
* returns a list of all the triangles in the BVH which interected a specific node.
* We use the BVH node structure to first cull out nodes which do not intereset the ray.
* For rays that did intersect, we test intersection of the ray with each triangle
* @param {Point} rayOrigin the origin position of the ray.
* @param {Point} rayDirection the direction vector of the ray.
* @param {Boolean} backfaceCulling if 'true', only intersections with front-faces of the mesh will be performed.
* @return IntersectionResult[] an array of intersection result, one for each triangle which intersected the BVH
*
* @typedef {Object} IntersectionResult
* @property Array[] triangle the triangle which the ray intersected
* @property number triangleIndex the position of the interescting triangle in the input triangle array provided to the BVH constructor.
* @property {Point} intersectionPoint the interesection point of the ray on the triangle.
*/
bvhtree.BVH.prototype.intersectRay = function(rayOrigin, rayDirection, backfaceCulling) {
var nodesToIntersect = [this._rootNode];
var trianglesInIntersectingNodes = []; // a list of nodes that intersect the ray (according to their bounding box)
var intersectingTriangles = [];
var i;
var invRayDirection = new bvhtree.BVHVector3(
1.0 / rayDirection.x,
1.0 / rayDirection.y,
1.0 / rayDirection.z
);
// go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray.
// note: these triangles may not intersect the ray themselves
while (nodesToIntersect.length > 0) {
var node = nodesToIntersect.pop();
if (bvhtree.BVH.intersectNodeBox(rayOrigin, invRayDirection, node)) {
if (node._node0) {
nodesToIntersect.push(node._node0);
}
if (node._node1) {
nodesToIntersect.push(node._node1);
}
for (i = node._startIndex; i < node._endIndex; i++) {
trianglesInIntersectingNodes.push(this._bboxArray[i*7]);
}
}
}
// go over the list of candidate triangles, and check each of them using ray triangle intersection
var a = new bvhtree.BVHVector3();
var b = new bvhtree.BVHVector3();
var c = new bvhtree.BVHVector3();
var rayOriginVec3 = new bvhtree.BVHVector3(rayOrigin.x, rayOrigin.y, rayOrigin.z);
var rayDirectionVec3 = new bvhtree.BVHVector3(rayDirection.x, rayDirection.y, rayDirection.z);
for (i = 0; i < trianglesInIntersectingNodes.length; i++) {
var triIndex = trianglesInIntersectingNodes[i];
a.setFromArray(this._trianglesArray, triIndex*9);
b.setFromArray(this._trianglesArray, triIndex*9+3);
c.setFromArray(this._trianglesArray, triIndex*9+6);
var intersectionPoint = bvhtree.BVH.intersectRayTriangle(a, b, c, rayOriginVec3, rayDirectionVec3, backfaceCulling);
if (intersectionPoint) {
intersectingTriangles.push({
triangle: [a.clone(), b.clone(), c.clone()],
triangleIndex: triIndex,
intersectionPoint: intersectionPoint
});
}
}
return intersectingTriangles;
};
/**
* Gets an array of triangle, and calculates the bounding box for each of them, and adds an index to the triangle's position in the array
* Each bbox is saved as 7 values in a Float32Array: (position, minX, minY, minZ, maxX, maxY, maxZ)
*/
bvhtree.BVH.prototype.calcBoundingBoxes = function(trianglesArray) {
var p0x, p0y, p0z;
var p1x, p1y, p1z;
var p2x, p2y, p2z;
var minX, minY, minZ;
var maxX, maxY, maxZ;
var triangleCount = trianglesArray.length / 9;
var bboxArray = new Float32Array(triangleCount * 7);
for (var i = 0; i < triangleCount; i++) {
p0x = trianglesArray[i*9];
p0y = trianglesArray[i*9+1];
p0z = trianglesArray[i*9+2];
p1x = trianglesArray[i*9+3];
p1y = trianglesArray[i*9+4];
p1z = trianglesArray[i*9+5];
p2x = trianglesArray[i*9+6];
p2y = trianglesArray[i*9+7];
p2z = trianglesArray[i*9+8];
minX = Math.min(Math.min(p0x, p1x), p2x);
minY = Math.min(Math.min(p0y, p1y), p2y);
minZ = Math.min(Math.min(p0z, p1z), p2z);
maxX = Math.max(Math.max(p0x, p1x), p2x);
maxY = Math.max(Math.max(p0y, p1y), p2y);
maxZ = Math.max(Math.max(p0z, p1z), p2z);
bvhtree.BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ);
}
return bboxArray;
};
/**
* Calculates the extents (i.e the min and max coordinates) of a list of bounding boxes in the bboxArray
* @param startIndex the index of the first triangle that we want to calc extents for
* @param endIndex the index of the last triangle that we want to calc extents for
* @param expandBy a small epsilon to expand the bbox by, for safety during ray-box intersections
*/
bvhtree.BVH.prototype.calcExtents = function(startIndex, endIndex, expandBy) {
expandBy = expandBy || 0.0;
if (startIndex >= endIndex) {
return [{'x': 0, 'y': 0, 'z': 0}, {'x': 0, 'y': 0, 'z': 0}];
}
var minX = Number.MAX_VALUE;
var minY = Number.MAX_VALUE;
var minZ = Number.MAX_VALUE;
var maxX = -Number.MAX_VALUE;
var maxY = -Number.MAX_VALUE;
var maxZ = -Number.MAX_VALUE;
for (var i = startIndex; i < endIndex; i++) {
minX = Math.min(this._bboxArray[i*7+1], minX);
minY = Math.min(this._bboxArray[i*7+2], minY);
minZ = Math.min(this._bboxArray[i*7+3], minZ);
maxX = Math.max(this._bboxArray[i*7+4], maxX);
maxY = Math.max(this._bboxArray[i*7+5], maxY);
maxZ = Math.max(this._bboxArray[i*7+6], maxZ);
}
return [
{'x': minX - expandBy, 'y': minY - expandBy, 'z': minZ - expandBy},
{'x': maxX + expandBy, 'y': maxY + expandBy, 'z': maxZ + expandBy}
];
};
bvhtree.BVH.prototype.splitNode = function(node) {
if ((node.elementCount() <= this._maxTrianglesPerNode) || (node.elementCount() === 0)) {
return;
}
var startIndex = node._startIndex;
var endIndex = node._endIndex;
var leftNode = [ [],[],[] ];
var rightNode = [ [],[],[] ];
var extentCenters = [node.centerX(), node.centerY(), node.centerZ()];
var extentsLength = [
node._extentsMax.x - node._extentsMin.x,
node._extentsMax.y - node._extentsMin.y,
node._extentsMax.z - node._extentsMin.z
];
var objectCenter = [];
objectCenter.length = 3;
for (var i = startIndex; i < endIndex; i++) {
objectCenter[0] = (this._bboxArray[i * 7 + 1] + this._bboxArray[i * 7 + 4]) * 0.5; // center = (min + max) / 2
objectCenter[1] = (this._bboxArray[i * 7 + 2] + this._bboxArray[i * 7 + 5]) * 0.5; // center = (min + max) / 2
objectCenter[2] = (this._bboxArray[i * 7 + 3] + this._bboxArray[i * 7 + 6]) * 0.5; // center = (min + max) / 2
for (var j = 0; j < 3; j++) {
if (objectCenter[j] < extentCenters[j]) {
leftNode[j].push(i);
}
else {
rightNode[j].push(i);
}
}
}
// check if we couldn't split the node by any of the axes (x, y or z). halt here, dont try to split any more (cause it will always fail, and we'll enter an infinite loop
var splitFailed = [];
splitFailed.length = 3;
splitFailed[0] = (leftNode[0].length === 0) || (rightNode[0].length === 0);
splitFailed[1] = (leftNode[1].length === 0) || (rightNode[1].length === 0);
splitFailed[2] = (leftNode[2].length === 0) || (rightNode[2].length === 0);
if (splitFailed[0] && splitFailed[1] && splitFailed[2]) {
return;
}
// choose the longest split axis. if we can't split by it, choose next best one.
var splitOrder = [0, 1, 2];
splitOrder.sort(function(axis0, axis1) {
return (extentsLength[axis1] - extentsLength[axis0])
});
var leftElements;
var rightElements;
for (j = 0; j < 3; j++) {
var candidateIndex = splitOrder[j];
if (!splitFailed[candidateIndex]) {
leftElements = leftNode[candidateIndex];
rightElements = rightNode[candidateIndex];
break;
}
}
// sort the elements in range (startIndex, endIndex) according to which node they should be at
var node0Start = startIndex;
var node0End = node0Start + leftElements.length;
var node1Start = node0End;
var node1End = endIndex;
var currElement;
var helperPos = node._startIndex;
var concatenatedElements = leftElements.concat(rightElements);
for (i = 0; i < concatenatedElements.length; i++) {
currElement = concatenatedElements[i];
bvhtree.BVH.copyBox(this._bboxArray, currElement, this._bboxHelper, helperPos);
helperPos++;
}
// copy results back to main array
var subArr = this._bboxHelper.subarray(node._startIndex * 7, node._endIndex * 7);
this._bboxArray.set(subArr, node._startIndex * 7);
// create 2 new nodes for the node we just split, and add links to them from the parent node
var node0Extents = this.calcExtents(node0Start, node0End, bvhtree.EPSILON);
var node1Extents = this.calcExtents(node1Start, node1End, bvhtree.EPSILON);
var node0 = new bvhtree.BVHNode(node0Extents[0], node0Extents[1], node0Start, node0End, node._level + 1);
var node1 = new bvhtree.BVHNode(node1Extents[0], node1Extents[1], node1Start, node1End, node._level + 1);
node._node0 = node0;
node._node1 = node1;
node.clearShapes();
// add new nodes to the split queue
this._nodesToSplit.push(node0);
this._nodesToSplit.push(node1);
};
bvhtree.BVH._calcTValues = function(minVal, maxVal, rayOriginCoord, invdir) {
var res = {min: 0, max: 0};
if ( invdir >= 0 ) {
res.min = ( minVal - rayOriginCoord ) * invdir;
res.max = ( maxVal - rayOriginCoord ) * invdir;
} else {
res.min = ( maxVal - rayOriginCoord ) * invdir;
res.max = ( minVal - rayOriginCoord ) * invdir;
}
return res;
};
bvhtree.BVH.intersectNodeBox = function(rayOrigin, invRayDirection, node) {
var t = bvhtree.BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x);
var ty = bvhtree.BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y);
if ( ( t.min > ty.max ) || ( ty.min > t.max ) ) {
return false;
}
// These lines also handle the case where tmin or tmax is NaN
// (result of 0 * Infinity). x !== x returns true if x is NaN
if ( ty.min > t.min || t.min !== t.min ) {
t.min = ty.min;
}
if ( ty.max < t.max || t.max !== t.max ) {
t.max = ty.max;
}
var tz = bvhtree.BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z);
if ( ( t.min > tz.max ) || ( tz.min > t.max ) ) {
return false;
}
if ( tz.min > t.min || t.min !== t.min ) {
t.min = tz.min;
}
if ( tz.max < t.max || t.max !== t.max ) {
t.max = tz.max;
}
//return point closest to the ray (positive side)
if (t.max < 0 ) {
return false;
}
return true;
};
bvhtree.BVH.intersectRayTriangle = (function () {
// Compute the offset origin, edges, and normal.
var diff = new bvhtree.BVHVector3();
var edge1 = new bvhtree.BVHVector3();
var edge2 = new bvhtree.BVHVector3();
var normal = new bvhtree.BVHVector3();
return function (a, b, c, rayOrigin, rayDirection, backfaceCulling) {
// from http://www.geometrictools.com/LibMathematics/Intersection/Wm5IntrRay3Triangle3.cpp
edge1.subVectors(b, a);
edge2.subVectors(c, a);
normal.crossVectors(edge1, edge2);
// Solve Q + t*D = b1*E1 + bL*E2 (Q = kDiff, D = ray direction,
// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
// |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
// |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
// |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
var DdN = rayDirection.dot(normal);
var sign;
if (DdN > 0) {
if (backfaceCulling) {
return null;
}
sign = 1;
} else if (DdN < 0) {
sign = -1;
DdN = -DdN;
} else {
return null;
}
diff.subVectors(rayOrigin, a);
var DdQxE2 = sign * rayDirection.dot(edge2.crossVectors(diff, edge2));
// b1 < 0, no intersection
if (DdQxE2 < 0) {
return null;
}
var DdE1xQ = sign * rayDirection.dot(edge1.cross(diff));
// b2 < 0, no intersection
if (DdE1xQ < 0) {
return null;
}
// b1+b2 > 1, no intersection
if (DdQxE2 + DdE1xQ > DdN) {
return null;
}
// Line intersects triangle, check if ray does.
var QdN = -sign * diff.dot(normal);
// t < 0, no intersection
if (QdN < 0) {
return null;
}
// Ray intersects triangle.
var t = QdN / DdN;
var result = new bvhtree.BVHVector3();
return result.copy( rayDirection ).multiplyScalar( t ).add( rayOrigin );
};
}());
bvhtree.BVH.setBox = function(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ) {
bboxArray[pos*7] = triangleId;
bboxArray[pos*7+1] = minX;
bboxArray[pos*7+2] = minY;
bboxArray[pos*7+3] = minZ;
bboxArray[pos*7+4] = maxX;
bboxArray[pos*7+5] = maxY;
bboxArray[pos*7+6] = maxZ;
};
bvhtree.BVH.copyBox = function(sourceArray, sourcePos, destArray, destPos) {
destArray[destPos*7] = sourceArray[sourcePos*7];
destArray[destPos*7+1] = sourceArray[sourcePos*7+1];
destArray[destPos*7+2] = sourceArray[sourcePos*7+2];
destArray[destPos*7+3] = sourceArray[sourcePos*7+3];
destArray[destPos*7+4] = sourceArray[sourcePos*7+4];
destArray[destPos*7+5] = sourceArray[sourcePos*7+5];
destArray[destPos*7+6] = sourceArray[sourcePos*7+6];
};
bvhtree.BVH.getBox = function(bboxArray, pos, outputBox) {
outputBox.triangleId = bboxArray[pos*7];
outputBox.minX = bboxArray[pos*7+1];
outputBox.minY = bboxArray[pos*7+2];
outputBox.minZ = bboxArray[pos*7+3];
outputBox.maxX = bboxArray[pos*7+4];
outputBox.maxY = bboxArray[pos*7+5];
outputBox.maxZ = bboxArray[pos*7+6];
};
/**
* A node in the BVH structure
* @class
* @param {Point} extentsMin the min coords of this node's bounding box ({x,y,z})
* @param {Point} extentsMax the max coords of this node's bounding box ({x,y,z})
* @param {number} startIndex an index in the bbox array, where the first element of this node is located
* @param {number} endIndex an index in the bbox array, where the last of this node is located, plus 1 (meaning that its non-inclusive).
* @param {number} the distance of this node from the root for the bvh tree. root node has level=0, its children have level=1 etc.
*/
bvhtree.BVHNode = function(extentsMin, extentsMax, startIndex, endIndex, level) {
this._extentsMin = extentsMin;
this._extentsMax = extentsMax;
this._startIndex = startIndex;
this._endIndex = endIndex;
this._level = level;
this._node0 = null;
this._node1 = null;
};
bvhtree.BVHNode.prototype.elementCount = function() {
return this._endIndex - this._startIndex;
};
bvhtree.BVHNode.prototype.centerX = function() {
return (this._extentsMin.x + this._extentsMax.x) * 0.5;
};
bvhtree.BVHNode.prototype.centerY = function() {
return (this._extentsMin.y + this._extentsMax.y) * 0.5;
};
bvhtree.BVHNode.prototype.centerZ = function() {
return (this._extentsMin.z + this._extentsMax.z) * 0.5;
};
bvhtree.BVHNode.prototype.clearShapes = function() {
this._startIndex = -1;
this._endIndex = -1;
};
bvhtree.BVHNode.calcBoundingSphereRadius = function(extentsMin, extentsMax) {
var centerX = (extentsMin.x + extentsMax.x) * 0.5;
var centerY = (extentsMin.y + extentsMax.y) * 0.5;
var centerZ = (extentsMin.z + extentsMax.z) * 0.5;
var extentsMinDistSqr =
(centerX - extentsMin.x) * (centerX - extentsMin.x) +
(centerY - extentsMin.y) * (centerY - extentsMin.y) +
(centerZ - extentsMin.z) * (centerZ - extentsMin.z);
var extentsMaxDistSqr =
(centerX - extentsMax.x) * (centerX - extentsMax.x) +
(centerY - extentsMax.y) * (centerY - extentsMax.y) +
(centerZ - extentsMax.z) * (centerZ - extentsMax.z);
return Math.sqrt(Math.max(extentsMinDistSqr, extentsMaxDistSqr));
};
// commonjs module definiton
if (typeof module !== 'undefined' && module.exports) {
module.exports.BVH = bvhtree.BVH;
module.exports.intersectRay = bvhtree.intersectRay;
}