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]>
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
legis the SHIN on Mixamo (LeftUpLegis the thigh) and the THIGH on a rig whose shin is calledknee. Same token, opposite bones, both common. SoRigRoleswalks 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_ORDERthat 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_Toptokenise tohairfland matched nothing, so a character imported with no hair chains at all. Two characters is short enough thatforearmandearringare still not swept in. - VRoid spells legs
UpperLeg/LowerLeg. Any CHECK that name-matchesthigh/shinwill silently pass or silently fail on it — seeverification.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.