UNPKG

3d-nft-viewer

Version:

Display 3D NFTs using ThreeJS to render model stored on arweave and minted on DeSo

2,116 lines (1,386 loc) 129 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) : typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MeshBVHLib = global.MeshBVHLib || {}, global.THREE)); })(this, (function (exports, three) { 'use strict'; // 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 three.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 ); 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 ); 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 ); 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 three.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 three.Vector3(); const dir2 = new three.Vector3(); const v02 = new three.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 three.Vector2(); const temp1 = new three.Vector3(); const temp2 = new three.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 three.Vector3(); const projectedPointTemp = new three.Vector3(); const planeTemp = new three.Plane(); const lineTemp = new three.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 SeparatingAxisTriangle extends three.Triangle { constructor( ...args ) { super( ...args ); this.isSeparatingAxisTriangle = true; this.satAxes = new Array( 4 ).fill().map( () => new three.Vector3() ); this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() ); this.points = [ this.a, this.b, this.c ]; this.sphere = new three.Sphere(); this.plane = new three.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; } } SeparatingAxisTriangle.prototype.closestPointToSegment = ( function () { const point1 = new three.Vector3(); const point2 = new three.Vector3(); const edge = new three.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 ); }; } )(); SeparatingAxisTriangle.prototype.intersectsTriangle = ( function () { const saTri2 = new SeparatingAxisTriangle(); const arr1 = new Array( 3 ); const arr2 = new Array( 3 ); const cachedSatBounds = new SeparatingAxisBounds(); const cachedSatBounds2 = new SeparatingAxisBounds(); const cachedAxis = new three.Vector3(); const dir1 = new three.Vector3(); const dir2 = new three.Vector3(); const tempDir = new three.Vector3(); const edge = new three.Line3(); const edge1 = new three.Line3(); const edge2 = new three.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.isSeparatingAxisTriangle ) { saTri2.copy( other ); saTri2.update(); other = saTri2; } else if ( other.needsUpdate ) { other.update(); } 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 ) { const plane1 = this.plane; const plane2 = other.plane; if ( Math.abs( plane1.normal.dot( plane2.normal ) ) > 1.0 - 1e-10 ) { // TODO find two points that intersect on the edges and make that the result console.warn( 'SeparatingAxisTriangle.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 ); } else { // find the edge that intersects the other triangle plane const points1 = this.points; let found1 = false; 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 ); if ( plane2.intersectLine( edge, found1 ? edge1.start : edge1.end ) ) { if ( found1 ) { break; } found1 = true; } } // find the other triangles edge that intersects this plane const points2 = other.points; let found2 = false; 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 ); if ( plane1.intersectLine( edge, found2 ? edge2.start : edge2.end ) ) { if ( found2 ) { break; } found2 = true; } } // 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; } 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; }; } )(); SeparatingAxisTriangle.prototype.distanceToPoint = ( function () { const target = new three.Vector3(); return function distanceToPoint( point ) { this.closestPointToPoint( point, target ); return point.distanceTo( target ); }; } )(); SeparatingAxisTriangle.prototype.distanceToTriangle = ( function () { const point = new three.Vector3(); const point2 = new three.Vector3(); const cornerFields = [ 'a', 'b', 'c' ]; const line1 = new three.Line3(); const line2 = new three.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 three.Box3 { constructor( ...args ) { super( ...args ); this.isOrientedBox = true; this.matrix = new three.Matrix4(); this.invMatrix = new three.Matrix4(); this.points = new Array( 8 ).fill().map( () => new three.Vector3() ); this.satAxes = new Array( 3 ).fill().map( () => new three.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 = 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 SeparatingAxisTriangle(); const pointsArr = new Array( 3 ); const cachedSatBounds = new SeparatingAxisBounds(); const cachedSatBounds2 = new SeparatingAxisBounds(); const cachedAxis = new three.Vector3(); return function intersectsTriangle( triangle ) { if ( this.needsUpdate ) { this.update(); } if ( ! triangle.isSeparatingAxisTriangle ) { 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 three.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 three.Line3() ); const segments2 = new Array( 12 ).fill().map( () => new three.Line3() ); const point1 = new three.Vector3(); const point2 = new three.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 );