# Baked-skirt-gait spike — design note

**Status: SPIKE, not production.** Ozan 2026-08-20: prove that an offline cloth
sim of a skirt against the run cycle can be baked onto the `skirt_*` strand ring
and shipped as a layered animation clip. Script: `bake_skirt_gait.py` (this dir).

## What it does

1. Loads `Regular_Female_SkirtRig24.glb` (24 strands × 5 segments) and the long
   tapa skirt `Female_Tapa_Skirt_Regular.gltf`, re-pointing the garment's skin at
   the body rig.
2. Loads `Jog_Fwd_Loop` from `UAL1.glb` — this is the game's actual `run` clip
   (`PlayerController.cs`: UAL1 `Jog_Fwd_Loop` → alias `run`). The pack's other
   119 actions and its mannequin are discarded in-memory. A CYCLES fmodifier
   loops it; scene fps is set to 30 *before* import so seconds map to frames.
3. Cloth-sims the garment against the animated body, reusing
   `build_pugu_tifi.py`'s idioms (pin group at the waistband, body as collider
   8 mm outer, tension/compression 8, shear 4, bending 0.06, 6 mm contact).
   One addition the drape-only pipeline didn't need: **the waistband pin group is
   pelvis-weighted and the Armature modifier sits above Cloth**, so the pinned
   edge *rides the animated hip* instead of being nailed to world space.
   20 settle frames + 3 looped cycles; the last cycle is captured.
4. Bakes cloth → bones (mapping below), blends the loop seam, writes a
   skirt-bones-only action, renders sim-vs-bake proof frames, and exports one GLB
   containing one animation with **only `skirt_*` rotation tracks**.

## The cloth→bone mapping

- **Rest-pose binning.** Each garment vertex is assigned once, from its rest
  position, to a (strand, boundary-knot) cell: strand by azimuth around the
  pelvis axis, knot by nearest of the strand's 6 boundary heights (5 segment
  heads + hem tail). Same conventions as `skirt_garment_weights.py` — azimuth
  and ring heights read off the rig, never bone-name order.
- **Per-frame targets.** For each simulated frame, a cell's world-space centroid
  becomes the target position of that strand's boundary knot (in armature
  space). ~12 verts/cell on this garment; empty cells fall back to "hang
  straight down from the parent joint".
- **Chain fit, rotation-only.** Per strand, per frame: the root head stays glued
  to the pelvis-carried rest position (the waistband is pinned there — the cloth
  top *is* pelvis motion). Each segment takes the minimal rotation from its
  parent-propagated rest orientation that aims head→tail at its target centroid;
  twist is inherited down the chain so strands don't spin. The pose-space basis
  quaternion comes out analytically
  (`basis = (parent_pose @ parent_rest⁻¹ @ rest)⁻¹ @ M_desired`) — no
  constraints, no `bake_action`, no per-bone depsgraph updates.
- **No bone scale, no bone translation.** Bone lengths are fixed; cloth
  compression shows up as segment curl, never stretch. This keeps the clip
  compatible with anything that assumes rigid strand segments (the spring rig's
  collision particles included).
- **Loop seam.** Quat sign continuity is enforced per bone, then the last 6
  frames of the cycle are slerp-blended toward frame 0. The game wraps last
  key → first (`LoopMode.Linear`), so the seam is smooth by construction.

## Runtime integration options (for the game side to pick)

1. **Override per gait (recommended starting point).** When the locomotion state
   machine enters run/walk, `SkirtSpringRig` stops simulating and the skeleton
   plays the baked `skirt_*` clip on the same bones, synced to (or just
   cross-faded with) the body clip. Cheapest possible runtime cost (zero sim),
   deterministic silhouette, and the clip loops with the gait. Cost: a crossfade
   pop unless blended; no reaction to collisions while overridden.
2. **Additive/under-layer.** Keep the spring sim running but feed the baked pose
   as its rest/target (or as a low-weight animated base under the springs).
   Keeps collision response, regains baked flair. More machinery: spring sim
   needs a "goal pose" input it doesn't currently have.
3. **Replace the sim during gaits only** (hybrid of 1): sim for idle/dance (low
   amplitude, collision matters), baked for run/walk/sprint (high amplitude,
   where the sim visibly struggles). This matches how the industry uses baked
   cloth and is option 1 with a state gate.

Not evaluated: retargeting one baked clip across bodies. The bake is
body-specific (ring radii are measured per body; the cloth is fitted per body) —
per-body bakes are the honest answer, same doctrine as the garment generator.

## What breaks / changes with the 16×4 male ring

Nothing structural — strand/segment counts are read off the rig at runtime, so
the same script bakes 16×4. Three real differences:

- **Fewer, longer segments** (4 vs 5, non-uniform fracs) → each segment spans
  more cloth column; the centroid fit smooths out more fold detail. Expect
  slightly stiffer-looking hem on the male ring.
- **Wider strand spacing** (16 vs 24 around the ring) → each strand owns a wider
  azimuth wedge; lateral fold corners between strands get more visible. The
  `AZ_SPREAD` raised-cosine weighting that fixed piecewise-linear hem folds in
  `skirt_garment_weights.py` is the analogous fix on the *weights* side; the
  bake itself doesn't care.
- The male long tapa sits on different bodies (Superhero's quads) — the cloth
  sim, not the bake, owns that problem.

## Verified / not verified

**Verified (run 5, `run5.log`, `renders/`):** the sim holds together at run
amplitude (20 settle + 3 cycles, waistband riding the pelvis, legs pushing the
cloth — `sim_f01`/`sim_f15` show two genuinely different phases, cloth flaring
and lifting over the advancing thigh), and the bake tracks the sim's gross
motion phase-for-phase (`baked_f01` vs `sim_f01` is the same silhouette; the
skirt visibly rides and flares through the cycle, `baked_f01..f28` × 3 views).
No gross leg poke-through in any rendered phase. Exported
`out/SkirtJogFwd.glb`: one animation named `SkirtJogFwd`, 120 channels,
**rotation-only, all `skirt_*` targets**, 0.933 s (one gait cycle), 225 KB.

**Bake fidelity:** the 24×5 rotation-only centroid fit smooths fine folds — the
baked skirt is lumpier than the raw sim and the hem reads slightly ragged at
peak lift (`baked_f14`). Crumple near the front waistband is the *sim's* own
behavior at the pinned edge, not a bake artifact (visible in `sim_f01` too).

**Not verified:** in-game playback (the game side has no loader for this yet —
that is integration, out of spike scope); gait transitions; the 16×4 male ring
(same code path, never executed); walk/sprint clips; other garments.

As-built lessons from the spike runs (full log: `run5.log`):

- **The shipped garment GLTF has per-face vertices** (flat-shaded; every quad is
  its own island). A cloth sim on it shreds into confetti — the script
  `remove_doubles`-welds the imported mesh before anything else touches it.
- **Pin per island, not per mesh.** The rope ring's back-rise sits ~5 cm above
  the tapa tube's top row; one pin line off the mesh top leaves the tube
  unpinned and the whole skirt falls to the floor. Pinning is computed per
  connected island (rope island fully pinned, tube top band pinned).
- **Pinned verts must be pelvis-only.** The garment's inherited pelvis/thigh
  blend would drag the pinned waistband with the stride; the script zeroes all
  other groups on pinned verts.
- **glTF export with `export_force_sampling=True` (the default) exports TRS for
  all 185 bones**, not just the animated ones — 555 tracks including the body.
  `export_force_sampling=False` + `ACTIVE_ACTIONS` yields exactly 120 tracks,
  all `skirt_*` rotations. The exporter also won't name the animation on this
  path; the script renames it in the GLB JSON post-export.
- **Blender 5.1 has no `Action.fcurves`** — slotted actions only
  (`slots.new` → `layers.new` → `strips.new(type="KEYFRAME")` →
  `channelbags.new(slot=...)` → `bag.fcurves.new(..., group_name=...)`), and the
  object must bind `action_slot` (same for NLA strips).
- **Never `scene.frame_set` backward into a cached cloth range** to grab a
  still — it re-evaluates stale state (run 4 produced two byte-identical
  "different phase" sim renders). Sim stills are shot inside the capture loop.

## Open problems / unknowns

- **Sync contract.** The clip is one gait cycle keyed 1:1 with the body clip's
  cycle. If the game ever plays `run` at a scaled speed, the skirt layer must
  scale identically or the cloth will fight the legs.
- **Gait transitions.** Run→walk→idle crossfades of the skirt layer are
  unproven; a naive crossfade of two phase-locked clips can momentarily halve
  the flare amplitude.
- **Collision deafness during override.** With the spring sim off, nothing
  pushes the skirt off world geometry (stairs, sitting). The bake only knows
  the legs.
- **Multi-garment.** One clip per (body, garment, gait) — the tapa long/short
  and piupiu each need their own bake. Matrix is small today; worth a driver
  config if it grows.
- **Wind/idle sway.** Baked clips are dead when the character stands still;
  the spring sim (or a third, subtle idle bake) still owns that.
