Python Scripting in MotionBuilder

06 – Animation Overview: FBAnimationNode

Part Six is a rundown of the major classes involved in animation which attempts to explain how properties, animation nodes, and FCurves fit together.

Watch on YouTube | Watch in a full window

Key Points


Code

Create a new camera and look through it in the viewport

camera = FBCamera('testCamera')
camera.Show = True
FBSystem().Renderer.CurrentCamera = camera

Determine whether a property can be animated

# Prints True, indicating that Roll is an FBPropertyAnimatable
print camera.Roll.IsAnimatable()

Set two properties to animated and get the newly created animation node for each

# Once a property is set to animated, it has an animation node that we can access
camera.Roll.SetAnimated(True)
rollNode = camera.Roll.GetAnimationNode()
print rollNode.Name, rollNode

camera.Translation.SetAnimated(True)
translationNode = camera.Translation.GetAnimationNode()
print translationNode.Name, translationNode

Access the FCurves at the leaf animation nodes for various property types

# float
print rollNode.FCurve                 # camera.Roll

# FBVector3d
print translationNode.Nodes[0].FCurve # camera.Translation[0] (X)
print translationNode.Nodes[1].FCurve # camera.Translation[1] (Y)
print translationNode.Nodes[2].FCurve # camera.Translation[2] (Z)

Create an empty take by copying the current one

# CopyTake makes the new take current after creating it
newTake = FBSystem().CurrentTake.CopyTake('NewTakeName')
FBPlayerControl().GotoStart()
newTake.ClearAllProperties(False)
newTake.LocalTimeSpan = FBTimeSpan(FBTime(0), FBTime(0, 0, 0, 150))

Create and configure a new animation layer (with the help of some utility functions)

def GetLayerIndex(take, layerVariant):
    '''
    Given a variable representing a layer, returns the index of that layer
    within the given take, or -1 if no such layer exists. layerVariant can
    either be an actual layer object, the name of a layer, or, for the sake of
    convenience, a layer's index.
    '''
    # If the layer variant is already an index, we simply return it
    if isinstance(layerVariant, int):
        return layerVariant

    # Otherwise, we need to qualify it to obtain a name
    if isinstance(layerVariant, basestring):
        name = layerVariant
    else:
        name = layerVariant.Name # It must be an FBAnimationLayer

    # Finally, search for the layer by name
    for i in range(0, take.GetLayerCount()):
        if take.GetLayer(i).Name == name:
            return i
    return -1

def CreateNewLayer(take, name):
    '''
    Creates a new layer in the given take, but conveniently allows an initial
    name to be supplied. Returns the new FBAnimationLayer object.
    '''
    # Puzzlingly, this API method doesn't return the new layer
    take.CreateNewLayer()

    # However, new layers are always created at the top of the stack
    layer = take.GetLayer(take.GetLayerCount() - 1)
    layer.Name = name
    return layer

def SetCurrentLayer(take, layerVariant):
    '''
    Given a variable representing a layer (either an index, a name, or an
    actual layer object), makes that layer current.
    '''
    layerIndex = GetLayerIndex(layerOrName)
    assert layerIndex >= 0

    take.SetCurrentLayer(layerIndex)

take = FBSystem().CurrentTake

# Create a new override layer and make it current
layer = CreateNewLayer(take, 'TestLayer')
layer.LayerMode = FBLayerMode.kFBLayerModeOverride
SetCurrentLayer(take, layer)

Notes & Errata

Input Animation Node

At 2:15, I mention that the animation nodes for each property are contained under a single input animation node. It's not worth getting too hung up on this point, but in case you're curious, there are a couple of things to get straight:

For one, animation nodes are stored separately from their corresponding properties. To use our camera as an example:

# The properties live in camera.PropertyList:
camera.PropertyList.Find('Roll')
# However, they're conveniently accessible via member variables:
camera.Roll

# The animation nodes live beneath camera.AnimationNode:
[n for n in camera.AnimationNode.Nodes if n.Name == 'Roll'][0]
# However, they're conveniently accessible via GetAnimationNode:
camera.Roll.GetAnimationNode()

The first time we set camera.Roll to animated, a new animation node is created and added as a child of camera.AnimationNode. camera.Roll.GetAnimationNode will then give us a reference to that new node. Because of this convenient interface, we don't have to worry too much about the top-level input node if we're only dealing with animated properties.

So why is it the input animation node? Well, recall that all animated objects, FBCamera included, are subclasses of FBBox. A box is a component that has an input animation node and an output animation node: you pipe data in on one side, and you get out some modified data on the other side. If you've ever used a relation constraint, you'll know what I mean. These two nodes are accessible via these two methods of FBBox:

camera.AnimationNodeInGet()
camera.AnimationNodeOutGet()

Naturally, all the property values are inputs, so they're contained under the input animation node. For the sake of convenience, FBModel provides access to this same node via the AnimationNode member variable. So the following two approaches are functionally equivalent:

for node in camera.AnimationNodeInGet().Nodes:
    print node.Name

for node in camera.AnimationNode.Nodes:
    print node.Name

Corrections?

If you see any errors that you'd like to point out, feel free to email me at awforsythe@gmail.com.