Files
Nicholas ButzkeandClaude Opus 5 2ce2175d99 docs(skill): record what four non-library rigs taught the role resolver
Names lie and anatomy does not — the four VRoid imports each broke role
resolution differently, and the lesson generalises: anything guessing
anatomy from a bone name needs a structural fallback. Also lists what is
shipping and which characters have chains that do not simulate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 13:46:23 -04:00

128 lines
5.6 KiB
Markdown

# 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.
### Names lie. Anatomy does not.
Four sources that were not authored against the library's spelling each broke
role resolution in a different way. All four fixes are in; the lesson is that
**anything guessing anatomy from a name needs a structural fallback.**
- A bare `leg` is the SHIN on Mixamo (`LeftUpLeg` is the thigh) and the THIGH on
a rig whose shin is called `knee`. Same token, opposite bones, both common. So
`RigRoles` walks the leg upward from the foot and fills in whatever the names
could not, stepping over twist bones.
- Claims are granted **longest-stem first**. Taking the first role in
`LIMB_ORDER` that matched at all let shin's catch-all `"leg"` beat thigh's
exact `"upperleg"`, and the outcome depended on bone iteration order.
- Cosmetic and spring classes accept a **two-character positional suffix**:
`HairFL`, `HairFR`, `HairF_Top` tokenise to `hairfl` and matched nothing, so a
character imported with no hair chains at all. Two characters is short enough
that `forearm` and `earring` are still not swept in.
- VRoid spells legs `UpperLeg`/`LowerLeg`. Any CHECK that name-matches
`thigh`/`shin` will silently pass or silently fail on it — see
`verification.md`.
If a new source fails with `could not identify these bones`, dump the joint names
first (`separation.md` has a no-Blender snippet) and decide whether it is a
missing stem or a case only anatomy can settle.
## 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.