3d-nft-viewer
Version:
Display 3D NFTs using ThreeJS to render model stored on arweave and minted on DeSo
2,186 lines (1,434 loc) • 125 kB
JavaScript
import { BufferAttribute, Vector3, Vector2, Plane, Line3, Triangle, Sphere, Box3, Matrix4, BackSide, DoubleSide, FrontSide, Object3D, BufferGeometry, Group, LineBasicMaterial, MeshBasicMaterial, Ray, Mesh, RGBAFormat, RGFormat, RedFormat, RGBAIntegerFormat, RGIntegerFormat, RedIntegerFormat, DataTexture, NearestFilter, IntType, UnsignedIntType, FloatType, UnsignedByteType, UnsignedShortType, ByteType, ShortType } from 'three';
// Split strategy constants
const CENTER = 0;
const AVERAGE = 1;
const SAH = 2;
// Traversal constants
const NOT_INTERSECTED = 0;
const INTERSECTED = 1;
const CONTAINED = 2;
// SAH cost constants
// TODO: hone these costs more. The relative difference between them should be the
// difference in measured time to perform a triangle intersection vs traversing
// bounds.
const TRIANGLE_INTERSECT_COST = 1.25;
const TRAVERSAL_COST = 1;
// Build constants
const BYTES_PER_NODE = 6 * 4 + 4 + 4;
const IS_LEAFNODE_FLAG = 0xFFFF;
// EPSILON for computing floating point error during build
// https://en.wikipedia.org/wiki/Machine_epsilon#Values_for_standard_hardware_floating_point_arithmetics
const FLOAT32_EPSILON = Math.pow( 2, - 24 );
class MeshBVHNode {
constructor() {
// internal nodes have boundingData, left, right, and splitAxis
// leaf nodes have offset and count (referring to primitives in the mesh geometry)
}
}
function arrayToBox( nodeIndex32, array, target ) {
target.min.x = array[ nodeIndex32 ];
target.min.y = array[ nodeIndex32 + 1 ];
target.min.z = array[ nodeIndex32 + 2 ];
target.max.x = array[ nodeIndex32 + 3 ];
target.max.y = array[ nodeIndex32 + 4 ];
target.max.z = array[ nodeIndex32 + 5 ];
return target;
}
function getLongestEdgeIndex( bounds ) {
let splitDimIdx = - 1;
let splitDist = - Infinity;
for ( let i = 0; i < 3; i ++ ) {
const dist = bounds[ i + 3 ] - bounds[ i ];
if ( dist > splitDist ) {
splitDist = dist;
splitDimIdx = i;
}
}
return splitDimIdx;
}
// copys bounds a into bounds b
function copyBounds( source, target ) {
target.set( source );
}
// sets bounds target to the union of bounds a and b
function unionBounds( a, b, target ) {
let aVal, bVal;
for ( let d = 0; d < 3; d ++ ) {
const d3 = d + 3;
// set the minimum values
aVal = a[ d ];
bVal = b[ d ];
target[ d ] = aVal < bVal ? aVal : bVal;
// set the max values
aVal = a[ d3 ];
bVal = b[ d3 ];
target[ d3 ] = aVal > bVal ? aVal : bVal;
}
}
// expands the given bounds by the provided triangle bounds
function expandByTriangleBounds( startIndex, triangleBounds, bounds ) {
for ( let d = 0; d < 3; d ++ ) {
const tCenter = triangleBounds[ startIndex + 2 * d ];
const tHalf = triangleBounds[ startIndex + 2 * d + 1 ];
const tMin = tCenter - tHalf;
const tMax = tCenter + tHalf;
if ( tMin < bounds[ d ] ) {
bounds[ d ] = tMin;
}
if ( tMax > bounds[ d + 3 ] ) {
bounds[ d + 3 ] = tMax;
}
}
}
// compute bounds surface area
function computeSurfaceArea( bounds ) {
const d0 = bounds[ 3 ] - bounds[ 0 ];
const d1 = bounds[ 4 ] - bounds[ 1 ];
const d2 = bounds[ 5 ] - bounds[ 2 ];
return 2 * ( d0 * d1 + d1 * d2 + d2 * d0 );
}
function ensureIndex( geo, options ) {
if ( ! geo.index ) {
const vertexCount = geo.attributes.position.count;
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
let index;
if ( vertexCount > 65535 ) {
index = new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
} else {
index = new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
}
geo.setIndex( new BufferAttribute( index, 1 ) );
for ( let i = 0; i < vertexCount; i ++ ) {
index[ i ] = i;
}
}
}
// Computes the set of { offset, count } ranges which need independent BVH roots. Each
// region in the geometry index that belongs to a different set of material groups requires
// a separate BVH root, so that triangles indices belonging to one group never get swapped
// with triangle indices belongs to another group. For example, if the groups were like this:
//
// [-------------------------------------------------------------]
// |__________________|
// g0 = [0, 20] |______________________||_____________________|
// g1 = [16, 40] g2 = [41, 60]
//
// we would need four BVH roots: [0, 15], [16, 20], [21, 40], [41, 60].
function getRootIndexRanges( geo ) {
if ( ! geo.groups || ! geo.groups.length ) {
return [ { offset: 0, count: geo.index.count / 3 } ];
}
const ranges = [];
const rangeBoundaries = new Set();
for ( const group of geo.groups ) {
rangeBoundaries.add( group.start );
rangeBoundaries.add( group.start + group.count );
}
// note that if you don't pass in a comparator, it sorts them lexicographically as strings :-(
const sortedBoundaries = Array.from( rangeBoundaries.values() ).sort( ( a, b ) => a - b );
for ( let i = 0; i < sortedBoundaries.length - 1; i ++ ) {
const start = sortedBoundaries[ i ], end = sortedBoundaries[ i + 1 ];
ranges.push( { offset: ( start / 3 ), count: ( end - start ) / 3 } );
}
return ranges;
}
// computes the union of the bounds of all of the given triangles and puts the resulting box in target. If
// centroidTarget is provided then a bounding box is computed for the centroids of the triangles, as well.
// These are computed together to avoid redundant accesses to bounds array.
function getBounds( triangleBounds, offset, count, target, centroidTarget = null ) {
let minx = Infinity;
let miny = Infinity;
let minz = Infinity;
let maxx = - Infinity;
let maxy = - Infinity;
let maxz = - Infinity;
let cminx = Infinity;
let cminy = Infinity;
let cminz = Infinity;
let cmaxx = - Infinity;
let cmaxy = - Infinity;
let cmaxz = - Infinity;
const includeCentroid = centroidTarget !== null;
for ( let i = offset * 6, end = ( offset + count ) * 6; i < end; i += 6 ) {
const cx = triangleBounds[ i + 0 ];
const hx = triangleBounds[ i + 1 ];
const lx = cx - hx;
const rx = cx + hx;
if ( lx < minx ) minx = lx;
if ( rx > maxx ) maxx = rx;
if ( includeCentroid && cx < cminx ) cminx = cx;
if ( includeCentroid && cx > cmaxx ) cmaxx = cx;
const cy = triangleBounds[ i + 2 ];
const hy = triangleBounds[ i + 3 ];
const ly = cy - hy;
const ry = cy + hy;
if ( ly < miny ) miny = ly;
if ( ry > maxy ) maxy = ry;
if ( includeCentroid && cy < cminy ) cminy = cy;
if ( includeCentroid && cy > cmaxy ) cmaxy = cy;
const cz = triangleBounds[ i + 4 ];
const hz = triangleBounds[ i + 5 ];
const lz = cz - hz;
const rz = cz + hz;
if ( lz < minz ) minz = lz;
if ( rz > maxz ) maxz = rz;
if ( includeCentroid && cz < cminz ) cminz = cz;
if ( includeCentroid && cz > cmaxz ) cmaxz = cz;
}
target[ 0 ] = minx;
target[ 1 ] = miny;
target[ 2 ] = minz;
target[ 3 ] = maxx;
target[ 4 ] = maxy;
target[ 5 ] = maxz;
if ( includeCentroid ) {
centroidTarget[ 0 ] = cminx;
centroidTarget[ 1 ] = cminy;
centroidTarget[ 2 ] = cminz;
centroidTarget[ 3 ] = cmaxx;
centroidTarget[ 4 ] = cmaxy;
centroidTarget[ 5 ] = cmaxz;
}
}
// A stand alone function for retrieving the centroid bounds.
function getCentroidBounds( triangleBounds, offset, count, centroidTarget ) {
let cminx = Infinity;
let cminy = Infinity;
let cminz = Infinity;
let cmaxx = - Infinity;
let cmaxy = - Infinity;
let cmaxz = - Infinity;
for ( let i = offset * 6, end = ( offset + count ) * 6; i < end; i += 6 ) {
const cx = triangleBounds[ i + 0 ];
if ( cx < cminx ) cminx = cx;
if ( cx > cmaxx ) cmaxx = cx;
const cy = triangleBounds[ i + 2 ];
if ( cy < cminy ) cminy = cy;
if ( cy > cmaxy ) cmaxy = cy;
const cz = triangleBounds[ i + 4 ];
if ( cz < cminz ) cminz = cz;
if ( cz > cmaxz ) cmaxz = cz;
}
centroidTarget[ 0 ] = cminx;
centroidTarget[ 1 ] = cminy;
centroidTarget[ 2 ] = cminz;
centroidTarget[ 3 ] = cmaxx;
centroidTarget[ 4 ] = cmaxy;
centroidTarget[ 5 ] = cmaxz;
}
// reorders `tris` such that for `count` elements after `offset`, elements on the left side of the split
// will be on the left and elements on the right side of the split will be on the right. returns the index
// of the first element on the right side, or offset + count if there are no elements on the right side.
function partition( index, triangleBounds, offset, count, split ) {
let left = offset;
let right = offset + count - 1;
const pos = split.pos;
const axisOffset = split.axis * 2;
// hoare partitioning, see e.g. https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme
while ( true ) {
while ( left <= right && triangleBounds[ left * 6 + axisOffset ] < pos ) {
left ++;
}
// if a triangle center lies on the partition plane it is considered to be on the right side
while ( left <= right && triangleBounds[ right * 6 + axisOffset ] >= pos ) {
right --;
}
if ( left < right ) {
// we need to swap all of the information associated with the triangles at index
// left and right; that's the verts in the geometry index, the bounds,
// and perhaps the SAH planes
for ( let i = 0; i < 3; i ++ ) {
let t0 = index[ left * 3 + i ];
index[ left * 3 + i ] = index[ right * 3 + i ];
index[ right * 3 + i ] = t0;
let t1 = triangleBounds[ left * 6 + i * 2 + 0 ];
triangleBounds[ left * 6 + i * 2 + 0 ] = triangleBounds[ right * 6 + i * 2 + 0 ];
triangleBounds[ right * 6 + i * 2 + 0 ] = t1;
let t2 = triangleBounds[ left * 6 + i * 2 + 1 ];
triangleBounds[ left * 6 + i * 2 + 1 ] = triangleBounds[ right * 6 + i * 2 + 1 ];
triangleBounds[ right * 6 + i * 2 + 1 ] = t2;
}
left ++;
right --;
} else {
return left;
}
}
}
const BIN_COUNT = 32;
const binsSort = ( a, b ) => a.candidate - b.candidate;
const sahBins = new Array( BIN_COUNT ).fill().map( () => {
return {
count: 0,
bounds: new Float32Array( 6 ),
rightCacheBounds: new Float32Array( 6 ),
leftCacheBounds: new Float32Array( 6 ),
candidate: 0,
};
} );
const leftBounds = new Float32Array( 6 );
function getOptimalSplit( nodeBoundingData, centroidBoundingData, triangleBounds, offset, count, strategy ) {
let axis = - 1;
let pos = 0;
// Center
if ( strategy === CENTER ) {
axis = getLongestEdgeIndex( centroidBoundingData );
if ( axis !== - 1 ) {
pos = ( centroidBoundingData[ axis ] + centroidBoundingData[ axis + 3 ] ) / 2;
}
} else if ( strategy === AVERAGE ) {
axis = getLongestEdgeIndex( nodeBoundingData );
if ( axis !== - 1 ) {
pos = getAverage( triangleBounds, offset, count, axis );
}
} else if ( strategy === SAH ) {
const rootSurfaceArea = computeSurfaceArea( nodeBoundingData );
let bestCost = TRIANGLE_INTERSECT_COST * count;
// iterate over all axes
const cStart = offset * 6;
const cEnd = ( offset + count ) * 6;
for ( let a = 0; a < 3; a ++ ) {
const axisLeft = centroidBoundingData[ a ];
const axisRight = centroidBoundingData[ a + 3 ];
const axisLength = axisRight - axisLeft;
const binWidth = axisLength / BIN_COUNT;
// If we have fewer triangles than we're planning to split then just check all
// the triangle positions because it will be faster.
if ( count < BIN_COUNT / 4 ) {
// initialize the bin candidates
const truncatedBins = [ ...sahBins ];
truncatedBins.length = count;
// set the candidates
let b = 0;
for ( let c = cStart; c < cEnd; c += 6, b ++ ) {
const bin = truncatedBins[ b ];
bin.candidate = triangleBounds[ c + 2 * a ];
bin.count = 0;
const {
bounds,
leftCacheBounds,
rightCacheBounds,
} = bin;
for ( let d = 0; d < 3; d ++ ) {
rightCacheBounds[ d ] = Infinity;
rightCacheBounds[ d + 3 ] = - Infinity;
leftCacheBounds[ d ] = Infinity;
leftCacheBounds[ d + 3 ] = - Infinity;
bounds[ d ] = Infinity;
bounds[ d + 3 ] = - Infinity;
}
expandByTriangleBounds( c, triangleBounds, bounds );
}
truncatedBins.sort( binsSort );
// remove redundant splits
let splitCount = count;
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
while ( bi + 1 < splitCount && truncatedBins[ bi + 1 ].candidate === bin.candidate ) {
truncatedBins.splice( bi + 1, 1 );
splitCount --;
}
}
// find the appropriate bin for each triangle and expand the bounds.
for ( let c = cStart; c < cEnd; c += 6 ) {
const center = triangleBounds[ c + 2 * a ];
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
if ( center >= bin.candidate ) {
expandByTriangleBounds( c, triangleBounds, bin.rightCacheBounds );
} else {
expandByTriangleBounds( c, triangleBounds, bin.leftCacheBounds );
bin.count ++;
}
}
}
// expand all the bounds
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
const leftCount = bin.count;
const rightCount = count - bin.count;
// check the cost of this split
const leftBounds = bin.leftCacheBounds;
const rightBounds = bin.rightCacheBounds;
let leftProb = 0;
if ( leftCount !== 0 ) {
leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea;
}
let rightProb = 0;
if ( rightCount !== 0 ) {
rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea;
}
const cost = TRAVERSAL_COST + TRIANGLE_INTERSECT_COST * (
leftProb * leftCount + rightProb * rightCount
);
if ( cost < bestCost ) {
axis = a;
bestCost = cost;
pos = bin.candidate;
}
}
} else {
// reset the bins
for ( let i = 0; i < BIN_COUNT; i ++ ) {
const bin = sahBins[ i ];
bin.count = 0;
bin.candidate = axisLeft + binWidth + i * binWidth;
const bounds = bin.bounds;
for ( let d = 0; d < 3; d ++ ) {
bounds[ d ] = Infinity;
bounds[ d + 3 ] = - Infinity;
}
}
// iterate over all center positions
for ( let c = cStart; c < cEnd; c += 6 ) {
const triCenter = triangleBounds[ c + 2 * a ];
const relativeCenter = triCenter - axisLeft;
// in the partition function if the centroid lies on the split plane then it is
// considered to be on the right side of the split
let binIndex = ~ ~ ( relativeCenter / binWidth );
if ( binIndex >= BIN_COUNT ) binIndex = BIN_COUNT - 1;
const bin = sahBins[ binIndex ];
bin.count ++;
expandByTriangleBounds( c, triangleBounds, bin.bounds );
}
// cache the unioned bounds from right to left so we don't have to regenerate them each time
const lastBin = sahBins[ BIN_COUNT - 1 ];
copyBounds( lastBin.bounds, lastBin.rightCacheBounds );
for ( let i = BIN_COUNT - 2; i >= 0; i -- ) {
const bin = sahBins[ i ];
const nextBin = sahBins[ i + 1 ];
unionBounds( bin.bounds, nextBin.rightCacheBounds, bin.rightCacheBounds );
}
let leftCount = 0;
for ( let i = 0; i < BIN_COUNT - 1; i ++ ) {
const bin = sahBins[ i ];
const binCount = bin.count;
const bounds = bin.bounds;
const nextBin = sahBins[ i + 1 ];
const rightBounds = nextBin.rightCacheBounds;
// dont do anything with the bounds if the new bounds have no triangles
if ( binCount !== 0 ) {
if ( leftCount === 0 ) {
copyBounds( bounds, leftBounds );
} else {
unionBounds( bounds, leftBounds, leftBounds );
}
}
leftCount += binCount;
// check the cost of this split
let leftProb = 0;
let rightProb = 0;
if ( leftCount !== 0 ) {
leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea;
}
const rightCount = count - leftCount;
if ( rightCount !== 0 ) {
rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea;
}
const cost = TRAVERSAL_COST + TRIANGLE_INTERSECT_COST * (
leftProb * leftCount + rightProb * rightCount
);
if ( cost < bestCost ) {
axis = a;
bestCost = cost;
pos = bin.candidate;
}
}
}
}
} else {
console.warn( `MeshBVH: Invalid build strategy value ${ strategy } used.` );
}
return { axis, pos };
}
// returns the average coordinate on the specified axis of the all the provided triangles
function getAverage( triangleBounds, offset, count, axis ) {
let avg = 0;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
avg += triangleBounds[ i * 6 + axis * 2 ];
}
return avg / count;
}
// precomputes the bounding box for each triangle; required for quickly calculating tree splits.
// result is an array of size tris.length * 6 where triangle i maps to a
// [x_center, x_delta, y_center, y_delta, z_center, z_delta] tuple starting at index i * 6,
// representing the center and half-extent in each dimension of triangle i
function computeTriangleBounds( geo, fullBounds ) {
const posAttr = geo.attributes.position;
const posArr = posAttr.array;
const index = geo.index.array;
const triCount = index.length / 3;
const triangleBounds = new Float32Array( triCount * 6 );
// support for an interleaved position buffer
const bufferOffset = posAttr.offset || 0;
let stride = 3;
if ( posAttr.isInterleavedBufferAttribute ) {
stride = posAttr.data.stride;
}
for ( let tri = 0; tri < triCount; tri ++ ) {
const tri3 = tri * 3;
const tri6 = tri * 6;
const ai = index[ tri3 + 0 ] * stride + bufferOffset;
const bi = index[ tri3 + 1 ] * stride + bufferOffset;
const ci = index[ tri3 + 2 ] * stride + bufferOffset;
for ( let el = 0; el < 3; el ++ ) {
const a = posArr[ ai + el ];
const b = posArr[ bi + el ];
const c = posArr[ ci + el ];
let min = a;
if ( b < min ) min = b;
if ( c < min ) min = c;
let max = a;
if ( b > max ) max = b;
if ( c > max ) max = c;
// Increase the bounds size by float32 epsilon to avoid precision errors when
// converting to 32 bit float. Scale the epsilon by the size of the numbers being
// worked with.
const halfExtents = ( max - min ) / 2;
const el2 = el * 2;
triangleBounds[ tri6 + el2 + 0 ] = min + halfExtents;
triangleBounds[ tri6 + el2 + 1 ] = halfExtents + ( Math.abs( min ) + halfExtents ) * FLOAT32_EPSILON;
if ( min < fullBounds[ el ] ) fullBounds[ el ] = min;
if ( max > fullBounds[ el + 3 ] ) fullBounds[ el + 3 ] = max;
}
}
return triangleBounds;
}
function buildTree( geo, options ) {
function triggerProgress( trianglesProcessed ) {
if ( onProgress ) {
onProgress( trianglesProcessed / totalTriangles );
}
}
// either recursively splits the given node, creating left and right subtrees for it, or makes it a leaf node,
// recording the offset and count of its triangles and writing them into the reordered geometry index.
function splitNode( node, offset, count, centroidBoundingData = null, depth = 0 ) {
if ( ! reachedMaxDepth && depth >= maxDepth ) {
reachedMaxDepth = true;
if ( verbose ) {
console.warn( `MeshBVH: Max depth of ${ maxDepth } reached when generating BVH. Consider increasing maxDepth.` );
console.warn( geo );
}
}
// early out if we've met our capacity
if ( count <= maxLeafTris || depth >= maxDepth ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
return node;
}
// Find where to split the volume
const split = getOptimalSplit( node.boundingData, centroidBoundingData, triangleBounds, offset, count, strategy );
if ( split.axis === - 1 ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
return node;
}
const splitOffset = partition( indexArray, triangleBounds, offset, count, split );
// create the two new child nodes
if ( splitOffset === offset || splitOffset === offset + count ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
} else {
node.splitAxis = split.axis;
// create the left child and compute its bounding box
const left = new MeshBVHNode();
const lstart = offset;
const lcount = splitOffset - offset;
node.left = left;
left.boundingData = new Float32Array( 6 );
getBounds( triangleBounds, lstart, lcount, left.boundingData, cacheCentroidBoundingData );
splitNode( left, lstart, lcount, cacheCentroidBoundingData, depth + 1 );
// repeat for right
const right = new MeshBVHNode();
const rstart = splitOffset;
const rcount = count - lcount;
node.right = right;
right.boundingData = new Float32Array( 6 );
getBounds( triangleBounds, rstart, rcount, right.boundingData, cacheCentroidBoundingData );
splitNode( right, rstart, rcount, cacheCentroidBoundingData, depth + 1 );
}
return node;
}
ensureIndex( geo, options );
// Compute the full bounds of the geometry at the same time as triangle bounds because
// we'll need it for the root bounds in the case with no groups and it should be fast here.
// We can't use the geometrying bounding box if it's available because it may be out of date.
const fullBounds = new Float32Array( 6 );
const cacheCentroidBoundingData = new Float32Array( 6 );
const triangleBounds = computeTriangleBounds( geo, fullBounds );
const indexArray = geo.index.array;
const maxDepth = options.maxDepth;
const verbose = options.verbose;
const maxLeafTris = options.maxLeafTris;
const strategy = options.strategy;
const onProgress = options.onProgress;
const totalTriangles = geo.index.count / 3;
let reachedMaxDepth = false;
const roots = [];
const ranges = getRootIndexRanges( geo );
if ( ranges.length === 1 ) {
const range = ranges[ 0 ];
const root = new MeshBVHNode();
root.boundingData = fullBounds;
getCentroidBounds( triangleBounds, range.offset, range.count, cacheCentroidBoundingData );
splitNode( root, range.offset, range.count, cacheCentroidBoundingData );
roots.push( root );
} else {
for ( let range of ranges ) {
const root = new MeshBVHNode();
root.boundingData = new Float32Array( 6 );
getBounds( triangleBounds, range.offset, range.count, root.boundingData, cacheCentroidBoundingData );
splitNode( root, range.offset, range.count, cacheCentroidBoundingData );
roots.push( root );
}
}
return roots;
}
function buildPackedTree( geo, options ) {
// boundingData : 6 float32
// right / offset : 1 uint32
// splitAxis / isLeaf + count : 1 uint32 / 2 uint16
const roots = buildTree( geo, options );
let float32Array;
let uint32Array;
let uint16Array;
const packedRoots = [];
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
for ( let i = 0; i < roots.length; i ++ ) {
const root = roots[ i ];
let nodeCount = countNodes( root );
const buffer = new BufferConstructor( BYTES_PER_NODE * nodeCount );
float32Array = new Float32Array( buffer );
uint32Array = new Uint32Array( buffer );
uint16Array = new Uint16Array( buffer );
populateBuffer( 0, root );
packedRoots.push( buffer );
}
return packedRoots;
function countNodes( node ) {
if ( node.count ) {
return 1;
} else {
return 1 + countNodes( node.left ) + countNodes( node.right );
}
}
function populateBuffer( byteOffset, node ) {
const stride4Offset = byteOffset / 4;
const stride2Offset = byteOffset / 2;
const isLeaf = ! ! node.count;
const boundingData = node.boundingData;
for ( let i = 0; i < 6; i ++ ) {
float32Array[ stride4Offset + i ] = boundingData[ i ];
}
if ( isLeaf ) {
const offset = node.offset;
const count = node.count;
uint32Array[ stride4Offset + 6 ] = offset;
uint16Array[ stride2Offset + 14 ] = count;
uint16Array[ stride2Offset + 15 ] = IS_LEAFNODE_FLAG;
return byteOffset + BYTES_PER_NODE;
} else {
const left = node.left;
const right = node.right;
const splitAxis = node.splitAxis;
let nextUnusedPointer;
nextUnusedPointer = populateBuffer( byteOffset + BYTES_PER_NODE, left );
if ( ( nextUnusedPointer / 4 ) > Math.pow( 2, 32 ) ) {
throw new Error( 'MeshBVH: Cannot store child pointer greater than 32 bits.' );
}
uint32Array[ stride4Offset + 6 ] = nextUnusedPointer / 4;
nextUnusedPointer = populateBuffer( nextUnusedPointer, right );
uint32Array[ stride4Offset + 7 ] = splitAxis;
return nextUnusedPointer;
}
}
}
class SeparatingAxisBounds {
constructor() {
this.min = Infinity;
this.max = - Infinity;
}
setFromPointsField( points, field ) {
let min = Infinity;
let max = - Infinity;
for ( let i = 0, l = points.length; i < l; i ++ ) {
const p = points[ i ];
const val = p[ field ];
min = val < min ? val : min;
max = val > max ? val : max;
}
this.min = min;
this.max = max;
}
setFromPoints( axis, points ) {
let min = Infinity;
let max = - Infinity;
for ( let i = 0, l = points.length; i < l; i ++ ) {
const p = points[ i ];
const val = axis.dot( p );
min = val < min ? val : min;
max = val > max ? val : max;
}
this.min = min;
this.max = max;
}
isSeparated( other ) {
return this.min > other.max || other.min > this.max;
}
}
SeparatingAxisBounds.prototype.setFromBox = ( function () {
const p = new Vector3();
return function setFromBox( axis, box ) {
const boxMin = box.min;
const boxMax = box.max;
let min = Infinity;
let max = - Infinity;
for ( let x = 0; x <= 1; x ++ ) {
for ( let y = 0; y <= 1; y ++ ) {
for ( let z = 0; z <= 1; z ++ ) {
p.x = boxMin.x * x + boxMax.x * ( 1 - x );
p.y = boxMin.y * y + boxMax.y * ( 1 - y );
p.z = boxMin.z * z + boxMax.z * ( 1 - z );
const val = axis.dot( p );
min = Math.min( val, min );
max = Math.max( val, max );
}
}
}
this.min = min;
this.max = max;
};
} )();
const areIntersecting = ( function () {
const cacheSatBounds = new SeparatingAxisBounds();
return function areIntersecting( shape1, shape2 ) {
const points1 = shape1.points;
const satAxes1 = shape1.satAxes;
const satBounds1 = shape1.satBounds;
const points2 = shape2.points;
const satAxes2 = shape2.satAxes;
const satBounds2 = shape2.satBounds;
// check axes of the first shape
for ( let i = 0; i < 3; i ++ ) {
const sb = satBounds1[ i ];
const sa = satAxes1[ i ];
cacheSatBounds.setFromPoints( sa, points2 );
if ( sb.isSeparated( cacheSatBounds ) ) return false;
}
// check axes of the second shape
for ( let i = 0; i < 3; i ++ ) {
const sb = satBounds2[ i ];
const sa = satAxes2[ i ];
cacheSatBounds.setFromPoints( sa, points1 );
if ( sb.isSeparated( cacheSatBounds ) ) return false;
}
};
} )();
const closestPointLineToLine = ( function () {
// https://github.com/juj/MathGeoLib/blob/master/src/Geometry/Line.cpp#L56
const dir1 = new Vector3();
const dir2 = new Vector3();
const v02 = new Vector3();
return function closestPointLineToLine( l1, l2, result ) {
const v0 = l1.start;
const v10 = dir1;
const v2 = l2.start;
const v32 = dir2;
v02.subVectors( v0, v2 );
dir1.subVectors( l1.end, l2.start );
dir2.subVectors( l2.end, l2.start );
// float d0232 = v02.Dot(v32);
const d0232 = v02.dot( v32 );
// float d3210 = v32.Dot(v10);
const d3210 = v32.dot( v10 );
// float d3232 = v32.Dot(v32);
const d3232 = v32.dot( v32 );
// float d0210 = v02.Dot(v10);
const d0210 = v02.dot( v10 );
// float d1010 = v10.Dot(v10);
const d1010 = v10.dot( v10 );
// float denom = d1010*d3232 - d3210*d3210;
const denom = d1010 * d3232 - d3210 * d3210;
let d, d2;
if ( denom !== 0 ) {
d = ( d0232 * d3210 - d0210 * d3232 ) / denom;
} else {
d = 0;
}
d2 = ( d0232 + d * d3210 ) / d3232;
result.x = d;
result.y = d2;
};
} )();
const closestPointsSegmentToSegment = ( function () {
// https://github.com/juj/MathGeoLib/blob/master/src/Geometry/LineSegment.cpp#L187
const paramResult = new Vector2();
const temp1 = new Vector3();
const temp2 = new Vector3();
return function closestPointsSegmentToSegment( l1, l2, target1, target2 ) {
closestPointLineToLine( l1, l2, paramResult );
let d = paramResult.x;
let d2 = paramResult.y;
if ( d >= 0 && d <= 1 && d2 >= 0 && d2 <= 1 ) {
l1.at( d, target1 );
l2.at( d2, target2 );
return;
} else if ( d >= 0 && d <= 1 ) {
// Only d2 is out of bounds.
if ( d2 < 0 ) {
l2.at( 0, target2 );
} else {
l2.at( 1, target2 );
}
l1.closestPointToPoint( target2, true, target1 );
return;
} else if ( d2 >= 0 && d2 <= 1 ) {
// Only d is out of bounds.
if ( d < 0 ) {
l1.at( 0, target1 );
} else {
l1.at( 1, target1 );
}
l2.closestPointToPoint( target1, true, target2 );
return;
} else {
// Both u and u2 are out of bounds.
let p;
if ( d < 0 ) {
p = l1.start;
} else {
p = l1.end;
}
let p2;
if ( d2 < 0 ) {
p2 = l2.start;
} else {
p2 = l2.end;
}
const closestPoint = temp1;
const closestPoint2 = temp2;
l1.closestPointToPoint( p2, true, temp1 );
l2.closestPointToPoint( p, true, temp2 );
if ( closestPoint.distanceToSquared( p2 ) <= closestPoint2.distanceToSquared( p ) ) {
target1.copy( closestPoint );
target2.copy( p2 );
return;
} else {
target1.copy( p );
target2.copy( closestPoint2 );
return;
}
}
};
} )();
const sphereIntersectTriangle = ( function () {
// https://stackoverflow.com/questions/34043955/detect-collision-between-sphere-and-triangle-in-three-js
const closestPointTemp = new Vector3();
const projectedPointTemp = new Vector3();
const planeTemp = new Plane();
const lineTemp = new Line3();
return function sphereIntersectTriangle( sphere, triangle ) {
const { radius, center } = sphere;
const { a, b, c } = triangle;
// phase 1
lineTemp.start = a;
lineTemp.end = b;
const closestPoint1 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint1.distanceTo( center ) <= radius ) return true;
lineTemp.start = a;
lineTemp.end = c;
const closestPoint2 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint2.distanceTo( center ) <= radius ) return true;
lineTemp.start = b;
lineTemp.end = c;
const closestPoint3 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint3.distanceTo( center ) <= radius ) return true;
// phase 2
const plane = triangle.getPlane( planeTemp );
const dp = Math.abs( plane.distanceToPoint( center ) );
if ( dp <= radius ) {
const pp = plane.projectPoint( center, projectedPointTemp );
const cp = triangle.containsPoint( pp );
if ( cp ) return true;
}
return false;
};
} )();
class ExtendedTriangle extends Triangle {
constructor( ...args ) {
super( ...args );
this.isExtendedTriangle = true;
this.satAxes = new Array( 4 ).fill().map( () => new Vector3() );
this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() );
this.points = [ this.a, this.b, this.c ];
this.sphere = new Sphere();
this.plane = new Plane();
this.needsUpdate = false;
}
intersectsSphere( sphere ) {
return sphereIntersectTriangle( sphere, this );
}
update() {
const a = this.a;
const b = this.b;
const c = this.c;
const points = this.points;
const satAxes = this.satAxes;
const satBounds = this.satBounds;
const axis0 = satAxes[ 0 ];
const sab0 = satBounds[ 0 ];
this.getNormal( axis0 );
sab0.setFromPoints( axis0, points );
const axis1 = satAxes[ 1 ];
const sab1 = satBounds[ 1 ];
axis1.subVectors( a, b );
sab1.setFromPoints( axis1, points );
const axis2 = satAxes[ 2 ];
const sab2 = satBounds[ 2 ];
axis2.subVectors( b, c );
sab2.setFromPoints( axis2, points );
const axis3 = satAxes[ 3 ];
const sab3 = satBounds[ 3 ];
axis3.subVectors( c, a );
sab3.setFromPoints( axis3, points );
this.sphere.setFromPoints( this.points );
this.plane.setFromNormalAndCoplanarPoint( axis0, a );
this.needsUpdate = false;
}
}
ExtendedTriangle.prototype.closestPointToSegment = ( function () {
const point1 = new Vector3();
const point2 = new Vector3();
const edge = new Line3();
return function distanceToSegment( segment, target1 = null, target2 = null ) {
const { start, end } = segment;
const points = this.points;
let distSq;
let closestDistanceSq = Infinity;
// check the triangle edges
for ( let i = 0; i < 3; i ++ ) {
const nexti = ( i + 1 ) % 3;
edge.start.copy( points[ i ] );
edge.end.copy( points[ nexti ] );
closestPointsSegmentToSegment( edge, segment, point1, point2 );
distSq = point1.distanceToSquared( point2 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
}
}
// check end points
this.closestPointToPoint( start, point1 );
distSq = start.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( start );
}
this.closestPointToPoint( end, point1 );
distSq = end.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( end );
}
return Math.sqrt( closestDistanceSq );
};
} )();
ExtendedTriangle.prototype.intersectsTriangle = ( function () {
const saTri2 = new ExtendedTriangle();
const arr1 = new Array( 3 );
const arr2 = new Array( 3 );
const cachedSatBounds = new SeparatingAxisBounds();
const cachedSatBounds2 = new SeparatingAxisBounds();
const cachedAxis = new Vector3();
const dir1 = new Vector3();
const dir2 = new Vector3();
const tempDir = new Vector3();
const edge = new Line3();
const edge1 = new Line3();
const edge2 = new Line3();
// TODO: If the triangles are coplanar and intersecting the target is nonsensical. It should at least
// be a line contained by both triangles if not a different special case somehow represented in the return result.
return function intersectsTriangle( other, target = null ) {
if ( this.needsUpdate ) {
this.update();
}
if ( ! other.isExtendedTriangle ) {
saTri2.copy( other );
saTri2.update();
other = saTri2;
} else if ( other.needsUpdate ) {
other.update();
}
const plane1 = this.plane;
const plane2 = other.plane;
if ( Math.abs( plane1.normal.dot( plane2.normal ) ) > 1.0 - 1e-10 ) {
// perform separating axis intersection test only for coplanar triangles
const satBounds1 = this.satBounds;
const satAxes1 = this.satAxes;
arr2[ 0 ] = other.a;
arr2[ 1 ] = other.b;
arr2[ 2 ] = other.c;
for ( let i = 0; i < 4; i ++ ) {
const sb = satBounds1[ i ];
const sa = satAxes1[ i ];
cachedSatBounds.setFromPoints( sa, arr2 );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
const satBounds2 = other.satBounds;
const satAxes2 = other.satAxes;
arr1[ 0 ] = this.a;
arr1[ 1 ] = this.b;
arr1[ 2 ] = this.c;
for ( let i = 0; i < 4; i ++ ) {
const sb = satBounds2[ i ];
const sa = satAxes2[ i ];
cachedSatBounds.setFromPoints( sa, arr1 );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
// check crossed axes
for ( let i = 0; i < 4; i ++ ) {
const sa1 = satAxes1[ i ];
for ( let i2 = 0; i2 < 4; i2 ++ ) {
const sa2 = satAxes2[ i2 ];
cachedAxis.crossVectors( sa1, sa2 );
cachedSatBounds.setFromPoints( cachedAxis, arr1 );
cachedSatBounds2.setFromPoints( cachedAxis, arr2 );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
}
if ( target ) {
// TODO find two points that intersect on the edges and make that the result
console.warn( 'ExtendedTriangle.intersectsTriangle: Triangles are coplanar which does not support an output edge. Setting edge to 0, 0, 0.' );
target.start.set( 0, 0, 0 );
target.end.set( 0, 0, 0 );
}
return true;
} else {
// find the edge that intersects the other triangle plane
const points1 = this.points;
let found1 = false;
let count1 = 0;
for ( let i = 0; i < 3; i ++ ) {
const p1 = points1[ i ];
const p2 = points1[ ( i + 1 ) % 3 ];
edge.start.copy( p1 );
edge.end.copy( p2 );
edge.delta( dir1 );
if ( plane2.normal.dot( dir1 ) === 0 && plane2.distanceToPoint( edge.start ) === 0 ) {
// if the edge lies on the plane then take the line
edge1.copy( edge );
count1 = 2;
break;
} else if ( plane2.intersectLine( edge, found1 ? edge1.start : edge1.end ) ) {
count1 ++;
if ( found1 ) {
break;
}
found1 = true;
}
}
if ( count1 !== 2 ) {
return false;
}
// find the other triangles edge that intersects this plane
const points2 = other.points;
let found2 = false;
let count2 = 0;
for ( let i = 0; i < 3; i ++ ) {
const p1 = points2[ i ];
const p2 = points2[ ( i + 1 ) % 3 ];
edge.start.copy( p1 );
edge.end.copy( p2 );
edge.delta( dir2 );
if ( plane1.normal.dot( dir2 ) === 0 && plane1.distanceToPoint( edge.start ) === 0 ) {
// if the edge lies on the plane then take the line
edge2.copy( edge );
count2 = 2;
break;
} else if ( plane1.intersectLine( edge, found2 ? edge2.start : edge2.end ) ) {
count2 ++;
if ( found2 ) {
break;
}
found2 = true;
}
}
if ( count2 !== 2 ) {
return false;
}
// find swap the second edge so both lines are running the same direction
edge1.delta( dir1 );
edge2.delta( dir2 );
if ( dir1.dot( dir2 ) < 0 ) {
let tmp = edge2.start;
edge2.start = edge2.end;
edge2.end = tmp;
}
// check if the edges are overlapping
const s1 = edge1.start.dot( dir1 );
const e1 = edge1.end.dot( dir1 );
const s2 = edge2.start.dot( dir1 );
const e2 = edge2.end.dot( dir1 );
const separated1 = e1 < s2;
const separated2 = s1 < e2;
if ( s1 !== e2 && s2 !== e1 && separated1 === separated2 ) {
return false;
}
// assign the target output
if ( target ) {
tempDir.subVectors( edge1.start, edge2.start );
if ( tempDir.dot( dir1 ) > 0 ) {
target.start.copy( edge1.start );
} else {
target.start.copy( edge2.start );
}
tempDir.subVectors( edge1.end, edge2.end );
if ( tempDir.dot( dir1 ) < 0 ) {
target.end.copy( edge1.end );
} else {
target.end.copy( edge2.end );
}
}
return true;
}
};
} )();
ExtendedTriangle.prototype.distanceToPoint = ( function () {
const target = new Vector3();
return function distanceToPoint( point ) {
this.closestPointToPoint( point, target );
return point.distanceTo( target );
};
} )();
ExtendedTriangle.prototype.distanceToTriangle = ( function () {
const point = new Vector3();
const point2 = new Vector3();
const cornerFields = [ 'a', 'b', 'c' ];
const line1 = new Line3();
const line2 = new Line3();
return function distanceToTriangle( other, target1 = null, target2 = null ) {
const lineTarget = target1 || target2 ? line1 : null;
if ( this.intersectsTriangle( other, lineTarget ) ) {
if ( target1 || target2 ) {
if ( target1 ) lineTarget.getCenter( target1 );
if ( target2 ) lineTarget.getCenter( target2 );
}
return 0;
}
let closestDistanceSq = Infinity;
// check all point distances
for ( let i = 0; i < 3; i ++ ) {
let dist;
const field = cornerFields[ i ];
const otherVec = other[ field ];
this.closestPointToPoint( otherVec, point );
dist = otherVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( otherVec );
}
const thisVec = this[ field ];
other.closestPointToPoint( thisVec, point );
dist = thisVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( thisVec );
if ( target2 ) target2.copy( point );
}
}
for ( let i = 0; i < 3; i ++ ) {
const f11 = cornerFields[ i ];
const f12 = cornerFields[ ( i + 1 ) % 3 ];
line1.set( this[ f11 ], this[ f12 ] );
for ( let i2 = 0; i2 < 3; i2 ++ ) {
const f21 = cornerFields[ i2 ];
const f22 = cornerFields[ ( i2 + 1 ) % 3 ];
line2.set( other[ f21 ], other[ f22 ] );
closestPointsSegmentToSegment( line1, line2, point, point2 );
const dist = point.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( point2 );
}
}
}
return Math.sqrt( closestDistanceSq );
};
} )();
class OrientedBox extends Box3 {
constructor( ...args ) {
super( ...args );
this.isOrientedBox = true;
this.matrix = new Matrix4();
this.invMatrix = new Matrix4();
this.points = new Array( 8 ).fill().map( () => new Vector3() );
this.satAxes = new Array( 3 ).fill().map( () => new Vector3() );
this.satBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() );
this.alignedSatBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() );
this.needsUpdate = false;
}
set( min, max, matrix ) {
super.set( min, max );
this.matrix.copy( matrix );
this.needsUpdate = true;
}
copy( other ) {
super.copy( other );
this.matrix.copy( other.matrix );
this.needsUpdate = true;
}
}
OrientedBox.prototype.update = ( function () {
return function update() {
const matrix = this.matrix;
const min = this.min;
const max = this.max;
const points = this.points;
for ( let x = 0; x <= 1; x ++ ) {
for ( let y = 0; y <= 1; y ++ ) {
for ( let z = 0; z <= 1; z ++ ) {
const i = ( ( 1 << 0 ) * x ) | ( ( 1 << 1 ) * y ) | ( ( 1 << 2 ) * z );
const v = points[ i ];
v.x = x ? max.x : min.x;
v.y = y ? max.y : min.y;
v.z = z ? max.z : min.z;
v.applyMatrix4( matrix );
}
}
}
const satBounds = this.satBounds;
const satAxes = this.satAxes;
const minVec = points[ 0 ];
for ( let i = 0; i < 3; i ++ ) {
const axis = satAxes[ i ];
const sb = satBounds[ i ];
const index = 1 << i;
const pi = points[ index ];
axis.subVectors( minVec, pi );
sb.setFromPoints( axis, points );
}
const alignedSatBounds = this.alignedSatBounds;
alignedSatBounds[ 0 ].setFromPointsField( points, 'x' );
alignedSatBounds[ 1 ].setFromPointsField( points, 'y' );
alignedSatBounds[ 2 ].setFromPointsField( points, 'z' );
this.invMatrix.copy( this.matrix ).invert();
this.needsUpdate = false;
};
} )();
OrientedBox.prototype.intersectsBox = ( function () {
const aabbBounds = new SeparatingAxisBounds();
return function intersectsBox( box ) {
// TODO: should this be doing SAT against the AABB?
if ( this.needsUpdate ) {
this.update();
}
const min = box.min;
const max = box.max;
const satBounds = this.satBounds;
const satAxes = this.satAxes;
const alignedSatBounds = this.alignedSatBounds;
aabbBounds.min = min.x;
aabbBounds.max = max.x;
if ( alignedSatBounds[ 0 ].isSeparated( aabbBounds ) ) return false;
aabbBounds.min = min.y;
aabbBounds.max = max.y;
if ( alignedSatBounds[ 1 ].isSeparated( aabbBounds ) ) return false;
aabbBounds.min = min.z;
aabbBounds.max = max.z;
if ( alignedSatBounds[ 2 ].isSeparated( aabbBounds ) ) return false;
for ( let i = 0; i < 3; i ++ ) {
const axis = satAxes[ i ];
const sb = satBounds[ i ];
aabbBounds.setFromBox( axis, box );
if ( sb.isSeparated( aabbBounds ) ) return false;
}
return true;
};
} )();
OrientedBox.prototype.intersectsTriangle = ( function () {
const saTri = new ExtendedTriangle();
const pointsArr = new Array( 3 );
const cachedSatBounds = new SeparatingAxisBounds();
const cachedSatBounds2 = new SeparatingAxisBounds();
const cachedAxis = new Vector3();
return function intersectsTriangle( triangle ) {
if ( this.needsUpdate ) {
this.update();
}
if ( ! triangle.isExtendedTriangle ) {
saTri.copy( triangle );
saTri.update();
triangle = saTri;
} else if ( triangle.needsUpdate ) {
triangle.update();
}
const satBounds = this.satBounds;
const satAxes = this.satAxes;
pointsArr[ 0 ] = triangle.a;
pointsArr[ 1 ] = triangle.b;
pointsArr[ 2 ] = triangle.c;
for ( let i = 0; i < 3; i ++ ) {
const sb = satBounds[ i ];
const sa = satAxes[ i ];
cachedSatBounds.setFromPoints( sa, pointsArr );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
const triSatBounds = triangle.satBounds;
const triSatAxes = triangle.satAxes;
const points = this.points;
for ( let i = 0; i < 3; i ++ ) {
const sb = triSatBounds[ i ];
const sa = triSatAxes[ i ];
cachedSatBounds.setFromPoints( sa, points );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
// check crossed axes
for ( let i = 0; i < 3; i ++ ) {
const sa1 = satAxes[ i ];
for ( let i2 = 0; i2 < 4; i2 ++ ) {
const sa2 = triSatAxes[ i2 ];
cachedAxis.crossVectors( sa1, sa2 );
cachedSatBounds.setFromPoints( cachedAxis, pointsArr );
cachedSatBounds2.setFromPoints( cachedAxis, points );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
}
return true;
};
} )();
OrientedBox.prototype.closestPointToPoint = ( function () {
return function closestPointToPoint( point, target1 ) {
if ( this.needsUpdate ) {
this.update();
}
target1
.copy( point )
.applyMatrix4( this.invMatrix )
.clamp( this.min, this.max )
.applyMatrix4( this.matrix );
return target1;
};
} )();
OrientedBox.prototype.distanceToPoint = ( function () {
const target = new Vector3();
return function distanceToPoint( point ) {
this.closestPointToPoint( point, target );
return point.distanceTo( target );
};
} )();
OrientedBox.prototype.distanceToBox = ( function () {
const xyzFields = [ 'x', 'y', 'z' ];
const segments1 = new Array( 12 ).fill().map( () => new Line3() );
const segments2 = new Array( 12 ).fill().map( () => new Line3() );
const point1 = new Vector3();
const point2 = new Vector3();
// early out if we find a value below threshold
return function distanceToBox( box, threshold = 0, target1 = null, target2 = null ) {
if ( this.needsUpdate ) {
this.update();
}
if ( this.intersectsBox( box ) ) {
if ( target1 || target2 ) {
box.getCenter( point2 );
this.closestPointToPoint( point2, point1 );
box.closestPointToPoint( point1, point2 );
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
}
return 0;
}
const threshold2 = threshold * threshold;
const min = box.min;
const max = box.max;
const points = this.points;
// iterate over every edge and compare distances
let closestDistanceSq = Infinity;
// check over all these points
for ( let i = 0; i < 8; i ++ ) {
const p = points[ i ];
point2.copy( p ).clamp( min, max );
const dist = p.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( p );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
// generate and check all line segment distances
let count = 0;
for ( let i = 0; i < 3; i ++ ) {
for ( let i1 = 0; i1 <= 1; i1 ++ ) {
for ( let i2 = 0; i2 <= 1; i2 ++ ) {
const nextIndex = ( i + 1 ) % 3;
const nextIndex2 = ( i + 2 ) % 3;
// get obb line segments
const index = i1 << nextIndex | i2 << nextIndex2;
const index2 = 1 << i | i1 << nextIndex | i2 << nextIndex2;
const p1 = points[ index ];
const p2 = points[ index2 ];
const line1 = segments1[ count ];
line1.set( p1, p2 );
// get aabb line segments
const f1 = xyzFields[ i ];
const f2 = xyzFields[ nextIndex ];
const f3 = xyzFields[ nextIndex2 ];
const line2 = segments2[ count ];
const start = line2.start;
const end = line2.end;
start[ f1 ] = min[ f1 ];
start[ f2 ] = i1 ? min[ f2 ] : max[ f2 ];
start[ f3 ] = i2 ? min[ f3 ] : max[ f2 ];
end[ f1 ] = max[ f1 ];
end[ f2 ] = i1 ? min[ f2 ] : max[ f2 ];
end[ f3 ] = i2 ? min[ f3 ] : max[ f2 ];
count ++;
}
}
}
// check all the other boxes point
for ( let x = 0; x <= 1; x ++ ) {
for ( let y = 0; y <= 1; y ++ ) {
for ( let z = 0; z <= 1; z ++ ) {
point2.x = x ? max.x : min.x;
point2.y = y ? max.y : min.y;
point2.z = z ? max.z : min.z;
this.closestPointToPoint( point2, point1 );
const dist = point2.distanceToSquared( point1 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
}
}
for ( let i = 0; i < 12; i ++ ) {
const l1 = segments1[ i ];
for ( let i2 = 0; i2 < 12; i2 ++ ) {
const l2 = segments2[ i2 ];
closestPointsSegmentToSegment( l1, l2, point1, point2 );
const dist = point1.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
}
return Math.sqrt( closestDistanceSq );
};
} )();
// Ripped and modified From THREE.js Mesh raycast
// https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L115
const vA = /* @__PURE__