First build
Vanilla WebGPU
SceneTrees holds a scene's trees on one device. Add meshes and instances, call update(encoder) once per frame, and
bind its buffers to the walk. This traces one ray with the package's batch kernel:
import { HIT_RECORD_BYTES, IDENTITY_TRANSFORM, packRays, RAY_BYTES, SceneTrees, TraceBatchKernel } from '@pmndrs/bvh';
const trees = await SceneTrees.create(device);
const mesh = trees.addMesh({ positions, indices });
trees.addInstance({ mesh, objectToWorld: IDENTITY_TRANSFORM });
const update = () => {
const encoder = device.createCommandEncoder();
trees.update(encoder); // starts builds, places finished trees, refits the top level
device.queue.submit([encoder.finish()]);
};
update();
await trees.whenBuilt(); // every mesh has a tree
update();
// One ray, traced with the package's batch kernel.
const kernel = await TraceBatchKernel.create(device);
const rays = device.createBuffer({ size: RAY_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(rays, 0, packRays([{ origin: [0, 0, 5], direction: [0, 0, -1], tMin: 0, tMax: 1e30 }]));
const results = device.createBuffer({ size: HIT_RECORD_BYTES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
const encoder = device.createCommandEncoder();
kernel.encode(encoder, trees.traceBuffers, { mode: 'closest', rays, results, count: 1 });
device.queue.submit([encoder.finish()]);
What happens:
SceneTrees.createcompiles the GPU builder's kernels (when the device hassubgroups).- The first
updatebuilds the mesh with H-PLOC on the GPU, and also starts a tinybvh build in a worker. - Builds start only in
update, so the firstupdate()must come beforeawait trees.whenBuilt(); without it the promise never resolves. - The next
updateafterwhenBuilt()places the tree. When tinybvh's tree arrives, a laterupdateswaps it in. See Fast first trees.
Read the hit back:
import { HIT_RECORD_WORD } from '@pmndrs/bvh';
const readback = device.createBuffer({ size: HIT_RECORD_BYTES, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(results, 0, readback, 0, HIT_RECORD_BYTES);
device.queue.submit([encoder.finish()]);
await readback.mapAsync(GPUMapMode.READ);
const floats = new Float32Array(readback.getMappedRange());
const words = new Uint32Array(floats.buffer);
const t = floats[HIT_RECORD_WORD.t]; // < 0: a miss
if (t >= 0) console.log('hit triangle', words[HIT_RECORD_WORD.triangle], 'of instance', words[HIT_RECORD_WORD.instance], 'at', t);
readback.unmap();
To trace from your own shader instead of the batch kernel, see Tracing in WGSL.
three.js on the GPU
The same trees, on the renderer's own device, kept in step with the scene graph, and read from TSL or WGSL:
import { Fn, If, vec3 } from 'three/tsl';
import { WebGPURenderer, type Scene } from 'three/webgpu';
import { BvhTraceNodes, deviceFromRendererAsync, SceneTreesObject3DSync, type BvhTslNode } from '@pmndrs/bvh/three';
const renderer = new WebGPURenderer();
const device = await deviceFromRendererAsync(renderer); // the renderer's own GPUDevice
const trees = await SceneTrees.create(device);
const sync = new SceneTreesObject3DSync(trees, scene); // adds every mesh and instance under `scene`
const bvh = new BvhTraceNodes(renderer, trees); // the walk in three's node system
// A TSL compute pass that traces (a material's nodes can call the same helpers):
const kernel = Fn(() => {
const hit = bvh.traceClosest(vec3(0, 1, 5), vec3(0, 0, -1)).toVar() as BvhTslNode;
If(hit.get('t').greaterThanEqual(0), () => {
/* hit.get('normal'), hit.get('instance'), hit.get('material'), ... */
});
})().compute(1);
// Each frame, before three uses the nodes:
const frame = () => {
scene.updateMatrixWorld();
sync.update(); // moved objects only
const encoder = device.createCommandEncoder();
trees.update(encoder);
device.queue.submit([encoder.finish()]);
bvh.update(); // rebinds if SceneTrees replaced a buffer
renderer.compute(kernel);
};
More in three.js: scenes and the sync and BvhTraceNodes.
three.js on the CPU
Drop-ins for three-mesh-bvh's functions of the same names. The raycast returns what three's Mesh.raycast returns:
import { BufferGeometry, Mesh, Raycaster, type Camera, type Vector2 } from 'three';
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, prepareBoundsTrees } from '@pmndrs/bvh/three';
await prepareBoundsTrees(); // loads tinybvh (WebAssembly) once
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
Mesh.prototype.raycast = acceleratedRaycast;
mesh.geometry.computeBoundsTree();
const raycaster = new Raycaster();
raycaster.setFromCamera(pointer, camera); // pointer: -1 to 1 on both axes
raycaster.firstHitOnly = true; // optional: the closest hit only
const hits = raycaster.intersectObject(mesh); // the same intersections three's raycast returns
More in CPU raycast in three.
Without SceneTrees
SceneTrees is the usual entry point. For a single tree, or full control over buffers, use a builder directly
(Choosing a builder) and buildTraceScene with uploadTraceScene
(Serialization and baking has an example).
Where next
- Scenes that change while they run: Runtime changes, Refits and Deforming meshes.
- Writing your own kernel: Tracing in WGSL, or Ray batches to trace without one.
- Picking and collision on the CPU: Raycast.
- Coming from three-mesh-bvh: the mapping.