feat(pipeline): grow skirt and hair bone chains for a costume that has none

Miku shipped with 0 cloth chains against Taila's 35, so her twin tails
hung off her skull like a helmet. Nothing downstream could fix it: the
spring solver simulates cloth BONES, and a garment with none is welded to
whatever body bone it was weighted to. Every auto-rigged model is in that
state, and her source was an unrigged mesh.

tools/cloth_bones.py builds them, which is the job a technical artist does
by hand on a model like this. It finds the geometry by MATERIAL SLOT — the
artist already answered which surface is hair, and on a joined mesh (what
the auto-rig leaves behind) the slot is the only separation left. Hair is
split into connected islands, because a strand is a connected piece of
surface and clustering by position would merge two ponytails passing near
each other. A skirt is split into radial wedges instead, because a skirt
is ONE connected surface and islands would return the whole thing as a
single piece — the bell-shaped failure. Each clump gets a polyline fitted
down its middle by binning vertices by distance and taking centroids, so
the chain follows the piece's own curve rather than cutting the corner on
a bend, and vertices are re-weighted onto it while the first 22% keeps its
original body weight so the scalp stays on the skull.

On Miku: 19 chains, 57 bones from one `hair` slot. Sidecar 0 -> 19 chains.
Idle stability 0.007-0.018 deg/frame. Mesh intact, verified by render.

Opt-in, via `pipeline.py --grow-cloth`, and run before the retarget so
describe_rig() finds the chains by name exactly as it would an artist's.

Known limits, recorded in the skill: it cannot find a garment sharing a
material with the body (Miku's skirt is on her `body` slot, so she got
hair and no skirt), and grown chains are a fallback — an artist's chains
carry intent that no geometric fit recovers.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-26 13:19:44 -04:00
co-authored by Claude Opus 5
parent 4559a1adc3
commit 7892669319
9 changed files with 9533 additions and 136 deletions
@@ -0,0 +1,76 @@
# Growing cloth bones on a model that has none
`tools/cloth_bones.py`, opt-in via `pipeline.py --grow-cloth`.
## When you need it
The spring solver simulates cloth **bones**. A garment with none is welded to
whatever body bone it was weighted to, and no runtime setting changes that. Every
auto-rigged model is in this state, and so is many a "rigged" download whose
skeleton is body-only.
Check before importing (no Blender needed — see `separation.md`): if the joint
list has nothing matching `hair|skirt|tail|ribbon`, the costume will not move.
## What it does
Runs on the **rigged** model, before the retarget.
1. **Finds the geometry by material slot.** A slot called `hair` is hair. The
artist already answered the question, and on a joined mesh — which is what the
auto-rig leaves behind — the material slot is the only separation left.
2. **Splits it into clumps.**
- *Hair*: connected islands over the mesh's own edges. A strand is a connected
piece of surface; clustering by position would merge two ponytails passing
near each other and split one that bends.
- *Skirt*: radial wedges around the body's up axis. A skirt is ONE connected
surface, so islands would return the whole thing as a single piece — which
is the bell-shaped failure. Wedges are the ZZZ-convention panel grid.
3. **Fits a polyline down each clump** by binning vertices by distance from the
anchored end and taking each bin's centroid, so the chain follows the piece's
own curve. A straight root-to-tip line cuts the corner on a bent ponytail and
every vertex on the outside of that bend ends up on a bone travelling the
wrong way.
4. **Builds a bone chain along the polyline**, parented to the body bone that was
already holding that geometry.
5. **Re-weights** each vertex onto the two bones either side of where it projects,
blended by how far between them it lands — while keeping the ORIGINAL body
weight over the first `ROOT_BLEND` (22%) of the chain, so the scalp stays on
the skull and the waistband stays on the hips.
Then `retarget.py::describe_rig` finds the chains by name exactly as it would an
artist's, and writes tips, hulls and neighbours to the sidecar.
## Tuning
```
--hair-segments 3 bones per hair strand
--skirt-segments 4 bones per skirt panel
--skirt-panels 12 radial panels; more = opens around a leg more smoothly
--classes hair,skirt which material names to look for
```
`MIN_STRAND_LENGTH` (60 mm) and `MIN_STRAND_VERTS` (12) drop fringe and
ornaments. Simulating those costs the same as a ponytail and only ever produces
jitter around the face.
## Worked result: Miku
```
19 chains, 57 bones grown from one `hair` material slot
sidecar: 0 cloth chains -> 19
idle stability: 0.007-0.018 deg/frame (quiet)
mesh intact, no tearing (debug/character_look_capture.gd)
```
## Limits
- **It cannot find a garment that shares a material with the body.** Miku's skirt
is on her `body` slot, so she got hair chains and no skirt. Splitting by
geometry rather than by material would be the next step.
- **It does not set `weights_authored`.** An auto-rigged model still gets the
destructive load-time `SkinLegRepair`. That repair only touches cross-leg
vertices, so it leaves hair alone, but it is worth knowing.
- **Grown chains are a fallback, not a substitute for a rigged source.** They
follow the geometry, but an artist's chains carry intent — where a panel should
split, which strands move together — that no fit recovers.