Repository: gkjohnson/three-mesh-bvh
Pull request #578 (2023-08-26 10:48:21)

Rearrange tests, add indirect tests 


Patch:
diff --git a/src/core/MeshBVH.js b/src/core/MeshBVH.js
index bcf3390..494fce0 100644
--- a/src/core/MeshBVH.js
+++ b/src/core/MeshBVH.js
@@ -37,7 +37,7 @@ export class MeshBVH {
 
 		const geometry = bvh.geometry;
 		const rootData = bvh._roots;
-		const indirectBuffer = bvh.indirectBuffer;
+		const indirectBuffer = bvh._indirectBuffer;
 		const indexAttribute = geometry.getIndex();
 		let result;
 		if ( options.cloneBuffers ) {
diff --git a/src/core/build/buildTree.js b/src/core/build/buildTree.js
index 0c3dac1..073c405 100644
--- a/src/core/build/buildTree.js
+++ b/src/core/build/buildTree.js
@@ -31,7 +31,7 @@ function buildTree( bvh, options ) {
 	// 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 geometry bounding box if it's available because it may be out of date.
 	const geometry = bvh.geometry;
-	const indexArray = geometry.index.array;
+	const indexArray = geometry.index ? geometry.index.array : null;
 	const maxDepth = options.maxDepth;
 	const verbose = options.verbose;
 	const maxLeafTris = options.maxLeafTris;
diff --git a/src/core/build/geometryUtils.js b/src/core/build/geometryUtils.js
index 63a5f3a..51df429 100644
--- a/src/core/build/geometryUtils.js
+++ b/src/core/build/geometryUtils.js
@@ -1,29 +1,39 @@
 import { BufferAttribute } from 'three';
 
+export function getVertexCount( geo ) {
+
+	return geo.index ? geo.index.count : geo.attributes.position.count;
+
+}
+
 export function getTriCount( geo ) {
 
-	return ( geo.index ? geo.index.count : geo.attributes.position.count ) / 3;
+	return getVertexCount( geo ) / 3;
 
 }
 
-// ensures that an index is present on the geometry
-export function ensureIndex( geo, options ) {
+export function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) {
 
-	if ( ! geo.index ) {
+	if ( vertexCount > 65535 ) {
 
-		const vertexCount = geo.attributes.position.count;
-		const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
-		let index;
-		if ( vertexCount > 65535 ) {
+		return new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
 
-			index = new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
+	} else {
 
-		} else {
+		return new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
 
-			index = new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
+	}
 
-		}
+}
+
+// ensures that an index is present on the geometry
+export function ensureIndex( geo, options ) {
 
+	if ( ! geo.index ) {
+
+		const vertexCount = geo.attributes.position.count;
+		const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
+		const index = getIndexArray( vertexCount, BufferConstructor );
 		geo.setIndex( new BufferAttribute( index, 1 ) );
 
 		for ( let i = 0; i < vertexCount; i ++ ) {
diff --git a/src/gpu/MeshBVHUniformStruct.js b/src/gpu/MeshBVHUniformStruct.js
index efbe6ea..fe82653 100644
--- a/src/gpu/MeshBVHUniformStruct.js
+++ b/src/gpu/MeshBVHUniformStruct.js
@@ -20,6 +20,92 @@ import {
 	OFFSET,
 	SPLIT_AXIS,
 } from '../core/utils/nodeBufferUtils.js';
+import { getIndexArray, getVertexCount } from '../core/build/geometryUtils.js';
+
+export class MeshBVHUniformStruct {
+
+	constructor() {
+
+		this.index = new UIntVertexAttributeTexture();
+		this.position = new FloatVertexAttributeTexture();
+		this.bvhBounds = new DataTexture();
+		this.bvhContents = new DataTexture();
+		this._cachedIndexAttr = null;
+
+		this.index.overrideItemSize = 3;
+
+	}
+
+	updateFrom( bvh ) {
+
+		const { geometry } = bvh;
+		bvhToTextures( bvh, this.bvhBounds, this.bvhContents );
+
+		this.position.updateFrom( geometry.attributes.position );
+
+		// dereference a new index attribute if we're using indirect storage
+		if ( bvh.indirect ) {
+
+			const indirectBuffer = bvh._indirectBuffer;
+			if (
+				this._cachedIndexAttr === null ||
+				this._cachedIndexAttr.count !== indirectBuffer.length
+			) {
+
+				if ( geometry.index ) {
+
+					this._cachedIndexAttr = geometry.index.clone();
+
+				} else {
+
+					const array = getIndexArray( getVertexCount( geometry ) );
+					this._cachedIndexAttr = new BufferAttribute( array, 1, false );
+
+				}
+
+			}
+
+			dereferenceIndex( geometry, indirectBuffer, this._cachedIndexAttr );
+			this.index.updateFrom( this._cachedIndexAttr );
+
+		} else {
+
+			this.index.updateFrom( geometry.index );
+
+		}
+
+	}
+
+	dispose() {
+
+		const { index, position, bvhBounds, bvhContents } = this;
+
+		if ( index ) index.dispose();
+		if ( position ) position.dispose();
+		if ( bvhBounds ) bvhBounds.dispose();
+		if ( bvhContents ) bvhContents.dispose();
+
+	}
+
+}
+
+function dereferenceIndex( geometry, indirectBuffer, target ) {
+
+	const unpacked = target.array;
+	const indexArray = geometry.index ? geometry.index.array : null;
+	for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
+
+		const i3 = 3 * i;
+		const v3 = 3 * indirectBuffer[ i ];
+		for ( let c = 0; c < 3; c ++ ) {
+
+			unpacked[ i3 + c ] = indexArray ? indexArray[ v3 + c ] : v3 + c;
+
+		}
+
+	}
+
+}
 
 function bvhToTextures( bvh, boundsTexture, contentsTexture ) {
 
@@ -103,77 +189,3 @@ function bvhToTextures( bvh, boundsTexture, contentsTexture ) {
 	contentsTexture.dispose();
 
 }
-
-export class MeshBVHUniformStruct {
-
-	constructor() {
-
-		this.autoDispose = true;
-		this.index = new UIntVertexAttributeTexture();
-		this.position = new FloatVertexAttributeTexture();
-		this.bvhBounds = new DataTexture();
-		this.bvhContents = new DataTexture();
-		this._cachedIndexAttr = null;
-
-		this.index.overrideItemSize = 3;
-
-	}
-
-	updateFrom( bvh ) {
-
-		const { geometry } = bvh;
-		bvhToTextures( bvh, this.bvhBounds, this.bvhContents );
-
-		this.position.updateFrom( geometry.attributes.position );
-
-		// dereference a new index attribute if we're using indirect storage
-		const indirectBuffer = bvh._indirectBuffer;
-		if ( indirectBuffer ) {
-
-			if (
-				this._cachedIndexAttr === null ||
-				this._cachedIndexAttr.count !== indirectBuffer.length
-			) {
-
-				this._cachedIndexAttr = geometry.index ?
-					geometry.index.clone() :
-					new BufferAttribute( indirectBuffer.slice(), 1, false );
-
-			}
-
-			dereferenceIndex( geometry, indirectBuffer, this._cachedIndexAttr );
-			this.index.updateFrom( this._cachedIndexAttr );
-
-		} else {
-
-			this.index.updateFrom( geometry.index );
-
-		}
-
-	}
-
-	dispose() {
-
-		const { index, position, bvhBounds, bvhContents } = this;
-
-		if ( index ) index.dispose();
-		if ( position ) position.dispose();
-		if ( bvhBounds ) bvhBounds.dispose();
-		if ( bvhContents ) bvhContents.dispose();
-
-	}
-
-}
-
-function dereferenceIndex( geometry, indirectBuffer, target ) {
-
-	const index = geometry.index;
-	for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
-
-		const iv = indirectBuffer[ i ];
-		let nv = index ? index.getX( iv ) : iv;
-		target.setX( i, nv );
-
-	}
-
-}


Test patch:
diff --git a/test/Math.OBB.test.js b/test/Math.OBB.test.js
new file mode 100644
index 0000000..713efea
--- /dev/null
+++ b/test/Math.OBB.test.js
@@ -0,0 +1,162 @@
+import { Vector3, Triangle, Plane } from 'three';
+import { OrientedBox } from '../src/math/OrientedBox.js';
+import { setRandomVector, getRandomOrientation } from './utils.js';
+
+describe( 'OBB Intersections', () => {
+
+	let box, center;
+	beforeEach( () => {
+
+		box = new OrientedBox();
+		box.min.set( - 1, - 1, - 1 );
+		box.max.set( 1, 1, 1 );
+		getRandomOrientation( box.matrix, 10 );
+		box.needsUpdate = true;
+
+		center = new Vector3();
+		center.setFromMatrixPosition( box.matrix );
+
+	} );
+
+	it( 'should intersect triangles with a vertex inside', () => {
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], 3 + 0.0001 + Math.random() )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
+				.add( center );
+
+			expect( box.intersectsTriangle( triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should intersect triangles with two vertices inside', () => {
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
+				.add( center );
+
+			expect( box.intersectsTriangle( triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should intersect triangles with all vertices inside', () => {
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
+				.add( center );
+
+			expect( box.intersectsTriangle( triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+
+	it( 'should intersect triangles that cut across', () => {
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], 3 + 0.0001 + Math.random() );
+
+			triangle[ fields[ i1 ] ]
+				.copy( triangle[ fields[ i0 ] ] )
+				.multiplyScalar( - 1 )
+				.add( center );
+
+			triangle[ fields[ i0 ] ]
+				.add( center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
+				.add( center );
+
+			expect( box.intersectsTriangle( triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should not intersect triangles outside sphere', () => {
+
+		const plane = new Plane();
+		const vec = new Vector3();
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			// project the triangle out onto a plane
+			setRandomVector( plane.normal, 1 );
+			plane.setFromNormalAndCoplanarPoint( plane.normal, center );
+			plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 5.001;
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( center );
+			plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( center );
+			plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( center );
+			plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
+
+			expect( box.intersectsTriangle( triangle ) ).toBe( false );
+
+		}
+
+	} );
+
+} );
diff --git a/test/Math.SphereIntersections.test.js b/test/Math.SphereIntersections.test.js
new file mode 100644
index 0000000..f9a543d
--- /dev/null
+++ b/test/Math.SphereIntersections.test.js
@@ -0,0 +1,167 @@
+import { Vector3, Triangle, Sphere, Plane } from 'three';
+import { sphereIntersectTriangle } from '../src/math/MathUtilities.js';
+import { setRandomVector } from './utils.js';
+
+describe( 'Sphere Intersections', () => {
+
+	it( 'should intersect triangles with a vertex inside', () => {
+
+		const sphere = new Sphere();
+		sphere.radius = 1;
+		setRandomVector( sphere.center, 10 );
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], 1 + 0.0001 + Math.random() )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
+				.add( sphere.center );
+
+			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should intersect triangles with two vertices inside', () => {
+
+		const sphere = new Sphere();
+		sphere.radius = 1;
+		setRandomVector( sphere.center, 10 );
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
+				.add( sphere.center );
+
+			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should intersect triangles with all vertices inside', () => {
+
+		const sphere = new Sphere();
+		sphere.radius = 1;
+		setRandomVector( sphere.center, 10 );
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
+				.add( sphere.center );
+
+			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should intersect triangles that only intersect the middle', () => {
+
+		const sphere = new Sphere();
+		sphere.radius = 1;
+		setRandomVector( sphere.center, 10 );
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( triangle[ fields[ i0 ] ], 1 + 0.0001 + Math.random() );
+
+			triangle[ fields[ i1 ] ]
+				.copy( triangle[ fields[ i0 ] ] )
+				.multiplyScalar( - 1 )
+				.add( sphere.center );
+
+			triangle[ fields[ i0 ] ]
+				.add( sphere.center );
+
+			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
+				.add( sphere.center );
+
+			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
+
+		}
+
+	} );
+
+	it( 'should not intersect triangles outside sphere', () => {
+
+		const sphere = new Sphere();
+		sphere.radius = 1;
+		setRandomVector( sphere.center, 10 );
+
+		const plane = new Plane();
+		const vec = new Vector3();
+
+		const triangle = new Triangle();
+		for ( let i = 0; i < 100; i ++ ) {
+
+			// project the triangle out onto a plane
+			setRandomVector( plane.normal, 1 );
+			plane.setFromNormalAndCoplanarPoint( plane.normal, sphere.center );
+			plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 1.001;
+
+			const fields = [ 'a', 'b', 'c' ];
+			const i0 = i % 3;
+			const i1 = ( i + 1 ) % 3;
+			const i2 = ( i + 2 ) % 3;
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( sphere.center );
+			plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( sphere.center );
+			plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
+
+			setRandomVector( vec, 10 * Math.random() )
+				.add( sphere.center );
+			plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
+
+			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( false );
+
+		}
+
+	} );
+
+} );
diff --git a/test/IntersectionUtils.test.js b/test/Math.TriangleIntersections.test.js
similarity index 52%
rename from test/IntersectionUtils.test.js
rename to test/Math.TriangleIntersections.test.js
index 707a75f..68c65e1 100644
--- a/test/IntersectionUtils.test.js
+++ b/test/Math.TriangleIntersections.test.js
@@ -1,8 +1,5 @@
-import { Vector3, Triangle, Sphere, Plane, Line3 } from 'three';
-import { sphereIntersectTriangle } from '../src/math/MathUtilities.js';
+import { Triangle, Line3 } from 'three';
 import { ExtendedTriangle } from '../src/math/ExtendedTriangle.js';
-import { OrientedBox } from '../src/math/OrientedBox.js';
-import { setRandomVector, getRandomOrientation } from './utils.js';
 
 describe( 'Triangle Intersections', () => {
 
@@ -371,326 +368,3 @@ describe( 'Triangle Intersection line', () => {
 	} );
 
 } );
-
-describe( 'Sphere Intersections', () => {
-
-	it( 'should intersect triangles with a vertex inside', () => {
-
-		const sphere = new Sphere();
-		sphere.radius = 1;
-		setRandomVector( sphere.center, 10 );
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], 1 + 0.0001 + Math.random() )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
-				.add( sphere.center );
-
-			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should intersect triangles with two vertices inside', () => {
-
-		const sphere = new Sphere();
-		sphere.radius = 1;
-		setRandomVector( sphere.center, 10 );
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
-				.add( sphere.center );
-
-			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should intersect triangles with all vertices inside', () => {
-
-		const sphere = new Sphere();
-		sphere.radius = 1;
-		setRandomVector( sphere.center, 10 );
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
-				.add( sphere.center );
-
-			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should intersect triangles that only intersect the middle', () => {
-
-		const sphere = new Sphere();
-		sphere.radius = 1;
-		setRandomVector( sphere.center, 10 );
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], 1 + 0.0001 + Math.random() );
-
-			triangle[ fields[ i1 ] ]
-				.copy( triangle[ fields[ i0 ] ] )
-				.multiplyScalar( - 1 )
-				.add( sphere.center );
-
-			triangle[ fields[ i0 ] ]
-				.add( sphere.center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
-				.add( sphere.center );
-
-			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should not intersect triangles outside sphere', () => {
-
-		const sphere = new Sphere();
-		sphere.radius = 1;
-		setRandomVector( sphere.center, 10 );
-
-		const plane = new Plane();
-		const vec = new Vector3();
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			// project the triangle out onto a plane
-			setRandomVector( plane.normal, 1 );
-			plane.setFromNormalAndCoplanarPoint( plane.normal, sphere.center );
-			plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 1.001;
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( sphere.center );
-			plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( sphere.center );
-			plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( sphere.center );
-			plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
-
-			expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( false );
-
-		}
-
-	} );
-
-} );
-
-describe( 'Box Intersections', () => {
-
-	let box, center;
-	beforeEach( () => {
-
-		box = new OrientedBox();
-		box.min.set( - 1, - 1, - 1 );
-		box.max.set( 1, 1, 1 );
-		getRandomOrientation( box.matrix, 10 );
-		box.needsUpdate = true;
-
-		center = new Vector3();
-		center.setFromMatrixPosition( box.matrix );
-
-	} );
-
-	it( 'should intersect triangles with a vertex inside', () => {
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], 3 + 0.0001 + Math.random() )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
-				.add( center );
-
-			expect( box.intersectsTriangle( triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should intersect triangles with two vertices inside', () => {
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
-				.add( center );
-
-			expect( box.intersectsTriangle( triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should intersect triangles with all vertices inside', () => {
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
-				.add( center );
-
-			expect( box.intersectsTriangle( triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-
-	it( 'should intersect triangles that cut across', () => {
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( triangle[ fields[ i0 ] ], 3 + 0.0001 + Math.random() );
-
-			triangle[ fields[ i1 ] ]
-				.copy( triangle[ fields[ i0 ] ] )
-				.multiplyScalar( - 1 )
-				.add( center );
-
-			triangle[ fields[ i0 ] ]
-				.add( center );
-
-			setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
-				.add( center );
-
-			expect( box.intersectsTriangle( triangle ) ).toBe( true );
-
-		}
-
-	} );
-
-	it( 'should not intersect triangles outside sphere', () => {
-
-		const plane = new Plane();
-		const vec = new Vector3();
-
-		const triangle = new Triangle();
-		for ( let i = 0; i < 100; i ++ ) {
-
-			// project the triangle out onto a plane
-			setRandomVector( plane.normal, 1 );
-			plane.setFromNormalAndCoplanarPoint( plane.normal, center );
-			plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 5.001;
-
-			const fields = [ 'a', 'b', 'c' ];
-			const i0 = i % 3;
-			const i1 = ( i + 1 ) % 3;
-			const i2 = ( i + 2 ) % 3;
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( center );
-			plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( center );
-			plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
-
-			setRandomVector( vec, 10 * Math.random() )
-				.add( center );
-			plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
-
-			expect( box.intersectsTriangle( triangle ) ).toBe( false );
-
-		}
-
-	} );
-
-} );
diff --git a/test/MeshBVH.options.test.js b/test/MeshBVH.options.test.js
new file mode 100644
index 0000000..6fc989b
--- /dev/null
+++ b/test/MeshBVH.options.test.js
@@ -0,0 +1,226 @@
+import {
+	Mesh,
+	Raycaster,
+	MeshBasicMaterial,
+	TorusGeometry,
+} from 'three';
+import {
+	MeshBVH,
+	getBVHExtremes,
+} from '../src/index.js';
+import { getMaxDepth } from './utils.js';
+
+describe( 'Options', () => {
+
+	let mesh, geometry;
+
+	beforeEach( () => {
+
+		geometry = new TorusGeometry( 5, 5, 400, 100 );
+		mesh = new Mesh( geometry, new MeshBasicMaterial() );
+
+	} );
+
+	describe( 'onProgress', () => {
+
+		it( 'should provide a progress update for every leaf node.', () => {
+
+			let minProgress = Infinity;
+			let maxProgress = - Infinity;
+			let count = 0;
+
+			const bvh = new MeshBVH( mesh.geometry, {
+
+				onProgress( progress ) {
+
+					minProgress = Math.min( minProgress, progress );
+					maxProgress = Math.max( maxProgress, progress );
+					count ++;
+
+				}
+
+			} );
+
+			const leafNodeCount = getBVHExtremes( bvh )[ 0 ].leafNodeCount;
+			expect( maxProgress ).toEqual( 1.0 );
+			expect( minProgress ).toBeLessThan( 0.001 );
+			expect( count ).toBe( leafNodeCount );
+
+		} );
+
+	} );
+
+	describe( 'setBoundingBox', () => {
+
+		it( 'should set the bounding box of the geometry when true.', () => {
+
+			const bvh = new MeshBVH( geometry, { setBoundingBox: true } );
+			expect( geometry.boundingBox ).not.toBe( null );
+			expect( bvh ).toBeTruthy();
+
+		} );
+
+		it( 'should not set the bounding box of the geometry when false.', () => {
+
+			const bvh = new MeshBVH( geometry, { setBoundingBox: false } );
+			expect( mesh.geometry.boundingBox ).toBe( null );
+			expect( bvh ).toBeTruthy();
+
+		} );
+
+	} );
+
+	describe( 'maxDepth', () => {
+
+		it( 'should not be limited by default.', () => {
+
+			const bvh = new MeshBVH( geometry );
+			const depth = getMaxDepth( bvh );
+			expect( depth ).toBeGreaterThan( 10 );
+
+		} );
+
+		it( 'should cap the depth of the bounds tree.', () => {
+
+			const bvh = new MeshBVH( geometry, { maxDepth: 10, verbose: false } );
+			const depth = getMaxDepth( bvh );
+			expect( depth ).toEqual( 10 );
+
+		} );
+
+		it( 'should successfully raycast.', () => {
+
+			const raycaster = new Raycaster();
+			raycaster.ray.origin.set( 0, 0, 10 );
+			raycaster.ray.direction.set( 0, 0, - 1 );
+
+			const bvh = new MeshBVH( mesh.geometry, { maxDepth: 3, verbose: false } );
+			const ogHits = raycaster.intersectObject( mesh, true );
+
+			mesh.geometry.boundsTree = bvh;
+			const bvhHits = raycaster.intersectObject( mesh, true );
+
+			raycaster.raycastFirst = true;
+			const firstHit = raycaster.intersectObject( mesh, true );
+
+			expect( ogHits ).toEqual( bvhHits );
+			expect( firstHit[ 0 ] ).toEqual( ogHits[ 0 ] );
+
+		} );
+
+	} );
+
+	describe( 'useSharedArrayBuffer', () => {
+
+		it( 'should initialize with shared array buffers if true.', () => {
+
+			const geo = geometry.toNonIndexed();
+			const bvh = new MeshBVH( geo, { useSharedArrayBuffer: true } );
+			expect( bvh._roots[ 0 ] instanceof SharedArrayBuffer ).toBe( true );
+			expect( geo.index.array.buffer instanceof SharedArrayBuffer ).toBe( true );
+
+		} );
+
+		it( 'should initialize with regular array buffers if false.', () => {
+
+			const geo = geometry.toNonIndexed();
+			const bvh = new MeshBVH( geo, { useSharedArrayBuffer: false } );
+			expect( bvh._roots[ 0 ] instanceof SharedArrayBuffer ).toBe( false );
+			expect( geo.index.array.buffer instanceof SharedArrayBuffer ).toBe( false );
+
+		} );
+
+		it( 'should initialize the indirect buffer with shared array buffers if true.', () => {
+
+			const geo = geometry.toNonIndexed();
+			const bvh = new MeshBVH( geo, { useSharedArrayBuffer: true, indirect: true } );
+			expect( bvh._indirectBuffer.buffer instanceof SharedArrayBuffer ).toBe( true );
+
+		} );
+
+	} );
+
+	describe( 'indirect', () => {
+
+		it( 'should not create an indirect buffer if false.', () => {
+
+			const bvh = new MeshBVH( geometry, { indirect: false } );
+			expect( bvh._indirectBuffer ).not.toBeTruthy();
+
+		} );
+
+		it( 'should not adjust the index buffer if false.', () => {
+
+			const clone = geometry.clone();
+			const bvh = new MeshBVH( geometry, { indirect: true } );
+			expect( geometry.index.array ).not.toBe( clone.index.array );
+			expect( geometry.index.array ).toEqual( clone.index.array );
+			expect( bvh ).toBeTruthy();
+
+		} );
+
+		it( 'should unpack to the same rearranged index buffer.', () => {
+
+			const geometry = new TorusGeometry( 5, 5, 40, 10 );
+			const bvh = new MeshBVH( geometry.clone(), { indirect: false } );
+			const bvhIndirect = new MeshBVH( geometry.clone(), { indirect: true } );
+
+			const indexArray = bvhIndirect.geometry.index.array;
+			const indirectBuffer = bvhIndirect._indirectBuffer;
+			const unpacked = bvhIndirect.geometry.index.array.slice();
+			indirectBuffer.forEach( ( v, i ) => {
+
+				for ( let c = 0; c < 3; c ++ ) {
+
+					unpacked[ 3 * i + c ] = indexArray[ 3 * v + c ];
+
+				}
+
+			} );
+
+			expect( bvh.geometry.index.array ).not.toEqual( bvhIndirect.geometry.index.array );
+			expect( bvh.geometry.index.array ).toEqual( unpacked );
+
+		} );
+
+		it( 'should create an indirect buffer the same length as the number of triangles.', () => {
+
+			const bvh = new MeshBVH( geometry, { indirect: true } );
+			expect( bvh._indirectBuffer ).toHaveLength( geometry.index.count / 3 );
+
+		} );
+
+		it( 'should not generate an index buffer if it does not exist.', () => {
+
+			const geo = geometry.toNonIndexed();
+			const bvh = new MeshBVH( geo, { indirect: true } );
+			expect( bvh._indirectBuffer ).toBeTruthy();
+			expect( bvh.indirect ).toBe( true );
+			expect( geo.index ).not.toBeTruthy();
+
+		} );
+
+		it( 'should produce an indirect buffer that would contain the index buffer.', () => {
+
+			const bvhIndexed = new MeshBVH( geometry.clone(), { indirect: false } );
+			const bvhNonIndexed = new MeshBVH( geometry.toNonIndexed(), { indirect: false } );
+			const bvhIndexedIndirect = new MeshBVH( geometry.clone(), { indirect: true } );
+			const bvhNonIndexedIndirect = new MeshBVH( geometry.toNonIndexed(), { indirect: true } );
+
+			const triCount = bvhIndexed.geometry.index.count / 3;
+			expect( bvhIndexed.geometry.index.count / 3 ).toEqual( triCount );
+			expect( bvhNonIndexed.geometry.index.count / 3 ).toEqual( triCount );
+			expect( bvhIndexedIndirect._indirectBuffer ).toHaveLength( triCount );
+			expect( bvhNonIndexedIndirect._indirectBuffer ).toHaveLength( triCount );
+
+		} );
+
+	} );
+
+	describe( 'strategy', () => {
+
+		it.todo( 'should set the split strategy' );
+
+	} );
+
+} );
diff --git a/test/MeshBVH.serialize.test.js b/test/MeshBVH.serialize.test.js
new file mode 100644
index 0000000..382e876
--- /dev/null
+++ b/test/MeshBVH.serialize.test.js
@@ -0,0 +1,143 @@
+import {
+	BufferGeometry,
+	SphereGeometry,
+	BufferAttribute,
+} from 'three';
+import {
+	MeshBVH,
+} from '../src/index.js';
+
+describe( 'Serialization', () => {
+
+	let geometry;
+	beforeEach( () => {
+
+		geometry = new SphereGeometry( 1, 10, 10 );
+
+	} );
+
+	it( 'should serialize then deserialize to the same structure.', () => {
+
+		const bvh = new MeshBVH( geometry );
+		const serialized = MeshBVH.serialize( bvh );
+		const deserializedBVH = MeshBVH.deserialize( serialized, geometry );
+
+		// use a custom object since anonymous functions cause the
+		// test function to fail
+		const testObj = { ...bvh };
+		delete testObj.resolveTriangleIndex;
+		expect( deserializedBVH ).toMatchObject( testObj );
+
+	} );
+
+	it( 'should serialize then deserialize to the same structure with indirect = true.', () => {
+
+		const bvh = new MeshBVH( geometry, { indirect: true } );
+		const serialized = MeshBVH.serialize( bvh );
+		const deserializedBVH = MeshBVH.deserialize( serialized, geometry );
+
+		// use a custom object since anonymous functions cause the
+		// test function to fail
+		const testObj = { ...bvh };
+		delete testObj.resolveTriangleIndex;
+		expect( deserializedBVH ).toMatchObject( testObj );
+
+	} );
+
+	it( 'should create a new index if one does not exist when deserializing.', () => {
+
+		const bvh = new MeshBVH( geometry );
+		const serialized = MeshBVH.serialize( bvh );
+
+		geometry.setIndex( null );
+		MeshBVH.deserialize( serialized, geometry );
+
+		expect( geometry.index ).toBeTruthy();
+
+	} );
+
+	it( 'should create an index buffer with Uint16Array if the geometry is small enough.', () => {
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( 60000 * 3 ), 3, false ) );
+
+		const bvh = new MeshBVH( geometry );
+		expect( geometry.index.array instanceof Uint16Array ).toBe( true );
+		expect( bvh ).toBeTruthy();
+
+	} );
+
+	it( 'should create an index buffer with Uint32Array if the geometry is large enough.', () => {
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( 70000 * 3 ), 3, false ) );
+
+		const bvh = new MeshBVH( geometry );
+		expect( geometry.index.array instanceof Uint32Array ).toBe( true );
+		expect( bvh ).toBeTruthy();
+
+	} );
+
+	describe( 'cloneBuffers', () => {
+
+		it( 'should clone the index buffer from the target geometry when true.', () => {
+
+			const bvh = new MeshBVH( geometry );
+			const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true, indirect: true } );
+			expect( geometry.index.array ).not.toBe( serialized.index );
+			expect( bvh._roots ).not.toBe( serialized.roots );
+			expect( bvh._roots[ 0 ] ).not.toBe( serialized.roots[ 0 ] );
+			expect( bvh._roots ).toEqual( serialized.roots );
+			expect( bvh._indirectBuffer ).toBe( serialized.indirectBuffer );
+
+		} );
+
+		it( 'should clone the index buffer from the target geometry when false.', () => {
+
+			const bvh = new MeshBVH( geometry );
+			const serialized = MeshBVH.serialize( bvh, { cloneBuffers: false, indirect: true } );
+			expect( geometry.index.array ).toBe( serialized.index );
+			expect( bvh._roots ).toBe( serialized.roots );
+			expect( bvh._roots[ 0 ] ).toBe( serialized.roots[ 0 ] );
+			expect( bvh._roots ).toEqual( serialized.roots );
+			expect( bvh._indirectBuffer ).toBe( serialized.indirectBuffer );
+
+		} );
+
+	} );
+
+	describe( 'setIndex', () => {
+
+		it( 'should not copy the index buffer onto the target geometry if setIndex is false.', () => {
+
+			const cloned = geometry.clone();
+			const bvh = new MeshBVH( geometry );
+			const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true } );
+
+			expect( cloned.index.array ).not.toBe( serialized.index );
+			expect( cloned.index.array ).not.toEqual( serialized.index );
+
+			MeshBVH.deserialize( serialized, cloned, { setIndex: false } );
+			expect( cloned.index.array ).not.toBe( serialized.index );
+			expect( cloned.index.array ).not.toEqual( serialized.index );
+
+		} );
+
+		it( 'should copy the index buffer onto the target geometry if setIndex is true.', () => {
+
+			const cloned = geometry.clone();
+			const bvh = new MeshBVH( geometry );
+			const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true } );
+
+			expect( cloned.index.array ).not.toBe( serialized.index );
+			expect( cloned.index.array ).not.toEqual( serialized.index );
+
+			MeshBVH.deserialize( serialized, cloned, { setIndex: true } );
+			expect( cloned.index.array ).not.toBe( serialized.index );
+			expect( cloned.index.array ).toEqual( serialized.index );
+
+		} );
+
+	} );
+
+} );
diff --git a/test/MeshBVH.test.js b/test/MeshBVH.test.js
index 26efa86..325969a 100644
--- a/test/MeshBVH.test.js
+++ b/test/MeshBVH.test.js
@@ -17,15 +17,14 @@ import {
 	acceleratedRaycast,
 	computeBoundsTree,
 	disposeBoundsTree,
-	getBVHExtremes,
 	validateBounds,
 } from '../src/index.js';
-import { getMaxDepth } from './utils.js';
 
 Mesh.prototype.raycast = acceleratedRaycast;
 BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
 BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
 
+// TODO: clean this up
 describe( 'Bounds Tree', () => {
 
 	it( 'should provide a bounding box that matches the built in one.', () => {
@@ -51,7 +50,7 @@ describe( 'Bounds Tree', () => {
 
 	} );
 
-	it( 'should be generated when calling BufferGeometry.computeBoundsTree', () => {
+	it( 'should be generated when calling BufferGeometry.computeBoundsTree.', () => {
 
 		const geom = new SphereGeometry( 1, 1, 1 );
 		expect( geom.boundsTree ).not.toBeDefined();
@@ -221,233 +220,6 @@ describe( 'Bounds Tree', () => {
 
 } );
 
-describe( 'Serialization', () => {
-
-	it( 'should serialize then deserialize to the same structure.', () => {
-
-		const geom = new SphereGeometry( 1, 10, 10 );
-		const bvh = new MeshBVH( geom );
-		const serialized = MeshBVH.serialize( bvh );
-
-		const deserializedBVH = MeshBVH.deserialize( serialized, geom );
-
-		// use a custom object since anonymous functions cause the
-		// test function to fail
-		const testObj = { ...bvh };
-		delete testObj.resolveTriangleIndex;
-		expect( deserializedBVH ).toMatchObject( testObj );
-
-	} );
-
-	it( 'should copy the index buffer from the target geometry unless copyIndex is set to false', () => {
-
-		const geom = new SphereGeometry( 1, 10, 10 );
-		const bvh = new MeshBVH( geom );
-
-		const serialized1 = MeshBVH.serialize( bvh );
-		expect( geom.index.array ).not.toBe( serialized1.index );
-		expect( bvh._roots ).not.toBe( serialized1.roots );
-		expect( bvh._roots[ 0 ] ).not.toBe( serialized1.roots[ 0 ] );
-		expect( bvh._roots ).toEqual( serialized1.roots );
-
-		const serialized2 = MeshBVH.serialize( bvh, { cloneBuffers: false } );
-		expect( geom.index.array ).toBe( serialized2.index );
-		expect( bvh._roots ).toBe( serialized2.roots );
-		expect( bvh._roots[ 0 ] ).toBe( serialized2.roots[ 0 ] );
-		expect( bvh._roots ).toEqual( serialized2.roots );
-
-	} );
-
-	it( 'should copy the index buffer onto the target geometry unless setIndex is set to false.', () => {
-
-		const geom1 = new SphereGeometry( 1, 10, 10 );
-		const geom2 = new SphereGeometry( 1, 10, 10 );
-		const bvh = new MeshBVH( geom1 );
-		const serialized = MeshBVH.serialize( bvh );
-
-		expect( geom2.index.array ).not.toBe( serialized.index );
-		expect( geom2.index.array ).not.toEqual( serialized.index );
-		MeshBVH.deserialize( serialized, geom2, { setIndex: false } );
-
-		expect( geom2.index.array ).not.toBe( serialized.index );
-		expect( geom2.index.array ).not.toEqual( serialized.index );
-		MeshBVH.deserialize( serialized, geom2, { setIndex: true } );
-
-		expect( geom2.index.array ).not.toBe( serialized.index );
-		expect( geom2.index.array ).toEqual( serialized.index );
-
-	} );
-
-	it( 'should create a new index if one does not exist when deserializing.', () => {
-
-		const geom = new SphereGeometry( 1, 10, 10 );
-		const bvh = new MeshBVH( geom );
-		const serialized = MeshBVH.serialize( bvh );
-
-		geom.index = null;
-
-		MeshBVH.deserialize( serialized, geom );
-
-		expect( geom.index ).toBeTruthy();
-
-	} );
-
-	it( 'should create an index buffer of the appropriate bit width.', () => {
-
-		const geom1 = new BufferGeometry();
-		geom1.setAttribute( 'position', new BufferAttribute( new Float32Array( 70000 * 3 ), 3, false ) );
-
-		new MeshBVH( geom1 );
-		expect( geom1.index.array instanceof Uint32Array ).toBe( true );
-
-		const geom2 = new BufferGeometry();
-		geom2.setAttribute( 'position', new BufferAttribute( new Float32Array( 60000 * 3 ), 3, false ) );
-
-		new MeshBVH( geom2 );
-		expect( geom2.index.array instanceof Uint32Array ).toBe( false );
-
-	} );
-
-} );
-
-describe( 'Options', () => {
-
-	let mesh = null;
-	beforeAll( () => {
-
-		const geometry = new TorusGeometry( 5, 5, 400, 100 );
-		mesh = new Mesh( geometry, new MeshBasicMaterial() );
-
-	} );
-
-	describe( 'onProgress', () => {
-
-		it( 'should provide a progress update for every leaf node', () => {
-
-			let minProgress = Infinity;
-			let maxProgress = - Infinity;
-			let count = 0;
-
-			const bvh = new MeshBVH( mesh.geometry, {
-
-				onProgress( progress ) {
-
-					minProgress = Math.min( minProgress, progress );
-					maxProgress = Math.max( maxProgress, progress );
-					count ++;
-
-				}
-
-			} );
-
-			const leafNodeCount = getBVHExtremes( bvh )[ 0 ].leafNodeCount;
-			expect( maxProgress ).toEqual( 1.0 );
-			expect( minProgress ).toBeLessThan( 0.001 );
-			expect( count ).toBe( leafNodeCount );
-
-		} );
-
-	} );
-
-	describe( 'setBoundingBox', () => {
-
-		it( 'should set the bounding box of the geometry when true.', () => {
-
-			mesh.geometry.boundingBox = null;
-			mesh.geometry.computeBoundsTree( { setBoundingBox: true } );
-
-			expect( mesh.geometry.boundingBox ).not.toBe( null );
-
-		} );
-
-		it( 'should not set the bounding box of the geometry when false.', () => {
-
-			mesh.geometry.boundingBox = null;
-			mesh.geometry.computeBoundsTree( { setBoundingBox: false } );
-
-			expect( mesh.geometry.boundingBox ).toBe( null );
-
-		} );
-
-	} );
-
-	describe( 'maxDepth', () => {
-
-		it( 'should not be limited by default', () => {
-
-			mesh.geometry.computeBoundsTree();
-
-			const depth = getMaxDepth( mesh.geometry.boundsTree );
-			expect( depth ).toBeGreaterThan( 10 );
-
-		} );
-
-		it( 'should cap the depth of the bounds tree', () => {
-
-			mesh.geometry.computeBoundsTree( { maxDepth: 10, verbose: false } );
-
-			const depth = getMaxDepth( mesh.geometry.boundsTree );
-			expect( depth ).toEqual( 10 );
-
-		} );
-
-		it( 'successfully raycast', () => {
-
-			const raycaster = new Raycaster();
-			raycaster.ray.origin.set( 0, 0, 10 );
-			raycaster.ray.direction.set( 0, 0, - 1 );
-
-			const bvh = new MeshBVH( mesh.geometry, { maxDepth: 3, verbose: false } );
-			const ogHits = raycaster.intersectObject( mesh, true );
-
-			mesh.geometry.boundsTree = bvh;
-			const bvhHits = raycaster.intersectObject( mesh, true );
-
-			raycaster.raycastFirst = true;
-			const firstHit = raycaster.intersectObject( mesh, true );
-
-			expect( ogHits ).toEqual( bvhHits );
-			expect( firstHit[ 0 ] ).toEqual( ogHits[ 0 ] );
-
-		} );
-
-	} );
-
-	describe( 'useSharedArrayBuffer', () => {
-
-		it( 'should initialize with shared array buffers if true.', () => {
-
-			const geometry = new TorusGeometry( 5, 5, 40, 10 );
-			let bvh1, bvh2;
-
-			geometry.setIndex( null );
-			bvh1 = new MeshBVH( geometry, { useSharedArrayBuffer: true } );
-			expect( bvh1._roots[ 0 ] instanceof SharedArrayBuffer ).toBe( true );
-			expect( geometry.index.array.buffer instanceof SharedArrayBuffer ).toBe( true );
-
-			geometry.setIndex( null );
-			bvh2 = new MeshBVH( geometry, { useSharedArrayBuffer: false } );
-			expect( bvh2._roots[ 0 ] instanceof SharedArrayBuffer ).toBe( false );
-			expect( geometry.index.array.buffer instanceof SharedArrayBuffer ).toBe( false );
-
-		} );
-
-	} );
-
-	describe( 'strategy', () => {
-
-		it.todo( 'should set the split strategy' );
-
-	} );
-
-	afterEach( () => {
-
-		mesh.geometry.boundsTree = null;
-
-	} );
-
-} );
-
 describe( 'BoundsTree API', () => {
 
 	it.todo( 'test bounds tree and node apis directly' );
diff --git a/test/MeshBVHUniformStruct.test.js b/test/MeshBVHUniformStruct.test.js
new file mode 100644
index 0000000..e0fbf29
--- /dev/null
+++ b/test/MeshBVHUniformStruct.test.js
@@ -0,0 +1,80 @@
+import { SphereGeometry } from 'three';
+import { MeshBVH, MeshBVHUniformStruct, getBVHExtremes } from '../src';
+
+describe( 'MeshBVHUniformStruct', () => {
+
+	let struct, geometry;
+	beforeEach( () => {
+
+		struct = new MeshBVHUniformStruct();
+		geometry = new SphereGeometry( 1, 30, 30 );
+
+	} );
+
+	it( 'should create textures allocated for each element.', () => {
+
+		const bvh = new MeshBVH( geometry );
+		struct.updateFrom( bvh );
+
+		const posAttr = geometry.attributes.position;
+		const indexAttr = geometry.index;
+		const bvhData = getBVHExtremes( bvh )[ 0 ];
+
+		expect( posAttr.count ).toBeLessThanOrEqual( struct.position.image.width * struct.position.image.height );
+		expect( indexAttr.count / 3 ).toBeLessThan( struct.index.image.width * struct.index.image.height );
+		expect( bvhData.nodeCount ).toBeLessThan( struct.bvhBounds.image.width * struct.bvhBounds.image.height / 2 );
+		expect( bvhData.nodeCount ).toBeLessThan( struct.bvhContents.image.width * struct.bvhContents.image.height );
+
+	} );
+
+	it( 'should create textures allocated for each element with indirect enabled.', () => {
+
+		const bvh = new MeshBVH( geometry, { indirect: true } );
+		struct.updateFrom( bvh );
+
+		const posAttr = geometry.attributes.position;
+		const indexAttr = geometry.index;
+		const bvhData = getBVHExtremes( bvh )[ 0 ];
+
+		expect( posAttr.count ).toBeLessThanOrEqual( struct.position.image.width * struct.position.image.height );
+		expect( indexAttr.count / 3 ).toBeLessThan( struct.index.image.width * struct.index.image.height );
+		expect( bvhData.nodeCount ).toBeLessThan( struct.bvhBounds.image.width * struct.bvhBounds.image.height / 2 );
+		expect( bvhData.nodeCount ).toBeLessThan( struct.bvhContents.image.width * struct.bvhContents.image.height );
+
+	} );
+
+	it( 'should produce the same textures with indirect enabled.', () => {
+
+		const bvh = new MeshBVH( geometry.clone(), { indirect: false } );
+		const struct = new MeshBVHUniformStruct();
+		struct.updateFrom( bvh );
+
+		const bvhIndirect = new MeshBVH( geometry.clone(), { indirect: true } );
+		const structIndirect = new MeshBVHUniformStruct();
+		structIndirect.updateFrom( bvhIndirect );
+
+		expect( struct.position.image ).toEqual( structIndirect.position.image );
+		expect( struct.index.image ).toEqual( structIndirect.index.image );
+		expect( struct.bvhBounds.image ).toEqual( structIndirect.bvhBounds.image );
+		expect( struct.bvhContents.image ).toEqual( structIndirect.bvhContents.image );
+
+	} );
+
+	it( 'should produce the same textures even with indirect enabled and no index.', () => {
+
+		const bvh = new MeshBVH( geometry.toNonIndexed(), { indirect: false } );
+		const struct = new MeshBVHUniformStruct();
+		struct.updateFrom( bvh );
+
+		const bvhIndirect = new MeshBVH( geometry.toNonIndexed(), { indirect: true } );
+		const structIndirect = new MeshBVHUniformStruct();
+		structIndirect.updateFrom( bvhIndirect );
+
+		expect( struct.position.image ).toEqual( structIndirect.position.image );
+		expect( struct.index.image ).toEqual( structIndirect.index.image );
+		expect( struct.bvhBounds.image ).toEqual( structIndirect.bvhBounds.image );
+		expect( struct.bvhContents.image ).toEqual( structIndirect.bvhContents.image );
+
+	} );
+
+} );
diff --git a/test/RandomRaycasts.test.js b/test/RandomRaycasts.test.js
index 27cbe62..fb23b92 100644
--- a/test/RandomRaycasts.test.js
+++ b/test/RandomRaycasts.test.js
@@ -34,39 +34,18 @@ describe( 'Random SAH intersections', () => runRandomTests( { strategy: SAH } )
 describe( 'Random Interleaved SAH intersections', () => runRandomTests( { strategy: SAH, interleaved: true } ) );
 describe( 'Random Indirect Buffer SAH intersections', () => runRandomTests( { strategy: SAH, indirect: true } ) );
 
-function createInterleavedPositionBuffer( bufferAttribute ) {
-
-	const array = bufferAttribute.array;
-	const newArray = new array.constructor( array.length * 2 );
-
-	const newBuffer = new InterleavedBufferAttribute( new InterleavedBuffer( newArray, 6 ), 3, 3, bufferAttribute.normalized );
-	for ( let i = 0; i < bufferAttribute.count; i ++ ) {
-
-		const x = bufferAttribute.getX( i );
-		const y = bufferAttribute.getY( i );
-		const z = bufferAttribute.getZ( i );
-
-		newBuffer.setXYZ( i, x, y, z );
-
-	}
-
-	return newBuffer;
-
-}
-
 function runRandomTests( options ) {
 
-	let scene = null;
-	let raycaster = null;
-	let ungroupedGeometry = null;
-	let ungroupedBvh = null;
-	let groupedGeometry = null;
-	let groupedBvh = null;
-
 	const transformSeed = Math.floor( Math.random() * 1e10 );
-
 	describe( `Transform Seed : ${ transformSeed }`, () => {
 
+		let scene,
+			raycaster,
+			ungroupedGeometry,
+			ungroupedBvh,
+			groupedGeometry,
+			groupedBvh;
+
 		beforeAll( () => {
 
 			ungroupedGeometry = new TorusGeometry( 1, 1, 40, 10 );
@@ -154,3 +133,23 @@ function runRandomTests( options ) {
 	} );
 
 }
+
+function createInterleavedPositionBuffer( bufferAttribute ) {
+
+	const array = bufferAttribute.array;
+	const newArray = new array.constructor( array.length * 2 );
+	const newBuffer = new InterleavedBufferAttribute( new InterleavedBuffer( newArray, 6 ), 3, 3, bufferAttribute.normalized );
+	for ( let i = 0; i < bufferAttribute.count; i ++ ) {
+
+		newBuffer.setXYZ(
+			i,
+			bufferAttribute.getX( i ),
+			bufferAttribute.getY( i ),
+			bufferAttribute.getZ( i ),
+		);
+
+	}
+
+	return newBuffer;
+
+}
diff --git a/test/ShapeCasts.test.js b/test/ShapeCasts.test.js
index 076665a..f994b40 100644
--- a/test/ShapeCasts.test.js
+++ b/test/ShapeCasts.test.js
@@ -34,6 +34,7 @@ BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
 runSuiteWithOptions( {} );
 runSuiteWithOptions( { indirect: true } );
 
+// TODO: clean this up
 function runSuiteWithOptions( defaultOptions ) {
 
 	const MeshBVH = class extends _MeshBVH {
diff --git a/test/AverageSplit.test.js b/test/repro/FloatingPointBoundsError.test.js
similarity index 88%
rename from test/AverageSplit.test.js
rename to test/repro/FloatingPointBoundsError.test.js
index d77bee1..ccbcbab 100644
--- a/test/AverageSplit.test.js
+++ b/test/repro/FloatingPointBoundsError.test.js
@@ -13,25 +13,24 @@ import {
 	CENTER,
 	SAH,
 	AVERAGE,
-} from '../src/index.js';
+} from '../../src/index.js';
 import fs from 'fs';
 import path from 'path';
 
-const dataPath = path.resolve( __dirname, './data/points.bin' );
-const buffer = fs.readFileSync( dataPath );
-const points = new Float32Array( buffer.buffer, buffer.byteOffset, buffer.byteLength / 4 );
-
 Mesh.prototype.raycast = acceleratedRaycast;
 BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
 BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
 
+// TODO: clean up test
 describe( 'AVERAGE Points Raycast', () => {
 
-	let geometry = null;
-	let mesh = null;
-	let raycaster = null;
+	let geometry, mesh, raycaster;
 	beforeEach( () => {
 
+		const dataPath = path.resolve( __dirname, '../data/points.bin' );
+		const buffer = fs.readFileSync( dataPath );
+		const points = new Float32Array( buffer.buffer, buffer.byteOffset, buffer.byteLength / 4 );
+
 		geometry = new BufferGeometry();
 		geometry.setAttribute( 'position', new BufferAttribute( points.slice(), 3 ) );
 		geometry.computeVertexNormals();
diff --git a/test/RandomRaycastsRepro.test.js b/test/repro/RaycastsRepro.test.js
similarity index 97%
rename from test/RandomRaycastsRepro.test.js
rename to test/repro/RaycastsRepro.test.js
index a13eee5..ed5fdb8 100644
--- a/test/RandomRaycastsRepro.test.js
+++ b/test/repro/RaycastsRepro.test.js
@@ -1,7 +1,7 @@
 // Test cases specifically for issue #180
 import { Mesh, BufferGeometry, TorusGeometry, Scene, Raycaster, MeshBasicMaterial } from 'three';
-import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, AVERAGE } from '../src/index.js';
-import { random, setSeed } from './utils.js';
+import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, AVERAGE } from '../../src/index.js';
+import { random, setSeed } from '../utils.js';
 
 Mesh.prototype.raycast = acceleratedRaycast;
 BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
diff --git a/test/utils.js b/test/utils.js
index befcfc9..08f97d2 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -56,3 +56,40 @@ export function getRandomOrientation( matrix, range ) {
 	return matrix;
 
 }
+
+export function runOptionsMatrix( options, cb ) {
+
+	traverse( Object.keys( options ) );
+
+	function traverse( remainingKeys, state = {} ) {
+
+		if ( remainingKeys.length === 0 ) {
+
+			cb( { ...state } );
+			return;
+
+		}
+
+		let values;
+		const key = remainingKeys.pop();
+		if ( Array.isArray( options[ key ] ) ) {
+
+			values = options[ key ];
+
+		} else {
+
+			values = [ options[ key ] ];
+
+		}
+
+		for ( let i = 0, l = values.length; i < l; i ++ ) {
+
+			const value = values[ i ];
+			const newState = { ...state, [ key ]: value };
+			traverse( [ ...remainingKeys ], newState );
+
+		}
+
+	}
+
+}