The tree object
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, withoutobject.shapecast({ intersectsBounds, intersectsTriangle, intersectsRange, boundsTraverseOrder }): boxes asBox3, triangles asExtendedTriangle(a threeTriangle), 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'straverse),serialize().getTriangleHitPointInfo(point, geometry, triangleIndex, target),estimateMemoryInBytes(tree),getBVHExtremes(tree)andvalidateBounds(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:
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), anddeserializeBoundsTree(bytes, geometry)orMeshBVH.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 indexedlayout) stays indexed, 16 bytes per triangle, its corners read from the geometry's own positions (CpuMeshBvh'scorners: 'positions'), with the same answers as the records tree. It reads the positions live, so after editing them callrefit()before querying the tree directly. - three-mesh-bvh's:
MeshBVH.serialize(tree)gives itsSerializedBVH({ version, roots, index, indirectBuffer }), which three-mesh-bvh'sMeshBVH.deserializereads, andMeshBVH.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,
indirectBuffermapping tree positions to triangles,indexa copy of the geometry's (cloneBuffers: falsehands over the same array). A geometry without an index givesindex: null; passsetIndex: falseto three-mesh-bvh'sdeserializethen. - 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) copiesdata.indexinto 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 })andmeshBvhFromThreeMeshBvh(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.