Files
animation/clothing/skirt_gait_spike/NOTES.md
T
ozan 70d4312f4a feat(clothing): skirt gait bake spike — Blender cloth sim baked to strand bones
bake_skirt_gait.py: headless Blender CLOTH sim of the long tifi against the
UAL1 Jog_Fwd_Loop run cycle, baked onto the skirt_* ring as a rotation-only
clip (out/SkirtJogFwd.glb, 0.933s loop, 120 tracks). NOTES.md: cloth→bone
centroid-fit mapping, runtime integration options (override/additive/hybrid),
Blender 5.1 slotted-action + export_force_sampling gotchas. Renders: sim vs
bake phase proofs. Game-side loader lives in the ariki-game clothing bed
(Run Baked).
2026-08-20 17:07:44 +01:00

9.0 KiB
Raw Blame History

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.newlayers.newstrips.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.