I retargeted free CMU motion capture onto a humanoid character in Godot 4 with SkeletonProfileHumanoid. Most of the time I lost was not on the animation itself. It went on results that looked like a bad clip or a bad rig and turned out to be a setting, a facing, or a measurement that read the wrong pose. These are the five that cost the most, each with what I saw, what caused it and what fixed it.
1. Wide legs: the source's rest pose is not its stance
The CMU skeleton rests in a wide straddle, with its ankles 0.818 m apart. In the clip itself the actor stands with the ankles 0.268 m apart. I first read the rest and called it "the source's stance", which made a correct transfer look broken.
The rest also poisons the transfer. SkeletonProfileHumanoid measures every motion against the rest pose. The source's legs moved inward relative to their straddled rest, so the target's legs moved by the same amount the other way and came out wide.
The fix belongs on the source import, not the target: retarget/rest_fixer/fix_silhouette/enable = true on the mocap file. It straightened the source rest while leaving the clip's world pose alone (the ankle gap in the take stayed 0.268 m). On the target the same option did harm: it moved the target's rest 32 cm away from its bind pose.
If you also straighten the legs in your own export script, turn that off. With both on, the stance came out wrong again.
2. Inverted arms: overwrite_axis and a rig that faces −Z
My rig was built facing −Z, so its left side is on −X. The humanoid profile and the mocap face +Z with the left on +X. Both rigs were named correctly; the problem is that overwrite_axis forces the profile's bone axes onto the target. On the legs and spine that is harmless, because those bones point along Y. On the arms, the only bones that point along X, the forced axis ran against the geometry, and every arm rotation came out inverted: the hands ended far above the hips in poses where the source has them below.
Turning the character to face +Z removed the conflict. To check facing and side, I put markers in the frame (a sphere at +X, one at +Z and a box on the bone named LeftHand) instead of deducing the side from a render. Two of my three guesses from renders alone were wrong.
3. Measurements that read the rest pose in a headless run
I measured legs and angles from a script run with godot --headless -s script.gd. Skeleton3D.get_bone_global_pose() returned the rest pose on every frame, even after force_update_all_bone_transforms() and with the AnimationPlayer playing. The animation was applied: get_bone_pose_rotation() on the same bone changed frame to frame. Only the global composition was stale. The numbers looked plausible, so I drew several wrong conclusions from them.
Composing the chain by hand gives the right answer:
func chain(n: String) -> Transform3D: var t := Transform3D.IDENTITY var b := sk.find_bone(n) while b >= 0: t = sk.get_bone_pose(b) * t b = sk.get_bone_parent(b) return t In a run that renders, the API and this function agree to three decimals.
4. 170 degrees of twist on a clip that looks fine
A twist audit built on get_bone_pose_rotation() reported 160 to 180 degrees of twist on almost every bone of clips that looked perfectly normal. That call returns the pose including the bone's rest orientation. Take the delta from the rest instead:
var rest_q := sk.get_bone_rest(bi).basis.get_rotation_quaternion().normalized() var delta := (rest_q.inverse() * sk.get_bone_pose_rotation(bi).normalized()).normalized() With that change the same audit read about 55 degrees on the upper arms of a lively take and 8 to 14 in a calm one, which is what the eye sees.
5. A perfect T-pose at rest and one arm straight up in every pose
This one starts in Blender, before the GLB reaches Godot. I rotated the mesh and the armature with bpy.ops.object.transform_apply(rotation=True), one after the other. Only the armature rotated. The skin's bind matrices absorbed the difference, so the exported GLB looked perfect at rest and threw one arm straight up in every animated pose.
Rotating the data instead keeps the two bound together:
body.data.transform(mat) rig.data.transform(mat) Rest renders cannot show this, so check it with the same markers as in trap 2, in an animated pose.
Checks that would have saved me the time
- Fix Silhouette on the mocap source only, and the source's stance measured inside the take, never from its rest.
- Facing and left/right verified with markers in Godot before any retarget setting is tuned.
- Bone measurements in headless scripts composed by hand from
get_bone_pose(), and rotations compared against the rest. - A measurement that stays exactly the same across a parameter change means the change never reached the export. In my case a flag had switched the bones to quaternion rotation, and a later smoothing step kept editing the now inert euler channels.
Judge clips in the engine, from frames captured in Godot, rather than from Blender's clay renders. Six hand-authored versions were rejected in a row while Blender's workbench renders of them looked passable; the first honest look came from frames captured in Godot. Bone world positions in the two agreed to the millimetre when I checked, so a disagreement between the two views is a misreading, not an import bug.

