# 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.