the symptom, in your words

"Retargeting free Mixamo mocap onto a custom rig"

✓ verified on 26.07.08e
lane Rigging & animationposted updated

▸ SYMPTOM

  • Mixamo clip on your custom rig looks hunched forward in idle (spine/chest/neck stacked on top of hips yaw).
  • Walk/run stride measures absurd (e.g. ~70 m/s) after you "fixed" scale.
  • Ambient idles pop at the loop seam; locomotion clips almost loop but double a frame.
  • Retarget script wipes your armature mid-build because Mixamo import resets the scene.

▸ CAUSE

Mixamo and your rig have different rest poses. Copying world rotations double-counts every ancestor. The fix is rest-delta + parent-relative transfer.

Classic forward-hunch: feeding the Poser a raw world delta means Hips yaw is applied again into Spine/Chest/Neck/Head on top of the pelvis's own motion.

Scale trap: Mixamo imports at 0.01 armature-object scale. matrix_world already folds that in (pose worlds come out in meters). transform_apply(scale=True) leaves LOCATION fcurves in the pre-apply unit → Hips translation blows up ~100×. Rotations are unaffected: only root-motion / hip-bob translation.

▸ FIX

Rest-delta + parent-relative

Per source bone at frame f:

snippet
dq_src = M_anim @ M_rest^-1          # world rotation away from THAT skeleton's rest
dq_src = F @ dq_src @ F^-1           # reframe Mixamo world → your armature world
Q      = dq_parent^-1 @ dq_bone      # PARENT-RELATIVE - not the raw world delta

Frame quat F for the common facing mismatch: Mixamo faces −Y / left +X, your rig faces +X / left +Y → F = Rz(+90). If your rig's facing differs, measure your own mapping once and substitute it.

Feed Q into your keyer as a world-delta-from-rest (or a key_world_q helper that keeps quaternion hemisphere continuity: negate if dot(prev, new) < 0).

Do not feed raw world deltas into a poser that composes bone_world = dq_parent @ Q @ rest_world: that is the hunch.

Scale

snippet
# Leave Mixamo armature object scale ON
# Read everything through matrix_world - never transform_apply(scale=True)

Loop / root motion

Clip typeLoopRoot motion
Locomotion (walk/run)Mixamo usually loops (frame 1 == N), drop the duplicate seam frameStrip Hips world XY (runtime moves the char); keep vertical bob × our_hip_z / mixamo_hip_z
Ambient idlePick lowest-seam sub-window (~5–8°/bone residual is OK)Keep full XYZ scaled, window-relative for sit/collapse
Soft seamLet vmdl fade_in/fade_out + Sequence.Blending hide residual-

Depicted speed handoff (don't re-time the clip):

snippet
depicted_mps = Mixamo_XY_travel * hip_scale / window_seconds

Use that const for PlaybackRate = actualSpeed / depicted_mps.

Build order

Preload / cache Mixamo quats BEFORE building your rig. Importing a take often read_factory_settings and wipes the scene; cached mathutils quats survive. Pattern: preload_all() at the top of each rig script, then build.

Export with fbx-export-recipe.

▸ WHY IT WORKS

Rest-delta cancels incompatible bind poses: you transfer motion away from rest, not absolute orientations. Parent-relative deltas stop ancestor rotations from being applied twice down the chain. Leaving Mixamo's object scale alone keeps matrix_world and LOCATION fcurves in the same unit, so hip travel stays meters instead of centimetres×100.

Verified on engine 26.07.08e: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Verified against source: removed stale needs-verification hedge on the Rz(+90) frame quat.

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord