diff --git a/example/testToRemove.html b/example/testToRemove.html new file mode 100644 index 000000000..49a724dd1 --- /dev/null +++ b/example/testToRemove.html @@ -0,0 +1,23 @@ + + + + three-mesh-bvh - Complex Geometry Raycasting + + + + + + + + diff --git a/example/testToRemove.js b/example/testToRemove.js new file mode 100644 index 000000000..91cbbefff --- /dev/null +++ b/example/testToRemove.js @@ -0,0 +1,133 @@ +import * as THREE from 'three'; +import { computeBoundsTree, SAH } from '../src'; + +THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree; + +const spawnPointRadius = 20; +const radius = 10; // if radius 100 and tube 0.1 and spawnRadius 100, sort works really good. +const tube = 0.1; +const segmentsMultiplier = 32; +const maxDepthSorted = 4; +const maxLeafTris = 10; +const strategy = SAH; + +const tries = 1000; +const seed = 20000; + +// const geometry = new THREE.SphereGeometry( radius, 8 * segmentsMultiplier, 4 * segmentsMultiplier ); +const geometry = new THREE.TorusKnotGeometry( radius, tube, 64 * segmentsMultiplier, 8 * segmentsMultiplier ); + +geometry.computeBoundsTree( { maxLeafTris, strategy } ); + +export class PRNG { + + constructor( seed ) { + + this._seed = seed; + + } + + next() { + + let t = ( this._seed += 0x6d2b79f5 ); + t = Math.imul( t ^ ( t >>> 15 ), t | 1 ); + t ^= t + Math.imul( t ^ ( t >>> 7 ), t | 61 ); + return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296; + + } + + range( min, max ) { + + return min + ( max - min ) * this.next(); + + } + +} + +const bvh = geometry.boundsTree; +const target = {}; + +const r = new PRNG( seed ); +const points = new Array( tries ); + +function generatePoints() { + + for ( let i = 0; i < tries; i ++ ) { + + points[ i ] = new THREE.Vector3( r.range( - spawnPointRadius, spawnPointRadius ), r.range( - spawnPointRadius, spawnPointRadius ), r.range( - spawnPointRadius, spawnPointRadius ) ); + + } + +} + + +// // TEST EQUALS RESULTS + +// generatePoints(); +// const target2 = {}; +// for ( let i = 0; i < tries; i ++ ) { + +// bvh.closestPointToPoint( points[ i ], target ); +// bvh.closestPointToPointHybrid( points[ i ], target2 ); + +// if ( target.distance !== target2.distance ) { + +// const diff = target.distance - target2.distance; +// console.error( "error: " + ( diff / target2.distance * 100 ) + "%" ); + +// } + +// } + +// TEST PERFORMANCE + +function benchmark() { + + generatePoints(); + + const startOld = performance.now(); + + for ( let i = 0; i < tries; i ++ ) { + + bvh.closestPointToPointOld( points[ i ], target ); + + } + + const endOld = performance.now() - startOld; + const startNew = performance.now(); + + for ( let i = 0; i < tries; i ++ ) { + + bvh.closestPointToPoint( points[ i ], target ); + + } + + const endNew = performance.now() - startNew; + const startSort = performance.now(); + + for ( let i = 0; i < tries; i ++ ) { + + bvh.closestPointToPointSort( points[ i ], target ); + + } + + const endSort = performance.now() - startSort; + const startHybrid = performance.now(); + + for ( let i = 0; i < tries; i ++ ) { + + bvh.closestPointToPointHybrid( points[ i ], target, maxDepthSorted ); + + } + + const endHybrid = performance.now() - startHybrid; + + const bestEnd = Math.min( endSort, endNew, endHybrid ); + const best = bestEnd === endSort ? "Sorted" : ( bestEnd === endNew ? "New" : "Hybrid" ); + + console.log( `New: ${endNew.toFixed( 1 )}ms / Sorted: ${endSort.toFixed( 1 )}ms / Hybrid: ${endHybrid.toFixed( 1 )}ms / Old: ${endOld.toFixed( 1 )}ms / Diff: ${( ( 1 - ( endOld / bestEnd ) ) * 100 ).toFixed( 2 )} % / Best: ${best}` ); + +} + +benchmark(); +setInterval( () => benchmark(), 1000 ); diff --git a/src/core/MeshBVH.js b/src/core/MeshBVH.js index 95ac647c5..14bab17b9 100644 --- a/src/core/MeshBVH.js +++ b/src/core/MeshBVH.js @@ -5,7 +5,13 @@ import { OrientedBox } from '../math/OrientedBox.js'; import { arrayToBox } from '../utils/ArrayBoxUtilities.js'; import { ExtendedTrianglePool } from '../utils/ExtendedTrianglePool.js'; import { shapecast } from './cast/shapecast.js'; -import { closestPointToPoint } from './cast/closestPointToPoint.js'; +import { closestPointToPoint } from './cast/closestPointToPoint.generated.js'; +import { closestPointToPoint_indirect } from './cast/closestPointToPoint_indirect.generated.js'; +import { closestPointToPointSort } from './cast/closestPointToPointSort.generated.js'; +import { closestPointToPointSort_indirect } from './cast/closestPointToPointSort_indirect.generated.js'; +import { closestPointToPointHybrid } from './cast/closestPointToPointHybrid.generated.js'; +import { closestPointToPointHybrid_indirect } from './cast/closestPointToPointHybrid_indirect.generated.js'; +import { closestPointToPointOld } from './cast/closestPointToPoint.js'; // REMOVE AFTER TEST import { iterateOverTriangles } from './utils/iterationUtils.generated.js'; import { refit } from './cast/refit.generated.js'; @@ -519,7 +525,90 @@ export class MeshBVH { closestPointToPoint( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) { - return closestPointToPoint( + const closestPointToPointFunc = this.indirect ? closestPointToPoint_indirect : closestPointToPoint; + const roots = this._roots; + let result = null; + + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + result = closestPointToPointFunc( + this, + i, + point, + target, + minThreshold, + maxThreshold, + ); + + // fix here, check old result and new + + if ( result && result.distance <= minThreshold ) break; + + } + + return result; + + } + + closestPointToPointSort( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) { + + const closestPointToPointFunc = this.indirect ? closestPointToPointSort_indirect : closestPointToPointSort; + const roots = this._roots; + let result = null; + + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + result = closestPointToPointFunc( + this, + i, + point, + target, + minThreshold, + maxThreshold, + ); + + // fix here, check old result and new + + if ( result && result.distance <= minThreshold ) break; + + } + + return result; + + } + + closestPointToPointHybrid( point, target = { }, sortedListMaxCount = 16, minThreshold = 0, maxThreshold = Infinity ) { + + const closestPointToPointFunc = this.indirect ? closestPointToPointHybrid_indirect : closestPointToPointHybrid; + const roots = this._roots; + let result = null; + + for ( let i = 0, l = roots.length; i < l; i ++ ) { + + result = closestPointToPointFunc( + this, + i, + point, + target, + sortedListMaxCount, + minThreshold, + maxThreshold, + ); + + // fix here, check old result and new + + if ( result && result.distance <= minThreshold ) break; + + } + + return result; + + } + + // REMOVE AFTER TEST + closestPointToPointOld( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) { + + return closestPointToPointOld( this, point, target, diff --git a/src/core/cast/closestPointToPoint.js b/src/core/cast/closestPointToPoint.js index c5ae1a462..dd039d8fc 100644 --- a/src/core/cast/closestPointToPoint.js +++ b/src/core/cast/closestPointToPoint.js @@ -1,9 +1,11 @@ import { Vector3 } from 'three'; +// DELETE THIS AFTER TEST + const temp = /* @__PURE__ */ new Vector3(); const temp1 = /* @__PURE__ */ new Vector3(); -export function closestPointToPoint( +export function closestPointToPointOld( bvh, point, target = { }, diff --git a/src/core/cast/closestPointToPoint.template.js b/src/core/cast/closestPointToPoint.template.js new file mode 100644 index 000000000..7fb935f62 --- /dev/null +++ b/src/core/cast/closestPointToPoint.template.js @@ -0,0 +1,117 @@ +import { Vector3 } from 'three'; +import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF } from '../utils/nodeBufferUtils.js'; +import { BufferStack } from '../utils/BufferStack.js'; +import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js'; +import { setTriangle } from '../../utils/TriangleUtilities.js'; +import { closestDistanceSquaredPointToBox } from '../utils/distanceUtils.js'; + +const temp = /* @__PURE__ */ new Vector3(); +const temp1 = /* @__PURE__ */ new Vector3(); + +export function closestPointToPoint/* @echo INDIRECT_STRING */( + bvh, + root, + point, + target, + minThreshold, + maxThreshold +) { + + const minThresholdSq = minThreshold * minThreshold; + const maxThresholdSq = maxThreshold * maxThreshold; + let closestDistanceSq = Infinity; + let closestDistanceTriIndex = null; + + const { geometry } = bvh; + const { index } = geometry; + const pos = geometry.attributes.position; + const triangle = ExtendedTrianglePool.getPrimitive(); + + BufferStack.setBuffer( bvh._roots[ root ] ); + const { float32Array, uint16Array, uint32Array } = BufferStack; + _closestPointToPoint( root ); + BufferStack.clearBuffer(); + + if ( closestDistanceSq === Infinity ) return null; + + const closestDistance = Math.sqrt( closestDistanceSq ); + + if ( ! target.point ) target.point = temp1.clone(); + else target.point.copy( temp1 ); + target.distance = closestDistance; + target.faceIndex = closestDistanceTriIndex; + + return target; + + + // early out if under minThreshold + // skip checking if over maxThreshold + // set minThreshold = maxThreshold to quickly check if a point is within a threshold + // returns Infinity if no value found + function _closestPointToPoint( nodeIndex32 ) { + + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + /* @if INDIRECT */ + + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, index, pos ); + + /* @else */ + + setTriangle( triangle, i * 3, index, pos ); + + /* @endif */ + + triangle.needsUpdate = true; + + triangle.closestPointToPoint( point, temp ); + const distSq = point.distanceToSquared( temp ); + if ( distSq < closestDistanceSq ) { + + temp1.copy( temp ); + closestDistanceSq = distSq; + closestDistanceTriIndex = i; + + if ( distSq < minThresholdSq ) return true; + + } + + } + + return; + + } + + const leftIndex = LEFT_NODE( nodeIndex32 ); + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + + const leftDistance = closestDistanceSquaredPointToBox( leftIndex, float32Array, point ); + const rightDistance = closestDistanceSquaredPointToBox( rightIndex, float32Array, point ); + + if ( leftDistance <= rightDistance ) { + + if ( leftDistance < closestDistanceSq && leftDistance < maxThresholdSq ) { + + if ( _closestPointToPoint( leftIndex ) ) return true; + if ( rightDistance < closestDistanceSq ) return _closestPointToPoint( rightIndex ); + + } + + } else if ( rightDistance < closestDistanceSq && rightDistance < maxThresholdSq ) { + + if ( _closestPointToPoint( rightIndex ) ) return true; + if ( leftDistance < closestDistanceSq ) return _closestPointToPoint( leftIndex ); + + } + + } + +} diff --git a/src/core/cast/closestPointToPointHybrid.template.js b/src/core/cast/closestPointToPointHybrid.template.js new file mode 100644 index 000000000..5271954e8 --- /dev/null +++ b/src/core/cast/closestPointToPointHybrid.template.js @@ -0,0 +1,181 @@ +import { Vector3 } from 'three'; +import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF } from '../utils/nodeBufferUtils.js'; +import { BufferStack } from '../utils/BufferStack.js'; +import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js'; +import { setTriangle } from '../../utils/TriangleUtilities.js'; +import { closestDistanceSquaredPointToBox } from '../utils/distanceUtils.js'; +import { SortedListDesc } from '../utils/SortedListDesc.js'; + +const temp = /* @__PURE__ */ new Vector3(); +const temp1 = /* @__PURE__ */ new Vector3(); +const sortedList = new SortedListDesc(); + +export function closestPointToPointHybrid/* @echo INDIRECT_STRING */( + bvh, + root, + point, + target, + maxDepthSorted, + minThreshold, + maxThreshold +) { + + const minThresholdSq = minThreshold * minThreshold; + const maxThresholdSq = maxThreshold * maxThreshold; + let closestDistanceSq = Infinity; + let closestDistanceTriIndex = null; + BufferStack.setBuffer( bvh._roots[ root ] ); + + const { geometry } = bvh; + const { index } = geometry; + const pos = geometry.attributes.position; + const triangle = ExtendedTrianglePool.getPrimitive(); + + const { float32Array, uint16Array, uint32Array } = BufferStack; + + sortedList.clear(); + + if ( maxDepthSorted > 0 ) { + + _fillSortedList( root, 0 ); + + } else { + + sortedList.push( { nodeIndex32: root, distance: closestDistanceSquaredPointToBox( root, float32Array, point ) } ); + + } + + const nodes = sortedList.array; + for ( let i = nodes.length - 1; i >= 0; i -- ) { + + const { distance, nodeIndex32 } = nodes[ i ]; + + if ( distance >= closestDistanceSq ) break; + + _closestPointToPoint( nodeIndex32 ); + + } + + BufferStack.clearBuffer(); + + if ( closestDistanceSq === Infinity ) return null; + + const closestDistance = Math.sqrt( closestDistanceSq ); + + if ( ! target.point ) target.point = temp1.clone(); + else target.point.copy( temp1 ); + target.distance = closestDistance; + target.faceIndex = closestDistanceTriIndex; + + return target; + + + function _fillSortedList( nodeIndex32, depth ) { + + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + sortedList.push( { nodeIndex32, distance: closestDistanceSquaredPointToBox( nodeIndex32, float32Array, point ) } ); + + return; + + } + + const leftIndex = LEFT_NODE( nodeIndex32 ); + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + + if ( depth === maxDepthSorted ) { + + const leftDistance = closestDistanceSquaredPointToBox( leftIndex, float32Array, point ); + const rightDistance = closestDistanceSquaredPointToBox( rightIndex, float32Array, point ); + + if ( leftDistance > rightDistance ) { // leftDistance < maxThresholdSq - consider this? + + sortedList.push( { nodeIndex32: leftIndex, distance: leftDistance } ); + sortedList.push( { nodeIndex32: rightIndex, distance: rightDistance } ); + + } else { + + sortedList.push( { nodeIndex32: rightIndex, distance: rightDistance } ); + sortedList.push( { nodeIndex32: leftIndex, distance: leftDistance } ); + + } + + return; + + } + + _fillSortedList( leftIndex, depth + 1 ); + _fillSortedList( rightIndex, depth + 1 ); + + } + + + function _closestPointToPoint( nodeIndex32 ) { + + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + /* @if INDIRECT */ + + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, index, pos ); + + /* @else */ + + setTriangle( triangle, i * 3, index, pos ); + + /* @endif */ + + triangle.needsUpdate = true; + + triangle.closestPointToPoint( point, temp ); + const distSq = point.distanceToSquared( temp ); + if ( distSq < closestDistanceSq ) { + + temp1.copy( temp ); + closestDistanceSq = distSq; + closestDistanceTriIndex = i; + + if ( distSq < minThresholdSq ) return true; + + } + + } + + return; + + } + + const leftIndex = LEFT_NODE( nodeIndex32 ); + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + + const leftDistance = closestDistanceSquaredPointToBox( leftIndex, float32Array, point ); + const rightDistance = closestDistanceSquaredPointToBox( rightIndex, float32Array, point ); + + if ( leftDistance <= rightDistance ) { + + if ( leftDistance < closestDistanceSq && leftDistance < maxThresholdSq ) { + + if ( _closestPointToPoint( leftIndex ) ) return true; + if ( rightDistance < closestDistanceSq ) return _closestPointToPoint( rightIndex ); + + } + + } else if ( rightDistance < closestDistanceSq && rightDistance < maxThresholdSq ) { + + if ( _closestPointToPoint( rightIndex ) ) return true; + if ( leftDistance < closestDistanceSq ) return _closestPointToPoint( leftIndex ); + + } + + } + +} diff --git a/src/core/cast/closestPointToPointSort.template.js b/src/core/cast/closestPointToPointSort.template.js new file mode 100644 index 000000000..69ca73bca --- /dev/null +++ b/src/core/cast/closestPointToPointSort.template.js @@ -0,0 +1,123 @@ +import { Vector3 } from 'three'; +import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF } from '../utils/nodeBufferUtils.js'; +import { BufferStack } from '../utils/BufferStack.js'; +import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js'; +import { setTriangle } from '../../utils/TriangleUtilities.js'; +import { closestDistanceSquaredPointToBox } from '../utils/distanceUtils.js'; +import { SortedListDesc } from '../utils/SortedListDesc.js'; + +const temp = /* @__PURE__ */ new Vector3(); +const temp1 = /* @__PURE__ */ new Vector3(); +const sortedList = new SortedListDesc(); + +export function closestPointToPointSort/* @echo INDIRECT_STRING */( + bvh, + root, + point, + target, + minThreshold, + maxThreshold +) { + + const minThresholdSq = minThreshold * minThreshold; + const maxThresholdSq = maxThreshold * maxThreshold; + let closestDistanceSq = Infinity; + let closestDistanceTriIndex = null; + BufferStack.setBuffer( bvh._roots[ root ] ); + + _closestPointToPoint(); + + BufferStack.clearBuffer(); + + if ( closestDistanceSq === Infinity ) return null; + + const closestDistance = Math.sqrt( closestDistanceSq ); + + if ( ! target.point ) target.point = temp1.clone(); + else target.point.copy( temp1 ); + target.distance = closestDistance; + target.faceIndex = closestDistanceTriIndex; + + return target; + + + function _closestPointToPoint() { + + const { geometry } = bvh; + const { index } = geometry; + const pos = geometry.attributes.position; + const triangle = ExtendedTrianglePool.getPrimitive(); + const { float32Array, uint16Array, uint32Array } = BufferStack; + sortedList.clear(); + + let node = { nodeIndex32: 0, distance: closestDistanceSquaredPointToBox( 0, float32Array, point ) }; + + do { + + const { distance, nodeIndex32 } = node; + + if ( distance >= closestDistanceSq ) return; + + const nodeIndex16 = nodeIndex32 * 2; + const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); + if ( isLeaf ) { + + const offset = OFFSET( nodeIndex32, uint32Array ); + const count = COUNT( nodeIndex16, uint16Array ); + + for ( let i = offset, l = count + offset; i < l; i ++ ) { + + /* @if INDIRECT */ + + const ti = bvh.resolveTriangleIndex( i ); + setTriangle( triangle, 3 * ti, index, pos ); + + /* @else */ + + setTriangle( triangle, i * 3, index, pos ); + + /* @endif */ + + triangle.needsUpdate = true; + + triangle.closestPointToPoint( point, temp ); + const distSq = point.distanceToSquared( temp ); + if ( distSq < closestDistanceSq ) { + + temp1.copy( temp ); + closestDistanceSq = distSq; + closestDistanceTriIndex = i; + + if ( distSq < minThresholdSq ) return; + + } + + } + + continue; + + } + + const leftIndex = LEFT_NODE( nodeIndex32 ); + const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); + + const leftDistance = closestDistanceSquaredPointToBox( leftIndex, float32Array, point ); + const rightDistance = closestDistanceSquaredPointToBox( rightIndex, float32Array, point ); + + if ( leftDistance < closestDistanceSq && leftDistance < maxThresholdSq ) { + + sortedList.push( { nodeIndex32: leftIndex, distance: leftDistance } ); + + } + + if ( rightDistance < closestDistanceSq && rightDistance < maxThresholdSq ) { + + sortedList.push( { nodeIndex32: rightIndex, distance: rightDistance } ); + + } + + } while ( node = sortedList.pop() ); + + } + +} diff --git a/src/core/utils/SortedListDesc.js b/src/core/utils/SortedListDesc.js new file mode 100644 index 000000000..5ce845a89 --- /dev/null +++ b/src/core/utils/SortedListDesc.js @@ -0,0 +1,47 @@ +export class SortedListDesc { + + constructor() { + + this.array = []; + + } + + clear() { + + this.array.length = 0; + + } + + + push( node ) { + + const index = this.binarySearch( node.distance ); + this.array.splice( index, 0, node ); + + } + + pop() { + + return this.array.pop(); + + } + + binarySearch( value ) { + + const array = this.array; + + let low = 0, high = array.length; + + while ( low < high ) { + + const mid = ( low + high ) >>> 1; + if ( array[ mid ].distance > value ) low = mid + 1; + else high = mid; + + } + + return low; + + } + +} diff --git a/src/core/utils/distanceUtils.js b/src/core/utils/distanceUtils.js new file mode 100644 index 000000000..7b9e8ef7d --- /dev/null +++ b/src/core/utils/distanceUtils.js @@ -0,0 +1,20 @@ +export function closestDistanceSquaredPointToBox( nodeIndex32, array, point ) { + + const xMin = array[ nodeIndex32 + 0 ] - point.x; + const xMax = point.x - array[ nodeIndex32 + 3 ]; + let dx = xMin > xMax ? xMin : xMax; + dx = dx > 0 ? dx : 0; + + const yMin = array[ nodeIndex32 + 1 ] - point.y; + const yMax = point.y - array[ nodeIndex32 + 4 ]; + let dy = yMin > yMax ? yMin : yMax; + dy = dy > 0 ? dy : 0; + + const zMin = array[ nodeIndex32 + 2 ] - point.z; + const zMax = point.z - array[ nodeIndex32 + 5 ]; + let dz = zMin > zMax ? zMin : zMax; + dz = dz > 0 ? dz : 0; + + return dx * dx + dy * dy + dz * dz; + +}