Skip to content

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.

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.

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.

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.