Scenes and the sync

A three scene graph as SceneTrees meshes and instances, kept in step by SceneTreesObject3DSync.

SceneTreesObject3DSync reads a three scene into a SceneTrees and keeps it in step: only what changed is pushed.

import { SceneTrees } from '@pmndrs/bvh';
import { SceneTreesObject3DSync } from '@pmndrs/bvh/three';
import type { Mesh, Object3D } from 'three';

const trees = await SceneTrees.create(device); // static instances merge by default (mergeStaticInstances)
scene.updateMatrixWorld(); // the world matrices are read as they are
const sync = new SceneTreesObject3DSync(trees, scene, {
    static: (object) => !object.userData.moves, // static instances can merge, and are never pushed again
    material: (mesh) => materialIndexOf(mesh), // becomes hit.material (0-65534)
    layers: true, // layers 0-7 become the instance mask (default)
});

// After adding or removing objects, or changing geometry, groups or materials:
sync.sync();
// Every frame: moved, shown, hidden or re-layered objects only.
sync.update();
  • sync() after objects are added or removed, or a geometry, draw range, group or material changes: only the meshes and instances that changed are added or removed. It walks and plans before it changes anything, so a throwing material callback leaves everything as it was.
  • update() each frame: it skips an object whose matrixWorld, effective visibility, layers and instanceMatrix.version are unchanged, without allocating. Set instanceMatrix.needsUpdate = true after setMatrixAt, as three needs anyway.
  • Then trees.update(encoder), submit, and bvh.update() if you use BvhTraceNodes.

What is read

  • Mesh, InstancedMesh (instances below its count), BatchedMesh (active, visible instances) and SkinnedMesh (posed on the GPU with a renderer; Skinned and morphed meshes).
  • The triangles three's Mesh.raycast tests: the draw range and, with an array of materials, the groups within it. Ranges are read as three steps through them, even off a multiple of 3 or overlapping.
  • Multi-material meshes (an array of materials, with groups) are folded: one mesh holding every group, in group order, and one instance per object (and per instance of an InstancedMesh). Each triangle carries its group's material (triangleMaterials, from material(mesh, info), asked once per group with info.group and info.materialIndex). The sync, bvhSceneFromObject3D and BvhSceneRaycaster fold the same way, so instance N is the same object in all three. A folded source's group and materialIndex are -1 (the hit's triangle names its group: see "Hits back to objects" below). An object whose instances give a group different materials (an InstancedMesh with a material per instance) is not folded: it keeps one mesh per group, shared by its instances, with each instance's material on the instance. multiMaterial: 'groups' keeps one mesh and instance per group for every object, as the sync and the scene raycaster did before.
  • Indexed, non-indexed and interleaved positions. Interleaved, normalized or non-float positions are compacted once per geometry.
  • One mesh per geometry and range, shared by every object and instance that uses it.
  • Visibility: an object counts as visible when it and every ancestor are. Hidden objects stay in as hidden instances.
  • Layers: layers 0-7 become instance mask bits 0-7; layers 8-31 are dropped. An object on none of layers 0-7 gets mask 0, which no ray hits. A default object (layer 0) gets 0x01. layers: false gives every instance 0xff.
  • Materials: the material(mesh, info) callback is read at sync() only. After changing what it returns, call sync().

Call scene.updateMatrixWorld() first: world matrices are read as they are.

Hits back to objects

import { threeGroupOf, threeMaterialOf } from '@pmndrs/bvh/three';

const source = sync.sourceOf(hitInstance); // { object, instanceId, group, materialIndex, mesh, static }
const meshSource = sync.meshSourceOf(hitInstance);
if (source && meshSource) {
    console.log(source.object.name, source.instanceId);
    // A folded multi-material mesh has group -1: the hit's triangle names its group and three material.
    const group = threeGroupOf(meshSource, hitTriangle); // { group, materialIndex, material, ... } or null
    const material = threeMaterialOf(source, meshSource, hitTriangle); // object.material[group.materialIndex], or object.material
    console.log(group?.materialIndex, material?.name);
}

hit.triangle is the triangle within its mesh (a folded mesh's groups one after another, a group, a draw range or a batch range). threeFaceIndex(meshSource, triangle) turns it into three's faceIndex; sync.meshSourceOf(instance) gives the mesh source (BvhSceneRaycaster.meshSourceOf, or scene.meshSources[source.mesh], elsewhere).

hit.material is what your material callback returned for the hit triangle's group (or the instance's answer, for a mesh of one material). To get from a hit to three's object.material[i], either return info.materialIndex (or a value you map back from it) in the callback, or call threeMaterialOf(source, meshSource, hit.triangle), which works without a callback. threeGroupOf(meshSource, hit.triangle) gives the group itself.

sync.meshHandles lists the SceneTrees mesh handles the sync holds (one per mesh it added: a shared geometry range, or a posed object's own), for trees.meshStatus(handle) and stats. It is a new array each time; sync() can add and remove handles. Merged static groups are SceneTrees' own (trees.groupStatus).

// The SceneTrees mesh handles the sync holds now: one per shared geometry (or posed object).
let nodes = 0;
for (const handle of sync.meshHandles) nodes += trees.meshStatus(handle).placement?.nodeCount ?? 0;

Static objects

static: true, or (object, instanceId) => boolean, marks instances for mergeStaticInstances (Merged static instances). The sync never pushes changes to static instances.

After SceneTrees merged an object's static instances, sync() skips removing that object or changing its geometry, draw range, groups or materials, and warns once per object (skipped in the counts). Mark objects that may go away static: false.

Without SceneTrees

bvhSceneFromObject3D(root, options) reads the scene once and returns meshes, instances and their three sources. Build it with buildTraceScene(scene.meshes, scene.instances, { build }), upload it with uploadTraceScene, and BvhTraceNodes takes the result as { buffers }.