The tree object

geometry.boundsTree as a BvhBoundsTree, three-mesh-bvh's MeshBVH API with three's types, its build options and the helper.

computeBoundsTree makes a BvhBoundsTree: the core's CpuMeshBvh with three's types on top. acceleratedRaycast runs through it, so the core and /three share one walk and one triangle test. It is also exported as MeshBVH, so switching from three-mesh-bvh can be one import change:

import { MeshBVH } from '@pmndrs/bvh/three'; // was: import { MeshBVH } from 'three-mesh-bvh';

const bvh = new MeshBVH(geometry, { maxLeafSize: 3, indirect: true }); // after await prepareBoundsTrees()
import { deserializeBoundsTree, getBVHExtremes, MeshBVHHelper, type BvhBoundsTree } from '@pmndrs/bvh/three';
import { Box3, Sphere } from 'three';

const tree: BvhBoundsTree = mesh.geometry.boundsTree ?? mesh.geometry.computeBoundsTree();

const hits = tree.raycast(new Raycaster().ray, DoubleSide); // three intersections in the geometry's space
const nearby = tree.intersectsSphere(new Sphere(new Vector3(), 0.5));
const closest = tree.closestPointToPoint(new Vector3(0, 3, 0)); // { point: Vector3, distance, faceIndex }
const box = tree.getBoundingBox(new Box3());

// After editing the position attribute (same index):
mesh.geometry.attributes.position.needsUpdate = true;
tree.refit();

// Save and load: our layout's bytes, not three-mesh-bvh's format.
const bytes = tree.serialize();
const loaded = deserializeBoundsTree(bytes, mesh.geometry);

const helper = new MeshBVHHelper(mesh, 10); // a Group of the boxes down to depth 10

Queries

The same methods as three-mesh-bvh's MeshBVH, with three's types (Box3, Sphere, Vector3, Matrix4, Ray):

  • raycast(ray, materialOrSide, near, far), raycastFirst(...): three intersections in the geometry's space, without object.
  • shapecast({ intersectsBounds, intersectsTriangle, intersectsRange, boundsTraverseOrder }): boxes as Box3, triangles as ExtendedTriangle (a three Triangle), original triangle indices.
  • intersectsBox(box, boxToMesh), intersectsSphere(sphere), intersectsGeometry(geometry, geometryToBvh).
  • bvhcast(otherBvh, matrixToLocal, { intersectsRanges, intersectsTriangles }), with tree positions.
  • closestPointToPoint(point, target, minThreshold, maxThreshold), closestPointToGeometry(geometry, geometryToBvh, target1, target2, minThreshold, maxThreshold).
  • refit(), getBoundingBox(box), resolveTriangleIndex(i), traverseNodes(callback) (three-mesh-bvh's traverse), serialize().
  • getTriangleHitPointInfo(point, geometry, triangleIndex, target), estimateMemoryInBytes(tree), getBVHExtremes(tree) and validateBounds(tree) as functions.

ExtendedTriangle, OrientedBox, SeparatingAxisBounds, CONTAINED, INTERSECTED and NOT_INTERSECTED come from /three too, and give three-mesh-bvh's results bit for bit.

import { INTERSECTED, NOT_INTERSECTED, type ExtendedTriangle } from '@pmndrs/bvh/three';

const sphere = new Sphere(new Vector3(0, 0, 1), 0.5);
let count = 0;
tree.shapecast({
    intersectsBounds: (box: Box3) => (sphere.intersectsBox(box) ? INTERSECTED : NOT_INTERSECTED),
    intersectsTriangle: (triangle: ExtendedTriangle) => {
        if (triangle.intersectsSphere(sphere)) count++;
        return false;
    },
});

Queries read the tree's triangles, not the geometry's current positions. After editing positions, call refit() first (three-mesh-bvh reads the geometry, so it sees edits straight away).

Build options

computeBoundsTree, new BvhBoundsTree, new MeshBVH and GenerateMeshBvhWorker.generate take three-mesh-bvh's options:

OptionWhat happens
strategy (CENTER 0, AVERAGE 1, SAH 2)Accepted. Every value builds the same tree: tinybvh's binned SAH, whose trees have a lower SAH cost than a centre or average split. Other values throw.
maxLeafSize, targetLeafSize, maxLeafTrisThe largest leaf (targetLeafSize wins, then maxLeafSize), clamped to 1-8: three-mesh-bvh's default 10 gives 8.
maxDepthAccepted, not honoured: tinybvh decides the depth (boundsTree.depth). A value below it logs one warning.
setBoundingBoxDefault true: sets geometry.boundingBox from the tree when it has none, and refit() keeps it current.
indirectAccepted and ignored: the geometry's index is never reordered.
onProgressCalled with 0 before the build and 1 after it: tinybvh builds in one call.
verbose, useSharedArrayBufferAccepted and ignored.
rangeThrows: the tree holds every triangle, and raycasts follow the draw range.
builder, layout, snapshot (ours)A tinybvh builder to use; a tree already built for this geometry; the geometry's versions when that build started (snapshotGeometry).

buildOptionsFromThreeMeshBvh(options) gives the core options these map to.

Save and load

Two formats, one call to read either:

  • Ours: boundsTree.serialize() gives our layout's bytes (serializeLayout), and deserializeBoundsTree(bytes, geometry) or MeshBVH.deserialize(bytes, geometry) reads them. These are what the GPU uploads, so a baked tree loads with no build. See Serialization and baking. Either triangle format loads: an indexed file (or an indexed layout) stays indexed, 16 bytes per triangle, its corners read from the geometry's own positions (CpuMeshBvh's corners: 'positions'), with the same answers as the records tree. It reads the positions live, so after editing them call refit() before querying the tree directly.
  • three-mesh-bvh's: MeshBVH.serialize(tree) gives its SerializedBVH ({ version, roots, index, indirectBuffer }), which three-mesh-bvh's MeshBVH.deserialize reads, and MeshBVH.deserialize(data, geometry) reads one written by three-mesh-bvh. Use it to swap a library in or out without rebuilding stored trees.
import { MeshBVH } from '@pmndrs/bvh/three';
import type { BufferGeometry } from 'three';

// Ours to three-mesh-bvh: its MeshBVH.deserialize(data, geometry) reads this.
const data = MeshBVH.serialize(new MeshBVH(geometry)); // { version: 1, roots, index, indirectBuffer }

// three-mesh-bvh's to ours: its MeshBVH.serialize(bvh) output, structured-cloned from a worker or stored.
const tree = MeshBVH.deserialize(theirs, geometry); // setIndex: true sets the index three-mesh-bvh wrote
geometry.boundsTree = tree;

// Our own bytes still go through the same call.
const again = MeshBVH.deserialize(tree.serialize(), geometry);
  • Writing always gives indirect mode (our trees never reorder the geometry): one root, indirectBuffer mapping tree positions to triangles, index a copy of the geometry's (cloneBuffers: false hands over the same array). A geometry without an index gives index: null; pass setIndex: false to three-mesh-bvh's deserialize then.
  • Reading keeps the tree's shape (which triangles share a leaf, which nodes are whose children) and recomputes every box from the triangles, so the boxes are exact. Leaves of more than 8 triangles (three-mesh-bvh's default is 10) are cut in half. A tree that reordered the index (not indirect) needs that index: setIndex: true (the default) copies data.index into the geometry as three-mesh-bvh does. Several roots (one per group) are joined; version 0 files are read as three-mesh-bvh reads them.
  • Limit: the tree must hold every triangle of the geometry. One built over a draw range or groups that leave triangles out is refused, since our trees hold every triangle.
  • The core has the same without three: serializeThreeMeshBvh(tree, { index }) and meshBvhFromThreeMeshBvh(data, { positions, indices }) from @pmndrs/bvh.

Tested against three-mesh-bvh 0.9.18: its deserialize reads our trees and its raycasts, box casts, validateBounds and getJSONStructure agree with ours; its trees (indirect or not, with groups, version 0) read into ours give its hits.

The helper

new MeshBVHHelper(mesh, depth) (or BVHHelper) is a Group that draws the tree's boxes down to depth (default 10) as line segments: displayEdges = false gives see-through faces, displayParents draws inner boxes too, with color, opacity, edgeMaterial, meshMaterial and instanceId. It follows the mesh's world matrix. Call update() after changing the tree or those fields. The boxes come from the core (bvhHelperPositions), so another engine can draw the same thing.