Everything about getting an anime-styled character into the game — import, stylization, the body/garment/hair separation, rigging, retargeting, cloth and hair physics, and how to measure any of it — collected into .claude/skills/character-pipeline/. Organised around the principle the Hoyoverse-class pipelines are built on and that every failure in this project traced back to: a character is not one object. It is a body, a set of garments and hair, authored and rigged separately and moved by different systems. The body is skinned and animated; the garments and hair are bone chains the animation never touches and physics moves. The skill's non-negotiables are the four ways that separation has been destroyed here before — joining meshes, keying cosmetic bones, skinning cloth to a leg, and running the auto-rig repair on authored weights. references/verification.md leads with the trap that invalidated every cloth measurement ever taken in this repo: Godot restores bone poses after the modifier pass, so a tool that reads them afterwards measures the animation and never sees what any modifier did. Also records what is known-unsolved, with numbers: peak cloth clipping in a run/slide/dash, no foot IK, no strafe clips. Co-Authored-By: Claude Opus 5 <[email protected]>
110 lines
5.2 KiB
Markdown
110 lines
5.2 KiB
Markdown
# 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 2–4 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.
|
||
|
||
## 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.
|