Automating a Character to Origin

App Homepage
App Homepage
App Homepage

About the post

A production ready python script for Motionbuilder

Date

August 11, 2024

Building Motion Sets are a lot of repetitive work, the more I build sets the more I try to build tools to reduce the main repetitive tasks. I wrote a nice script for Motionbuilder’s Python to automate creating a Character Track, adding a clip to the track, calculating the transform offset required to align the character to the scene’s origin (center of scene) and then plot it back to the character. This is a lot of clicks over a large motion set – it can save hours of work.

Let’s have a look at the script:

from pyfbsdk import *
import time
'''
Batch Center All Takes
Send take to Story (make sure that it selects entire control rig hierarchy)
Calculates position of the offset required
Applies new position
Plots back to Control Rig on Same Take
Thanks to Vic Debaie for the refresh bug workaround
'''
# Gets the FK effectors for a given character.
def GetControlRigFKEffectors(character=None):
    if not character:
        character = FBApplication().CurrentCharacter
    fkEffectors = []
    if character:
        for nodeId in FBBodyNodeId.values.values():
            if nodeId not in [FBBodyNodeId.kFBInvalidNodeId, FBBodyNodeId.kFBLastNodeId]:
                effector = character.GetCtrlRigModel(nodeId)
                if effector:
                    fkEffectors.append(effector)
        if not fkEffectors:
            fkEffectors = None
    return fkEffectors
# Gets the IK effectors for a given character.
def GetControlRigIKEffectors(character=None):
    if not character:
        character = FBApplication().CurrentCharacter
    ikEffectors = []
    if character:
        ctrlRig = character.GetCurrentControlSet()
        if ctrlRig:
            for nodeId in FBEffectorId.values.values():
                if nodeId not in [FBEffectorId.kFBInvalidEffectorId, FBEffectorId.kFBLastEffectorId]:
                    effector = ctrlRig.GetIKEffectorModel(nodeId, 0)
                    if effector:
                        ikEffectors.append(effector)
            if not ikEffectors:
                ikEffectors = None
    return ikEffectors
# Gets both the FK and IK effectors for a given character.
def GetControlRigEffectors(character=None):
    if not character:
        character = FBApplication().CurrentCharacter
    if character:
        controlRigEffectors = GetControlRigFKEffectors(character) + GetControlRigIKEffectors(character)
        return controlRigEffectors
# Gets a control rig effector by name.
def GetEffectorByName(effectorName, character=None):
    effectors = GetControlRigEffectors(character)
    effectorToReturn = None
    for effector in effectors:
        if effector and effector.Name == effectorName:
            effectorToReturn = effector
            break
    return effectorToReturn
# Example usage to find and print the global location of the hips effector
def printHipsEffectorGlobalLocation(character=None):
    hips_effector = GetEffectorByName("HipsEffector", character)
    if not hips_effector:
        print("Hips effector not found.")
        return
    hips_global_translation = hips_effector.Translation
    print(f"Hips Effector Global Location: {hips_global_translation[0]}, {hips_global_translation[1]}, {hips_global_translation[2]}")
    print(clip.Translation)
def getHipsEffectorTranslation(character=None):
    hips_effector = GetEffectorByName("HipsEffector", character)
    if not hips_effector:
        print("Hips effector not found.")
        return
    return hips_effector.Translation
def getHipsEffector(character=None):
    hips_effector = GetEffectorByName("HipsEffector", character)
    if not hips_effector:
        print("Hips effector not found.")
        return
    return hips_effector
## Scene Refresh Bug Workaround
def SceneRefresh():
    FBPlayerControl().GotoNextKey()
    FBSystem().Scene.Evaluate()
    FBPlayerControl().GotoPreviousKey()
    FBSystem().Scene.Evaluate()
def plotCharacter():
    ## Deal With The User's Story Mode Activity
    FBStory().Mute = False
    SceneRefresh()
    ## Plot Options
    lPlotClipOptions = FBPlotOptions()
    lPlotClipOptions.ConstantKeyReducerKeepOneKey = False
    lPlotClipOptions.PlotAllTakes = False
    lPlotClipOptions.PlotOnFrame = True
    lPlotClipOptions.PlotPeriod = FBTime( 0, 0, 0, 1 )
    lPlotClipOptions.PlotTranslationOnRootOnly = False
    lPlotClipOptions.PreciseTimeDiscontinuities = False
    lPlotClipOptions.RotationFilterToApply = FBRotationFilter.kFBRotationFilterUnroll
    lPlotClipOptions.UseConstantKeyReducer = False
    ## Plot Story Clip On Current Character
    lChar = FBApplication().CurrentCharacter
    print("Plotting Character " + lChar.Name)
    lChar.PlotAnimation(FBCharacterPlotWhere.kFBCharacterPlotOnControlRig,lPlotClipOptions )
## Function to select all models in the scene
def selectAllModels():
    for component in FBSystem().Scene.Components:
        if isinstance(component, FBModel):
            component.Selected = True
## Function to deselect all models in the scene
def deselectAllModels():
    for component in FBSystem().Scene.Components:
        if isinstance(component, FBModel):
            component.Selected = False
def enableAllGhosts(lTrack):
    lTrack.Ghost = True
    lTrack.GhostModel = True
    lTrack.GhostPivot = True
    lTrack.GhostTravelling = True
def processTake():
    take = FBSystem().CurrentTake
    print(f"Processing take: {take.Name}")
    character = FBApplication().CurrentCharacter
    ## Create A Character Animation Track Within The Root Folder Of Story
    lTrack = FBStoryTrack(FBStoryTrackType.kFBStoryTrackCharacter, FBStory().RootFolder)
    lTrack.Details.append(character)
    # Insert current take in the newly created track
    clip = lTrack.CopyTakeIntoTrack(FBSystem().CurrentTake.LocalTimeSpan, FBSystem().CurrentTake)
    # Enable all ghost-related properties for the track
    enableAllGhosts(lTrack)
    FBStory().Mute = False
    # Enable keying mode
    lTrack.AcceptKey = True
    lTrack.OffsetEnable = True
    clip.Selected = True
    # Set the playbar time to 0
    player_control = FBPlayerControl()
    player_control.GotoStart()
    original_translation = FBVector3d(clip.Translation)
    original_rotation = FBVector3d(clip.Rotation)
    hipsEffector = getHipsEffector(character)
    hips_translation = hipsEffector.Translation
    x = original_translation[0]
    z = original_translation[2]
    print(f"Original Translation: {original_translation[0]}, {original_translation[1]}, {original_translation[2]}")
    print(f"x: {x}")
    print(f"z: {z}")
    #we need to clear offset if it's present on x or z (y not really used but can be added)
    if original_translation[0] != 0:
        x = 0.00
    if original_translation[2] != 0:
        z = 0.00
    print(original_translation)
    print(f"x after: {x}")
    print(f"z after: {z}")
    clip.Translation = FBVector3d(
        x,
        0,
        z
    )
    SceneRefresh()
    clip.Translation = FBVector3d(
        -hips_translation[0],
        0,
        -hips_translation[2]
    )
    # Select everything in the scene
    selectAllModels()
    clip.Selected = True
    #plot once - to get value onto offset?
    plotCharacter()
    print(f"Hips Translation: {hips_translation[0]}, {hips_translation[1]}, {hips_translation[2]}")
    deselectAllModels()
    # Clean up by removing the track after plotting
    FBStory().RootFolder.Tracks.remove(lTrack)
    print(f"Completed take: {take.Name}")
processTake()


I'd like to thank Vic DeBaie as well for some helpful utility script snippets to help jiggle the scene as well, otherwise repeating this over many takes the scene takes start to breakdown and stop behaving properly. The jiggle works quite well as a quick refresh.

To use this script, open the Window > Python Editor and in the bottom panel paste this script in. Now click the Green play button.