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

5.6 KiB

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

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.