First build

A vanilla WebGPU quick start, a three.js GPU quick start and a three.js CPU raycast.

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.create compiles the GPU builder's kernels (when the device has subgroups).
  • The first update builds the mesh with H-PLOC on the GPU, and also starts a tinybvh build in a worker.
  • Builds start only in update, so the first update() must come before await trees.whenBuilt(); without it the promise never resolves.
  • The next update after whenBuilt() places the tree. When tinybvh's tree arrives, a later update swaps 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