Files
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

153 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.