Deforming meshes
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,setPoseis skipped (deformingCounts.posesSkipped, with one warning) and rays see the pose the tree was built in: usesetPoseCputhere. - 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
f32copy 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 withnodeFormat: 'f32'holds 64 and needs no copy.
To refit trees outside SceneTrees, see Refits.