CPU raycast in three
These are drop-ins for three-mesh-bvh's functions of the same names. They run the core's CPU walk over one tinybvh tree per geometry.
import { BufferGeometry, Mesh as ThreeMesh, Raycaster, SphereGeometry, MeshBasicMaterial, DoubleSide, Vector3 } from 'three';
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, prepareBoundsTrees, SAH } from '@pmndrs/bvh/three';
await prepareBoundsTrees(); // once: loads tinybvh
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
ThreeMesh.prototype.raycast = acceleratedRaycast;
const mesh = new ThreeMesh(new SphereGeometry(1, 256, 128), new MeshBasicMaterial({ side: DoubleSide }));
mesh.geometry.computeBoundsTree({ strategy: SAH, maxLeafSize: 8 }); // three-mesh-bvh's options are accepted
const raycaster = new Raycaster(new Vector3(0, 0, 5), new Vector3(0, 0, -1));
raycaster.firstHitOnly = true;
const [hit] = raycaster.intersectObject(mesh); // distance, point, face, faceIndex, barycoord, uv, normal, ...
What it returns
The same intersections as three's Mesh.raycast:
- the draw range and, with an array of materials, the groups; ranges off a multiple of 3 and overlapping groups too;
- each material's
side(front, back or double), read at every raycast; raycaster.nearandfar;- the same fields:
distance,point,face(a,b,c,normal,materialIndex),faceIndex,barycoord,uv,uv1andnormal; InstancedMeshandBatchedMeshthroughMesh.raycast(a tree per batched geometry withcomputeBatchedBoundsTree, below, or one over the whole batch withbatchedMesh.geometry.computeBoundsTree()).
Without firstHitOnly every hit is returned, as three does. Tested against three on 8,000 random rays, and 5,000 more
through ranges and groups off a multiple of 3. The one difference: three reports a triangle that reads past the end of
the index at a NaN distance; we report nothing there.
When it falls back to three's raycast
- The geometry has no tree.
- A
SkinnedMesh, or a mesh with morph targets (three poses each vertex on the CPU; that is exact), unless its tree is aSkinnedMeshBVH, which holds the pose (A tree that follows the pose). - The position attribute's version, or the index, changed since
computeBoundsTree. It warns once and stays on three's raycast untilgeometry.boundsTree.refit()(same index) orcomputeBoundsTreeruns again.
BatchedMesh
A tree per geometry of a BatchedMesh, as three-mesh-bvh's computeBatchedBoundsTree makes them: each instance's
raycast walks only its own geometry's tree.
import { BatchedMesh, Mesh } from 'three';
import { acceleratedRaycast, computeBatchedBoundsTree, disposeBatchedBoundsTree } from '@pmndrs/bvh/three';
Mesh.prototype.raycast = acceleratedRaycast; // other meshes; the BatchedMesh needs only its own line below
BatchedMesh.prototype.raycast = acceleratedRaycast; // each instance walks its geometry's tree
BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
batched.computeBoundsTree(); // a tree per geometry, in batched.boundsTrees
// After batched.setGeometryAt(changedGeometry, geometry): that geometry's tree again, the others refitted.
batched.computeBoundsTree(changedGeometry);
for (const tree of batched.boundsTrees ?? []) tree?.refit();
computeBoundsTree(index = -1, options)builds every geometry's tree intobatchedMesh.boundsTrees(by geometry id) and returns the array, or builds one geometry's tree and returns it (null past the geometries, or for a deleted one). The options arecomputeBoundsTree's.disposeBoundsTree(index = -1)drops one tree or all of them.- Each tree is a
BvhBoundsTreeover that geometry's range of the batched geometry. The hits are three's:faceIndex,faceand the interpolated attributes over the whole batched geometry, andbatchId. three-mesh-bvh reorders the batch's index in place, so itsfaceIndexnumbers differ from three's; ours don't. BatchedMesh.prototype.raycast = acceleratedRaycastis enough for aBatchedMesh: it runs three'sBatchedMesh.raycastloop (active, visible instances, each geometry's draw range, matrix, box and sphere) through a stand-in of its own that walks the trees, soMesh.prototype.raycastmay stay three's. With onlyMesh.prototype.raycastset, three's ownBatchedMesh.raycastsends its stand-in there and the trees are walked too. A geometry with no tree gets three's raycast either way.- TypeScript: with three-mesh-bvh's types in the same program,
computeBoundsTreeandboundsTreesonBatchedMeshmay take three-mesh-bvh's types; see Both libraries in one TypeScript program. - After
setGeometryAt, or another edit of the batch's positions or index, the trees are stale: raycasts fall back to three's, with one warning, until the changed geometry's tree is built again and the others are refitted or rebuilt. - A geometry whose index count isn't a multiple of 3 loses the one triangle three would read past its range.
Tested against three's raycast and three-mesh-bvh's accelerated BatchedMesh raycast, indexed and not, and with only
BatchedMesh.prototype.raycast set.
A whole scene
BvhSceneRaycaster is three-mesh-bvh's ObjectBVH: one walk over the scene's top level and its mesh trees, in place
of a raycast per object.
import { BvhSceneRaycaster, raycastScene } from '@pmndrs/bvh/three';
import { Frustum, Matrix4, type Camera } from 'three';
await prepareBoundsTrees();
const picker = new BvhSceneRaycaster(scene); // a tree per geometry and range, and the top level
const hits = raycastScene(picker, raycaster); // what raycaster.intersectObject(scene, true) returns
// After objects move, show, hide, or come and go:
scene.updateMatrixWorld();
picker.update(); // the top level only, unless meshes changed
const frustum = new Frustum().setFromProjectionMatrix(new Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
const seen = picker.intersectsFrustum(frustum, camera.layers); // [{ object, instanceId, group }]
- It returns three's
intersectObject(root, true)with three's ownMesh.raycast: the same triangles,side,near/far,instanceIdandbatchId, sorted. Hidden objects are hit too, as three'sRaycasterhits them (includeHidden: falseskips them).raycaster.firstHitOnlyreturns the one closest hit over the scene. - Multi-material meshes are folded as
bvhSceneFromObject3Dand the sync fold them (one instance per object, the groups in one mesh;multiMaterial: 'groups'splits them), sopicker.sourceOf(i)is the same source asscene.sources[i]. Each hit still gets its group'ssideandface.materialIndex;picker.meshSourceOf(i)withthreeGroupOforthreeMaterialOfgives a hit's group from its triangle. - Differs from three: layers 8-31 aren't seen, skinned and morphed meshes are read in their rest pose, and points, lines and sprites aren't read.
- Raycasts see the scene as of the last
update(). Callscene.updateMatrixWorld()andupdate()after objects move, show or hide, come or go, and after geometry edits. With the same meshes it rebuilds only the top level. intersectsFrustum(frustum, layers)walks the top level with three'sFrustum.intersectsBoxand returns the objects and instances whose world box meets it.
Tested against three's intersectObject on 10,000 rays before and after moves, and 300 camera frustums against brute
force.
Shapecast over the objects
picker.shapecast(callbacks) is three-mesh-bvh's ObjectBVH.shapecast: the walk over the objects' world boxes that the
raycast uses, with your own tests. Use it to find what a shape (a sphere, a box, a capsule) might touch, then test those
objects' triangles.
import { BvhSceneRaycaster, CONTAINED, INTERSECTED, NOT_INTERSECTED } from '@pmndrs/bvh/three';
import type { Object3D, Sphere } from 'three';
const picker = new BvhSceneRaycaster(scene); // after `await prepareBoundsTrees()`
const near: Object3D[] = [];
picker.shapecast({
// World boxes of the objects (and instances); CONTAINED takes a subtree whole.
intersectsBounds: (box) => {
if (!sphere.intersectsBox(box)) return NOT_INTERSECTED;
return sphere.containsPoint(box.min) && sphere.containsPoint(box.max) ? CONTAINED : INTERSECTED;
},
// Each object in an entered box: instanceId is the InstancedMesh or BatchedMesh instance, else 0.
intersectsObject: (object, instanceId) => {
console.log(object.name, instanceId);
near.push(object);
return false; // true stops the cast
},
});
intersectsBounds(box, isLeaf, score, depth, nodeIndex): a worldBox3(scratch, valid during the call). ReturnNOT_INTERSECTED(or anything falsy) to skip it,INTERSECTEDto enter it,CONTAINEDto hand over every object under it without asking about the boxes below.intersectsObject(object, instanceId, contained, depth, source): each object in an entered box.instanceIdis theInstancedMeshorBatchedMeshinstance, else 0, as in three-mesh-bvh;sourceis ours ({ object, instanceId, group, ... }; a multi-material mesh withmultiMaterial: 'groups'comes once per group). True stops the cast.intersectsRange(offset, count, contained, depth, nodeIndex, box)andboundsTraverseOrder(box)work as in three-mesh-bvh. Ranges are positions in the top level (picker.cpuScene.instanceAt(position)).- The boxes are the padded world boxes of each object's mesh tree, not three-mesh-bvh's (the geometry's box shrunk to
its sphere); both hold the object. Hidden objects are in unless
includeHidden: false; aBatchedMesh's hidden instances are not. - Without three:
CpuSceneBvh.shapecast({ intersectsBounds, intersectsInstance, intersectsRange, boundsTraverseOrder }), with instances byinstanceIndex, andCpuSceneBvh.traverse.
Tested call for call against three-mesh-bvh's shapecastTraverse run over the same top level, and against
ObjectBVH.shapecast on sphere casts before and after objects move.
Off the main thread
import { GenerateMeshBvhWorker } from '@pmndrs/bvh/three';
const generator = new GenerateMeshBvhWorker(); // on @pmndrs/bvh/worker's pool; inline in Node
geometry.boundsTree = await generator.generate(geometry, { maxLeafSize: 8, onProgress: (p) => console.log(p) });
generator.dispose(); // stops the pool and rejects unfinished builds
GenerateMeshBvhWorker builds on @pmndrs/bvh/worker's pool (Workers). The geometry's arrays are
copied, not moved, so it stays usable, and several builds may run at once. Pass { pool } to share a pool.
Speed
On a 100,000-triangle torus knot, Node 22 on the Apple M5 (indicative): 692,630 firstHitOnly raycasts per second
against three-mesh-bvh's 664,652 (CENTER) and three's 294; build 28 ms against 31 ms (CENTER) and 95 ms (SAH). How
it was measured: Raycast. In the browser (three/raycast example, headless Chrome on
the M5): 0.40 million raycasts per second against three's 306.