Familiar problem if you do any volume work: the actor walks into the capture space at whatever angle is convenient, and every take starts with the root bone facing a different direction. In MotionBuilder you end up manually rotating the hips on each take before you can start any real cleanup. Forty takes in a session is forty manual corrections before the actual work begins.
So I wrote a script. It finds the root bone by name, reads the Y rotation at frame 0 for each take, calculates the delta to hit your target facing angle, and applies that offset across every rotation keyframe in the take. Wraparound normalization handles the edge cases so you get a -10 degree correction instead of 350.
import pyfbsdk as fb
ROOT_BONE_NAME = "Hips"
TARGET_FACING_Y = 0.0 # degrees; 0 = facing +Z in MotionBuilder
def get_y_at_frame(anim_node, frame):
y_curve = anim_node.Nodes[1].FCurve
if not y_curve or y_curve.Keys.GetCount() == 0:
return 0.0
return y_curve.Evaluate(fb.FBTime(0, 0, 0, frame))
def offset_y_keys(anim_node, offset):
y_curve = anim_node.Nodes[1].FCurve
if not y_curve:
return
for i in range(y_curve.Keys.GetCount()):
y_curve.Keys[i].Value += offset
def fix_all_takes():
lSystem = fb.FBSystem()
scene = lSystem.Scene
root = scene.Components.Find(ROOT_BONE_NAME, True)
if root is None:
fb.FBMessageBox("Script Error", f"Bone not found: {ROOT_BONE_NAME}", "OK")
return
original_take = lSystem.CurrentTake
fixed = 0
for take in scene.Takes:
lSystem.CurrentTake = take
anim = root.AnimationNode
if anim is None or len(anim.Nodes) < 3:
continue
current_y = get_y_at_frame(anim, 0)
offset = TARGET_FACING_Y - current_y
offset = ((offset + 180) % 360) - 180
if abs(offset) < 1.0:
continue
offset_y_keys(anim, offset)
fixed += 1
lSystem.CurrentTake = original_take
fb.FBMessageBox("Done", f"Corrected {fixed} take(s).", "OK")
fix_all_takes()
A few things to know before running it:
- Assumes the actor is reasonably still at frame 0. Falls apart on takes that start mid-movement.
- Only fixes root Y rotation. Translation drift on the root is a separate problem I haven't solved cleanly.
- If you have an active HIK control rig on the character, bake and remove it first. The script reads FCurve data directly and may not find the keys you expect with a rig active.
The edge case I'm still working on: takes that start mid-locomotion with no clean reference frame at frame 0. I've been thinking about averaging the forward direction across the first second of capture instead of just sampling frame 0, but haven't built that yet. Has anyone tackled this differently, or do those just get a manual pass?