Guide5 min read4 sections

Skeletal model deformation

docs/SKELETAL_ANIMATION.mdSource updated 2026-10-24
Markdown
On this page 4 sections

The development branch imports GLB skins, samples their joint poses and renders linear blend deformation in color and shadow passes. This extends the rigid clip evaluator. Authored world models can carry rest palettes through ECS presentation and checkpoint reconstruction. Playable traveler characters still use the existing procedural representation; actor animation state and character art integration remain future work.

#Import and ownership

Artifact format 6 (glb-skinned.1) records ordered joint node IDs, inverse-bind matrices and four joint/weight lanes per emitted vertex. Compiler jobs use glb-compiler.6 and asset-job.6. Existing non-skinned artifacts retain their formats and content identities.

The importer accepts unsigned byte/short JOINTS_0 and FLOAT or normalized unsigned byte/short WEIGHTS_0, with matching vertex counts. Weights must be nonnegative and sum to one within the source encoding tolerance; the owned artifact normalizes them. All four joint indices are checked, including zero-weight lanes. Duplicate positive-weight influences and additional influence sets fail. Flat-normal expansion and tangent seam processing preserve the influences.

Each skin has 1–64 distinct joints; a model has at most 16 skins and 256 declared joints in total. Joints must belong to the selected hierarchy and share an ancestor. An optional skeleton node must be an ancestor of every joint. Inverse binds use FLOAT MAT4 accessors with at least as many elements as joints; only the corresponding joint matrices are consumed. Missing inverse binds are identity. Consumed matrices must be finite, affine, nonsingular and within the numeric budget. The independent artifact reader validates these records, influences, rest bounds, resource census and checksum again.

A palette entry is the evaluated joint world matrix multiplied by its inverse bind. Unit conversion is included in the joint world matrix. The skinned mesh node's transform does not transform its own geometry; it still participates in the hierarchy for children. Imported bounds and optional triangle collision use the weighted rest positions, followed by external world placement. Collision does not deform during animation.

#Pose and presentation

ModelSkeletonPlayer.create owns and validates the artifact. sample(clipIndex, seconds, loop) starts from authored rest transforms and evaluates a complete pose without advancing simulation or owning a clock. sample(null, seconds) returns the rest pose. It returns owned joint worlds, per-node palettes, conservative bounds and drawable objects. Shared geometry can have independent palettes in separate instances.

All joint matrices in a prepared skin must share an orientation. A common reflection is handled by reflecting mesh geometry, normals and tangent frames, reversing winding and correcting the palette. Mixed joint orientations and a sampled orientation change fail before publication. Singular or out-of-budget samples also fail; admission does not promise that every point on every imported curve is renderable.

Presentation format 4 carries the matrices explicitly. Older unskinned scenes continue to use formats 2 or 3. Each object has at most 64 matrices, and the scene plus automatically installed models has a shared 4,096-matrix limit. World compilation counts each primitive's palette before admission. Geometry-specific validation ensures every referenced joint is available before accepting a baseline or delta. Invalid updates preserve the last accepted scene.

#Rendering and resources

Shader profile forward-shaders.6 compiles 17 pipelines atomically, adding single/double-sided skinned color and shadow variants. A skinned mesh owns separate position/normal/UV, tangent, index and joint/weight buffers. Joint indices remain uint32; weights are float32. One fixed 262,144-byte storage buffer per skinned catalog holds up to 4,096 matrices. The 112-byte instance ABI stores the palette offset in its previously unused emission W lane.

Both color and shadow vertices use the same weighted transform. Normals use the inverse transpose of the blended linear transform, with a finite fallback for degenerate blends; tangents are transformed and reorthogonalized. Existing texture, normal/occlusion and alpha-cutoff material behavior is shared. Common reflection and independent instance palettes retain mesh sharing. Camera and shadow visibility use the current joint influence bounds with float32 padding, rather than the rest mesh bounds.

Catalog admission conservatively counts rigid/skinned reflected variants, influences, material uniforms, textures and the palette. Candidate plus accepted GPU ownership remains bounded to 32 MiB. Partial allocation unwinds owned resources; replacement, cancellation and device loss use the existing ownership fences. Resource counts describe logical engine ownership, not physical VRAM or hardware performance.

#Evidence and remaining work

The analytic panel fixture has two joints, a deliberately displaced/reflected mesh node and a translation clip. Additional fixtures exercise common reflection and joint rotation. Independently authored static references calculate the blended positions, normals and tangents directly; they do not call the production skin evaluator. Regenerate the nine source/reference pairs with node --import tsx tools/cli/skeletal-model-fixture.ts.

The Observatory compares nine GPU reference frames: translation, reflection, rotation, two instances sharing geometry with different palettes, and directional/point shadows. Both shadow checks include a broken-deformation control that must change the image. It also checks looping, current-pose culling, invalid-palette frame preservation, stable resource counts across twelve poses, exact restoration after actual device destruction, and complete disposal. Headless coverage includes malformed source/artifacts, influence encodings and expansion, independent blended-position bounds, reflected collision, shared meshes, palette transport/reservations, partial GPU allocation, portable compiler execution and authored-world checkpoint restoration.

This is a bounded skeletal rendering foundation. Animation blending, attachment APIs, simulation-owned actor animation state, network/replay semantics for that state, original playable character art, animated collision policies, physical GPU/mobile acceptance and the broader roadmap gates remain open. No full engine milestone is closed. The current beta is not rebuilt or deployed by this increment.

Acceptance evidence records source aabb267, 272 passing headless tests, the full 26-scenario browser run and the final committed-source renderer follow-up. All nine skeletal GPU references have zero maximum channel error; breaking shadow deformation changes 6,890 and 5,097 pixels for directional and point lighting respectively. The catalog owns 34 buffers, three textures and 264,564 logical bytes, with zero retained resources after disposal. Prior remote failures remain recorded in animation CI; remote skeletal acceptance is pending.

The subsequent animation-state integration connects these evaluators to the simulation clock, canonical ECS projection and full native checkpoints. Shared actor transport, lightweight player saves and original playable character integration remain open.

End of Skeletal animation