Skip to content

The strand hair ​

A studio character's hair can move. When its package carries Sara's strand hair (hair/hair.json and hair/hair.bind.bin, written by the studio's packer), the engine makes her guide strands part of the character's skinning: every strand point is a bone slot after the rig's joints, the hair rows ride those slots with her weights, and her runtime moves the strands each frame from the rig's bone matrices. A package without hair/ draws exactly as before. (29 Oct 2026, Isaac's ask: the Full build's body, cloth and hair together.)

What a game does ​

Nothing. The hair comes with the character on both doors:

ts
// gameable/three
const character = await loadGameableCharacter(renderer, url); // hair: true by default
character.hair; // { strands, slots, rows, ms, frames, reset(), setParams() } or null

// the bridge
createCharacterBridge({ engine, renderer, scene, extras: { hair: true } });
bridge.entryOf(entity)?.hair; // the same state, once the character is attached

hair: false (the loader) or extras: { hair: false } (the bridge) loads the character with its hair as captured, skinned to the rig. hair.ms is her runtime's time on the last frame; hair.reset() puts the strands back at rest (after a teleport); hair.setParams() takes her runtime's own settings (HAIR_DEFAULTS in @gameable/character).

What the package carries ​

FileWhat it is
hair/hair.jsonHer builder's strands over the package's rows: guide curves on the scalp, each rooted on a rig bone, the rig bones they ride (proxies), 16 check rows.
hair/hair.bind.binHBND0001: the package rows that ride the strands (ascending), four slots and four weights each, then the hair mask over every row.

The studio's packer makes both after the package is assembled, as one CPU step on the studio's machine (her export of the package, her builder on the character's hair label, the result re-indexed to the package's own rows); character.json's hair names the folder and the counts, and package.files lists the two files with their hashes, so a host serves them with the rest.

How the engine draws it ​

  1. Slots. The bone table holds the rig's joints and then one slot per proxy and per strand point (hairSlotCount). The WGSL path sizes its uniform table per character (the rig's joints plus the slots, 128 at least); the TSL path's table grows to the same count (MAX_BONES is 1024). A package whose joints and slots would pass 1024 is drawn without its hair, and the console says so.
  2. Overrides at pack time. When the splat's records are packed, the rows named in hair.bind.bin get her four slots (offset past the rig's joints) and her four weights, and their face blend becomes 0 (hairRecordLanes, hairSplitLanes). Every other row keeps its own binding.
  3. Her runtime, each frame. After the rig's skin matrices are written, her HairSim takes them as her 3x4 rows (toRows12), advances by the frame's time (capped at 0.1 s), and its matrices for the slots are written after the rig's bones (fromRows12) before the table goes to the GPU. The runtime is hers, copied as is under packages/character/src/hair/vendor/ (hairSim_handover 0.5.0).
  4. The files must be this splat's. The hair is used only when hair.json's row count equals the package's and its 16 check rows' rest centres match within 0.1 mm (hairRestMismatch); otherwise the character loads without it and the console says why.
  5. Never on WebGL2. The fallback skin's bone table holds the rig alone; hair is null there.

What is not in yet ​

  • Body collision stays off until Sara's clean body lands (her README: the clean body is not the rigged drop-in yet). Her runtime runs with bodyCollide 0 and no skin grid: the strands swing from the rig's bones and settle on their own stiffness; they do not stop at the shoulders.
  • The cloth (her garment runtime) is not in; it follows in the same shape (slots after the rig's joints, overrides at pack time, her runtime ticked from the bone matrices).

The lab ​

examples/hair-lab draws one character on either door with the hair on or off, playing a clip, and measures the frame time and her runtime's time (window.__lab); its record(seconds) captures the canvas at 60 fps. The proof clips of the landing (HE-1, HE-2, HE-3) come from it.