Refits
A refit keeps a tree's shape and recomputes every box. SceneTrees uses both refits for you. Use them directly for
trees you place in your own buffers. Both need subgroups.
A top level over moving instances
import { GpuBvhRefit, leafSlotsOf, parentSlotsOf, type GpuLayoutMeshBvh } from '@pmndrs/bvh';
const refit = new GpuBvhRefit(device);
await refit.prepare();
// Once per tree shape, from a CPU copy of the tree:
const words = new Uint32Array(layout.nodes);
refit.setShape({
nodeWordOffset: 0,
nodeCount: layout.nodeCount,
parentSlots: parentSlotsOf(words, 0, layout.nodeCount, layout.parents),
leafSlots: leafSlotsOf(words, 0, layout.nodeCount),
generation: 1,
});
// Every frame, after a pass wrote one box per primitive (8 floats: min x, y, z, unused,
// max x, y, z, unused), in the order the tree's leaves index them:
refit.encode(pass, treeBuffer, primitiveBoxes);
BVH_INSTANCE_BOX_WGSL is a WGSL helper for writing one instance's world box from its transform and its mesh's box.
10,000 moving instances refit in about 0.09 ms of GPU time (RTX 3060 Laptop, Windows, two measurements of the refit's
own page).
One deforming mesh
import { GpuMeshRefit } from '@pmndrs/bvh';
const meshRefit = new GpuMeshRefit(device, { diagnostics: true });
await meshRefit.prepare();
// Once, from a CPU copy of the tree (any builder's), in its own buffers:
meshRefit.setTree(layout);
// For a mesh inside a scene's buffers, the placement says where, and in what node format:
// meshRefit.setTree(layout, meshRefitPlacement(scene, meshIndex));
// Every frame, after the pass that writes the moved vertices:
meshRefit.pollStats(); // maps an earlier frame's stats copy, without waiting
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
meshRefit.encode(pass, { nodes: nodeBuffer, triangles: triangleBuffer }, { positionBuffer: skinnedPositions, vertexCount, indexBuffer, indexFormat: 'uint32' });
pass.end();
meshRefit.copyStats(encoder); // with diagnostics only
device.queue.submit([encoder.finish()]);
// A frame or two late: the refit tree's SAH cost over its root's area. Rebuild when it drifts up.
const cost = meshRefit.latestStats?.relativeCost;
setTreetakes the shape from a CPU copy of the tree. A tree from tinybvh is on the CPU already; one from H-PLOC or the LBVH is read back once with the builder'sreadLayouts(trees).- Several deforming meshes need one
GpuMeshRefiteach, and can share compiled pipelines:new GpuMeshRefit(device, { kernels: new GpuMeshRefitKernels(device) }). - For a mesh inside a scene's shared buffers, pass
meshRefitPlacement(scene, meshIndex)tosetTree. It gives the word offsets and the scene's node format, and refuses merged meshes. Scenes store quantized nodes by default while builders write 64-byte trees, so a placement by hand must saynodeFormattoo:setTreethrows without it. With quantized nodes the refit runs on anf32copy and packs it into the scene.
When to rebuild
With diagnostics: true, both refits report the refit tree's quality, latestStats.relativeCost (its summed node
half-area over the root's), a frame or two late. Nothing else is read back. Rebuild when it has stayed well above the
built tree's cost (relativeSahCost(floats, words, 0, layout.nodeCount) over the built tree's nodes, on the CPU):
SceneTrees rebuilds after 60 updates above 1.1 times.
The CPU twin, refitOnCpu, gives the same boxes in TypeScript, for tests and for CPU trees (CpuMeshBvh.refit). It
reads 64-byte f32 nodes only and throws for quantized ones: refit those through an f32 copy (dequantizeNodes,
then quantizeNodeWords back), as SceneTrees.setPoseCpu does.