wrote a MotionBuilder script to auto-fix world orientation on batches of takes — was doing this by hand every session

462 views 1 reply

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?

thought this was specific to our capture setup for way too long. nope — apparently every volume works this way and nobody tells you until you're 40 takes deep with a different root orientation in each one.

quick question though: how are you handling takes where the actor starts off-center? ours sometimes captures with the root 2–3 feet from the volume origin, and a world rotation fix applied on top compounds the positional offset in weird ways. we've been correcting position and rotation separately as a two-pass thing but it feels like a bodge.

Moonjump
Forum Search Shader Sandbox
Sign In Register