Files
Papay-Shooter/.claude/skills/character-pipeline/references/separation.md
T
Nicholas ButzkeandClaude Opus 5 daf9627ece docs(skill): compact the character pipeline into a reusable skill
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]>
2026-07-26 12:46:50 -04:00

5.2 KiB
Raw Blame History

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:

{ "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

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.