Files
Papay-Shooter/.claude/skills/character-pipeline/references/separation.md
T
Nicholas ButzkeandClaude Opus 5 4559a1adc3 docs(skill): record why Miku has no cloth — the source had no skeleton
The two shipped characters are a controlled comparison: Taila's source
arrived rigged (35 cloth chains, 8 twist bones, 18 meshes, artist
weights), Miku's did not (5 meshes, 0 joints), so Miku was auto-rigged
into one mesh with nearest-bone weights and no cloth chains at all. Her
twin tails and skirt are dead geometry and the destructive load-time
weight repair runs on her every spawn.

None of that is recoverable downstream, which makes picking a source that
already has skirt and hair bones the highest-leverage decision in the
pipeline. Added the no-Blender check for vetting a candidate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 12:47:42 -04:00

146 lines
6.6 KiB
Markdown
Raw 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.
# Body, garments, hair — what must stay separate
The single structural idea behind an anime-styled character rig, and the thing
every failure in this project traced back to.
## The convention this pipeline follows
Hoyoverse-class character rigs (Genshin, Star Rail, Zenless Zone Zero) are built
the same way, and the parts that matter are visible in any of their exported
assets and in the toolchains built around them (Magica Cloth 2, UnityChan
SpringBone, VRM's spring-bone spec — all of which exist because this shape is
the convention):
| Convention | What this repo does |
|---|---|
| Body, face, hair and each garment are SEPARATE meshes with separate materials | Never join meshes; 18 meshes on Taila are all kept |
| Skirts get a radial grid of bone chains — many panels, several segments each | 21 panels × 4 segments, subdivided at build time |
| Hair is chains of 24 bones from the scalp | Detected from the source rig; 14 chains on Taila |
| Cloth/hair bones carry NO animation keys; physics owns them | `export_optimize_animation_keep_anim_armature=False` |
| Physics colliders are a small set of capsules: thighs, shins, and a big one at the waist acting as a lid | 5 capsules, measured from the mesh (`_leg_colliders`) |
| Neighbouring skirt panels are linked sideways | 278 cross-panel distance links from shared vertices |
| Cel shading with a ramp, plus a separate outline pass | `LevelMaterials.apply_toon_recursive` + `apply_character_look` |
Where we differ: their collider capsules and cloth parameters are hand-authored
per character by a technical artist. We MEASURE them from the model's own
geometry at build time, because there is no artist in this loop. That is the
whole reason `<model>.rig.json` exists.
## Why the separation is load-bearing
**Materials.** The body wants skin shading, hair wants an anisotropic-ish ramp
and its own outline weight, cloth wants flat banding. One merged mesh gets one
treatment and everything reads as plastic.
**The cloth solver.** `SkinnedPlayerModel._cloth_hulls` extracts, per cloth bone,
the vertices that bone dominates — that is only meaningful while the garment is
its own mesh with its own weights. Merge the meshes and the solver has no way to
know which vertices are skirt.
**Weights.** A joined mesh rebound by nearest-bone weighting produced 2817
vertices pulled by BOTH legs on Taila (16% of the model, worst a dead 50/50).
Such a vertex sits between the legs and stays there while they separate,
stretching every triangle around it. That is the "squashing on jump" and the
"elongated boot".
## How cloth is detected and classed
`tools/rig_map.py::is_cosmetic` matches WHOLE TOKENS in a bone name against:
```
hair skirt cloth ribbon tail cape coat scarf sleeve breast bust
feather strap antenna wing (+ face/eye classes that must never swing)
```
Whole-token only — `shoulder` must not match `should`, and a bone called
`hair_root` is hair while `chairbone` is not.
`retarget.py::SPRING_CLASSES` is a NARROWER set: the classes that actually get
secondary motion. A face-shape or eye chain is cosmetic but must never swing.
Each chain lands in `<model>.rig.json` as:
```json
{ "class": "skirt",
"root_parent": "DEF-spine.001",
"bones": ["DEF-skirt", "DEF-skirt.seg1", "DEF-skirt.seg2", "DEF-skirt.seg3"],
"tips": [[x,y,z], ...], // where each bone points, in its own space
"hulls": [[[x,y,z], ...], ...], // sample of the geometry it drives
"neighbours": [{"DEF-skirt.L": 10.7, ...}] // shared-vertex weight
}
```
`tips` exists because **a glTF skeleton carries no bone tails at all**, and
Taila's skirt panel bones have no children either, so nothing in the skeleton
says which way a panel hangs. It is measured from the geometry the bone drives.
`neighbours` means SHARED VERTICES — the artist's own answer to which pieces of
cloth are sewn together. Adjacency by name or by rest distance would both be
guesses.
## The three rules that keep it intact
1. **Cloth may only ever parent to the trunk, never to a limb.**
`rebuild_hierarchy` enforces this. A skirt parented to a thigh becomes
trousers.
2. **Cloth is never SKINNED to a leg.** There was a `bind_cloth_to_legs()` that
gave cloth vertices near a thigh a share of that thigh, so the skirt would
ride the leg the way a real one does. It is deleted. A vertex weighted 0.9 to
a thigh cannot be moved by its own cloth bone, so the solver loses the
authority to push it out of that leg — and 0.9 of a rotation always lags the
surface doing 1.0 of it, so the leg overtakes it anyway. It also poisoned the
collider measurement: 2258 skirt vertices counted as thigh geometry and fitted
a 0.28 m thigh.
3. **Cloth bones carry no animation tracks.** If the exporter bakes rest-pose
tracks onto them (`keep_anim_armature`), the AnimationPlayer overwrites the
spring solver every frame.
## Worked example: why the two shipped characters differ so much
Both are in `assets/characters/skins/`. Compare their sidecars:
| | Taila | Miku |
|---|---|---|
| source had a skeleton | yes | **no — 5 meshes, 0 joints** |
| `weights_authored` | true | **false** |
| cloth chains | 35 (127 bones) | **0** |
| twist bones | 8 | **0** |
| meshes shipped | 18 | **1** |
Miku's source (`assets/characters/incoming/miku_test.glb`) is an unrigged mesh,
so she went through `autorig.py`: joined to one mesh, rebound by nearest-bone
weighting, no cloth chains. Her twin tails and skirt are dead geometry that
cannot move, and `SkinLegRepair` runs destructively on her every spawn.
Nothing downstream can recover this. **The single highest-leverage decision in
this whole pipeline is choosing a source model that already has a skeleton with
skirt and hair bones.** Everything else is recoverable; this is not.
A quick check on any candidate, without Blender:
```python
import json, struct
with open(path,'rb') as f:
f.read(12); clen,_=struct.unpack('<II',f.read(8))
j=json.loads(f.read(clen))
nodes=[n.get('name','') for n in j['nodes']]
joints=[nodes[i] for s in j.get('skins',[]) for i in s['joints']]
print(len(j['meshes']), 'meshes', len(joints), 'joints')
print([n for n in joints if any(t in n.lower() for t in ('hair','skirt','tail','ribbon'))])
```
Several meshes, 50+ joints, and a non-empty cosmetic list means a good source.
## Checking a source model before importing
```bash
python tools/verify_character.py <model.glb>
```
What you want to see: several meshes, bone names containing `skirt`/`hair`,
twist bones (`thigh.L.001`), and weights that are NOT all at 4 influences.
`weights_authored` in the sidecar is measured from exactly this and decides
whether the destructive load-time repair runs.