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]>
This commit is contained in:
Nicholas Butzke
2026-07-26 12:46:50 -04:00
co-authored by Claude Opus 5
parent 040b595397
commit daf9627ece
6 changed files with 661 additions and 0 deletions
@@ -0,0 +1,102 @@
# Rigging and retargeting
## Roles, not names
`tools/rig_map.py` resolves a skeleton to ROLES — `hips`, `spine[]`, `neck`,
`head`, and `limb[(role, side)]` for `thigh/shin/foot/toe/shoulder/upper_arm/
forearm/hand`. Matching is by whole tokens plus anatomy (chain length, position,
which bone is a child of which), so a Rigify `DEF-thigh.L`, a Mixamo
`mixamorig:LeftUpLeg` and a bespoke `Bip01_L_Thigh` all land on the same role.
This is what removed the need to destroy foreign skeletons. `roles.missing_core()`
is the gate: if the core roles cannot be found the pipeline stops rather than
guessing.
The resolved roles are written to `<model>.rig.json` and read at runtime by
`ShooterPoseModifier._resolve`, which aliases its library-flavoured names
(`DEF-hips`, `DEF-spine.001`…) onto whatever this rig calls them. Taila's hips
are `DEF-spine`, her head is `DEF-spine.006`, and she has **no bone with "neck"
in its name at all** — unresolved, every lean, aim pitch and slide head-lift
silently did nothing.
## Rebuilding the hierarchy
A Rigify DEF-rig exports its chain roots parented straight to the armature root,
because Rigify drives them by constraint rather than by hierarchy. Left that way,
rotating the hips leaves the legs, skirt and hair floating in place.
`rebuild_hierarchy` re-attaches orphans: by anatomy where the role is known, and
by rest geometry (nearest plausible parent) otherwise. **Cloth may only attach to
the trunk.**
## Subdividing cloth panels
`subdivide_cloth_panels(arm, meshes, roles, segments=4)`.
A skirt panel that is a single bone from the waist is a rigid flap: it can only
rotate about its own head, and a contact near that head is unreachable at any
angle. Splitting each panel into a chain is what lets it bend, and it is why the
ZZZ-convention skirt is a grid rather than a fan.
On Taila this turns 21 panel bones into 21 chains of 4. The segment lengths come
out uneven (49/49/49/141 mm) because the last segment runs on to the hem.
Weights are redistributed along the panel as it is split, so the mesh follows the
new chain.
## Twist bones
A forearm or thigh twist bone takes half the roll of its parent so the skin does
not candy-wrap. They are detected (`is_segment_of`) and recorded in the sidecar's
`twist` list. They are also folded into the limb when measuring collider radii:
most of a thigh's surface belongs to `DEF-thigh.L.001`, and what is left
dominated by `DEF-thigh.L` is mostly hip flare, which fitted a 0.154 m radius —
a 30 cm thigh.
## Joint helpers
`SkinJointHelper.install` runs for EVERY model however it was rigged. Linear-blend
skinning collapses any joint by cos(angle/2) no matter how good the weights are;
measured at the knee, 0.77 without helpers against 0.99 with. They are updated
LAST, inside the modification pass, so each helper tracks whatever final rotation
its child bone ended up with.
## The retarget maths
Bake each clip as a **rest-relative delta**:
```
R_world = src_pose_rot * src_rest_rot⁻¹ what the clip does
tgt_rot = R_world * tgt_rest_rot done to THIS rig
```
Copying absolute world orientation instead — which is what a constraint bake does
— forces the library's bone ROLL onto a mesh bound with a different one, and
twists every limb by a constant offset.
Also handled: a facing correction (`facing_correction`) when the library and the
character face different ways, and a hips-height scale so a short character does
not float.
## Export flags that matter
```python
export_bake_animation=False,
export_optimize_animation_keep_anim_armature=False,
```
`keep_anim_armature` forces a track onto every bone whether or not the clip
touches it. Off, the skirt and hair export with **no tracks at all** and belong
entirely to the spring solver. This one flag is the animation/physics split.
## Height normalisation
`flatten_and_scale(arm, meshes, TARGET_HEIGHT)` — default 1.75 m. Applied before
the retarget so the library's stride matches the character's legs.
## When a model has no skeleton
`tools/autorig.py` will fit one, and the pipeline accepts the quality loss:
nearest-bone weights, cross-leg bleed, no cloth chains. `weights_authored` comes
out false, `SkinLegRepair` runs at load to snap the worst of it, and the
character will have no secondary motion. Prefer finding a rigged source.