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).
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user