Mesh
<Mesh> rasterises a GLB alongside the raymarched SDF shapes, sharing their
depth attachment. See Depth and z-sort for
how it orders against them.
Loading is lazy: loadGLB and meshEngine are behind import(), so a scene
without a <Mesh> ships neither.
Skinning is done on the CPU
Section titled “Skinning is done on the CPU”Skinned positions are computed each frame and re-uploaded to one vertex buffer. This is slower than GPU skinning for high bone counts, and it is what makes the bind-pose buffer below possible.
The bind-pose buffer
Section titled “The bind-pose buffer”Any per-vertex effect that must stay fixed to the surface — roughness bumping,
dithering, per-vertex noise, generated texture coordinates — samples at
bindPosBuffer, not at the animated world position.
Sampling noise at the world position makes the pattern slide across the surface as the mesh animates.
bindPosBuffer holds the skinned bind pose, the output of
applySkeletalPose(data, bindMats) — not the raw data.positions from the
loader.
For skinned primitives such as hands, data.positions is in bone-local space,
clustered near the bone origin. The vertex buffer is rewritten at init by the
post-load applySkeletalPose(bindMats) call to produce the true bind pose;
bindPosBuffer must mirror that same rewrite.
Leave it at data.positions and the unskinned parts — body, head — bump
correctly while skinned sub-meshes get near-constant, sliding noise.
Reuse bindPosBuffer rather than adding a parallel buffer. If you do add one,
write it after the applySkeletalPose(data, bindMats) call, not during buffer
upload, or it captures pre-skinned positions.
Rotation tracks need shortest-path NLERP
Section titled “Rotation tracks need shortest-path NLERP”sampleTrack interpolates glTF animation tracks. Translation and scale are
component-wise lerps. Rotation is not.
// Rotation: check the dot product, negate, then lerp and normalise.if (dot(q1, q2) < 0) q2 = negate(q2)Antipodal quaternions describe the same rotation, but lerping between them takes the long arc through the wrong hemisphere — roughly a 270° swing.
Symptom. Wild bone swings on dynamic clips, most visibly arms and hands. Idle and walk rarely trigger it because consecutive keyframes stay close and the dot product stays positive.
Re-normalisation downstream hides it from any length or NaN assertion — you get a unit quaternion pointing the wrong way. A single component-wise loop covering every track type is the bug.