Deforming meshes

Skinned and morphed meshes whose vertices move on the GPU, refit every frame and rebuilt when the tree drifts.

A deforming mesh keeps its triangles and their connectivity; only the vertices move. SceneTrees keeps the shape of the tree its builder made and refits the boxes from each pose on the GPU, with nothing read back per vertex.

// bounds: a box every pose stays in (object space), kept by the top level. true: the rest pose's box.
const mesh = trees.addMesh(character, { deforming: { bounds: [-1, 0, -1, 1, 2, 1] } });
trees.addInstance({ mesh, objectToWorld: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] });

// Each frame, after your pass wrote the posed vertices (STORAGE and COPY_SRC usage):
trees.setPose(mesh, { positionBuffer: skinnedPositions, vertexCount, indexBuffer, indexFormat: 'uint32' });
// the next trees.update(encoder) refits the tree on the GPU, and rebuilds it if the refit drifted.
console.log(trees.driftRatio(mesh), trees.deformingCounts);

The pose

setPose(mesh, vertices) takes the moved vertices as a GPU buffer, in the mesh's own vertex order and indices: positionBuffer, vertexCount, and optionally positionStrideFloats, positionOffsetFloats, indexBuffer, indexFormat and indexOffset. The buffer needs STORAGE usage, and COPY_SRC for drift rebuilds.

The next update(encoder) refits the tree before anything traces: GpuMeshRefit rewrites the corners of the mesh's triangle records and refits its boxes bottom-up, in the update's encoder. A tree placed later (a swap, a rebuild) is refit at once from the latest pose, so a tree built from an older pose never shows.

In three, the sync does all of this for SkinnedMesh and morph targets: see Skinned and morphed meshes.

Poses on the CPU

setPoseCpu(mesh, positions) takes the pose as a Float32Array (x, y, z per vertex, the mesh's own vertices), with or without subgroups:

const mesh = trees.addMesh(cloth, { deforming: true });
trees.addInstance({ mesh, objectToWorld: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] });

// x, y, z per vertex, the mesh's own vertices; with or without subgroups.
trees.setPoseCpu(mesh, posedPositions);
// the next trees.update(encoder) refits the tree on the CPU, uploads it and moves the top-level box.
console.log(trees.deformingCounts.cpuPoses);

The next update(encoder) refits the tree on the CPU (refitLayoutToPose), copies its nodes (re-quantized with quantized nodes) and corners (indexed: its vertices) in through the update's encoder, and moves the mesh's top-level box to the posed root box. A tree placed later gets the latest pose too. It costs a pass over the triangles and an upload of the mesh's tree per pose, so it suits small meshes or a few poses a second. With subgroups the latest of setPose and setPoseCpu wins; without them setPose is skipped, so the last setPoseCpu stays. A GPU-built first tree works too: a deforming mesh's GPU build reads its layout back for this. If a placed tree has no layout (its readback failed, or a custom first-tree builder ignored readLayouts) it warns once, and the pose waits for the next tree (tinybvh's refinement).

The deform box

The top level holds the mesh's deform box, a box every pose stays in, not the refit root's box. So a pose never needs a readback or a new top-level shape. Give it with deforming: { bounds } at addMesh (true uses the rest pose's box). For poses that leave it, such as a character walking, call setDeformBounds(mesh, box); every instance of the mesh is refit into the top level at the next update.

Drift and rebuilds

A refit tree gets worse as the pose moves away from the one it was built in. The refit's relative SAH cost is copied out at most once every 15 updates per posed mesh (STATS_EVERY_FRAMES) and read back a few frames later. When every sample has stayed above 1.1 times the built tree's cost (DRIFT_THRESHOLD) for 60 updates (DRIFT_HOLD_FRAMES), the mesh is rebuilt with H-PLOC from the current pose. A sway rarely holds that long; a bend that stays does.

driftRatio(mesh) gives the latest ratio. deformingCounts counts refits, rebuilds and poses skipped.

To rebuild without waiting for the drift rule (after a pose change you know is big: a ragdoll, a cut, a teleported limb), rebuild(mesh) asks for it: the next update starts an H-PLOC build from the latest GPU pose, and the update after its batch comes back places it, as a drift rebuild is (onBuild reports it with rebuild: true; deformingCounts.driftRebuilds counts both). One rebuild starts per update, so several asked at once go one after another. It returns false, and does nothing, when there is nothing to rebuild from: no subgroups (no GPU builder; CPU poses are refit only), no setPose yet, or no tree placed yet. meshStatus(mesh).deforming says which meshes deform.

// After a big pose change (a ragdoll, a cut): rebuilt from the latest GPU pose at the next update, drift or not.
if (!trees.rebuild(mesh)) console.log('nothing to rebuild from: no subgroups, no GPU pose or no tree yet');

Rules

  • Deforming meshes never merge, and each posed object needs its own mesh (its own pose).
  • Without subgroups, setPose is skipped (deformingCounts.posesSkipped, with one warning) and rays see the pose the tree was built in: use setPoseCpu there.
  • With indexed leaves, a deforming mesh is not welded (its vertices must stay the app's).
  • With quantized nodes (the default), the refit runs on a 64-byte f32 copy of the nodes, and a pass re-quantizes it into the scene. A deforming mesh so holds 96 bytes per node (32 in the scene, 64 in the copy). A scene made with nodeFormat: 'f32' holds 64 and needs no copy.

To refit trees outside SceneTrees, see Refits.