Python Scripting in MotionBuilder

05 – FBTime and Playback Control

Part Five describes the utility classes related to time and animation playback, including FBTime, FBTimeSpan, and FBPlayerControl.

Watch on YouTube | Watch in a full window

Key Points


Code

Access and modify the internal value of an FBTime

# Multiply our time by 3.5
ticks = t.Get()
t.Set(long(ticks * 3.5))

Convert an FBTime to more standard formats

print t.GetSecondDouble() # 3.5 sec
print t.GetMilliSeconds() # 3500 ms
print t.GetFrame(True)    # 105 at 30 fps, 210 at 60 fps, etc.

Convert an FBTime to frame numbers with an explicit framerate

# In MotionBuilder 2013, the first parameter is omitted (see note below)
print t.GetFrame(True, FBTimeMode.kFBTimeMode30Frames)     # 105
print t.GetFrame(True, FBTimeMode.kFBTimeModeNTSC_Drop)    # 104
print t.GetFrame(True, FBTimeMode.kFBTimeMode60Frames)     # 210
print t.GetFrame(True, FBTimeMode.kFBTimeModeCustom, 48.0) # 168

Initialize an FBTime from a frame number, with an explicit framerate

t = FBTime(0, 0, 0, 30, 0, FBTimeMode.kFBTimeMode30Frames) 
print t.GetSecondDouble() # 30 frames at 30 fps: 1.0 sec

t = FBTime(0, 0, 0, 30, 0, FBTimeMode.kFBTimeMode60Frames)
print t.GetSecondDouble() # 30 frames at 60 fps: 0.5 sec

t = FBTime(0, 0, 0, 30, 0, FBTimeMode.kFBTimeModeCustom, 48.0)
print t.GetSecondDouble() # 30 frames at 48 fps: 0.625 sec

Get and print the current playback position (local time)

localTime = FBSystem().LocalTime
print '%s\n      (%f sec)\n      (%d frames)' % (
    localTime,
    localTime.GetSecondDouble(),
    localTime.GetFrame(True)
)

Set the playback position to a specific frame number

# Scrub to frame 44
t = FBTime(0, 0, 0, 44, 0)
FBPlayerControl().Goto(t)

Set the playback position to the start and end of the take

FBPlayerControl().GotoStart()
FBPlayerControl().GotoEnd()

Toggle playback of the current take

playback = FBPlayerControl()
playback.Stop() if playback.IsPlaying else playback.Play()

Get the current playback framerate

print FBPlayerControl().GetTransportFps() # Returns the FBTimeMode value
print FBPlayerControl().GetTransportFpsValue() # Returns the actual FPS value

Set the playback framerate

# Set the playback framerate to 60 fps
FBPlayerControl().SetTransportFps(FBTimeMode.kFBTimeMode60Frames)

Print the frame range of the current take

print '%d -- %d' % (
    FBSystem().CurrentTake.LocalTimeSpan.GetStart().GetFrame(True),
    FBSystem().CurrentTake.LocalTimeSpan.GetStop().GetFrame(True)
)

Set the frame range of the current take

# Change the current take to go from frame 2000 to frame 5000
FBSystem().CurrentTake.LocalTimeSpan = FBTimeSpan(
    FBTime(0, 0, 0, 2000, 0),
    FBTime(0, 0, 0, 5000, 0)
)

Notes & Errata

GetFrame and MotionBuilder 2013

At 1:15, I demonstrate how to use the GetFrame method. In MotionBuilder 2012, it has three arguments:

Here's how those arguments work, assuming that our scene framerate is set to 30 fps:

t = FBTime(0, 0, 2, 15, 0) # Two and a half seconds

print t.GetFrame(False)                                    # 15
print t.GetFrame(True)                                     # 75
print t.GetFrame(True, FBTimeMode.kFBTimeMode60Frames)     # 150
print t.GetFrame(True, FBTimeMode.kFBTimeModeCustom, 15.0) # 37

In the 2013 version of the SDK, however, the first parameter (pCummul) has been removed outright, and GetFrame always returns a cummulative frame number. So we'd have to modify our code like so to work with MotionBuilder 2013:

t = FBTime(0, 0, 2, 15, 0) # Two and a half seconds

#     t.GetFrame(False) is no longer supported
print t.GetFrame()                                          # 75
print t.GetFrame(FBTimeMode.kFBTimeMode60Frames)            # 150
print t.GetFrame(FBTimeMode.kFBTimeModeCustom, 15.0)        # 37

Of course, this means that FBTime.GetFrame is mutually incompatible between MotionBuilder 2012 and 2013. If you were really interested in maintaining compatibility between both versions, you'd have to use some ugly piece of glue code like this:

def IsVersion2013():
    import pyfbsdk
    return hasattr(pyfbsdk, 'FBHUD')
    
def GetFrame(time, timeMode = FBTimeMode.kFBTimeModeDefault,
                   customFramerate = 0.0):
    if IsVersion2013():
        return time.GetFrame(timeMode, customFramerate)
    return time.GetFrame(True, timeMode, customFramerate)

t = FBTime(0, 0, 2, 15, 0)
print GetFrame(t)                                     # 75
print GetFrame(t, FBTimeMode.kFBTimeMode60Frames)     # 150
print GetFrame(t, FBTimeMode.kFBTimeModeCustom, 15.0) # 37


Types of Time

There are three kinds of time value that MotionBuilder refers to. Local time (FBSystem().LocalTime) is the playback position within the current take. System time (FBSystem().SystemTime) is the wall-clock time given by the system, which steadily advances regardless of what MotionBuilder does.

The third kind of time, reference time, is used for independent timeframes that are synchronized to the take. For example, if you had an LTC device writing timecode data into the take, then takes with recorded data would have a reference timecode associated with them. To get the current reference time, then, you could use the FBReferenceTime class:

refTime = FBReferenceTime()
t = refTime.GetTime(refTime.ItemIndex, FBSystem().SystemTime)
print t.GetSecondDouble()


Goto and Scene Evaluation

When you change the local time in a script, it's important to realize that the scene is not immediately reevaluated at the new time. If you access an animated property after setting the playback time, you'll still get the value at the old time. The solution is to call FBScene.Evaluate after setting the time, causing an explicit update of the scene.

To help understand this issue, try running the following code in an empty scene. It simply creates a cube and animates its translation over the course of the take.

# Create a cube and get the FCurve for its translation in X
cube = FBModelCube('Cube')
cube.Show = True
cube.Translation.SetAnimated(True)
fcurve = cube.Translation.GetAnimationNode().Nodes[0].FCurve

# Get the start and end time of the current take
startTime = FBSystem().CurrentTake.LocalTimeSpan.GetStart()
stopTime = FBSystem().CurrentTake.LocalTimeSpan.GetStop()

# Key the cube at x translation 0 at the start of the take
# Key the cube at x translation 100 at the end of the take
fcurve.KeyAdd(startTime, 0.0)
fcurve.KeyAdd(stopTime, 100.0)

Now, if we manually scrub the timeline and then print cube.Translation, we get the translation of the cube at that point in time. However, try setting the playback time and accessing the translation from the same script:

FBPlayerControl().GotoStart()
print cube.Translation

FBPlayerControl().GotoEnd()
print cube.Translation

You'll notice that the values you get vary, and that they're generally incorrect. The problem is that MotionBuilder doesn't respond to the change in local time until the next frame. Because Python scripts are executed in a single, blocking thread, this doesn't occur until after the entire script is finished running.

Therefore, if we modify the current time in a script, and we then want to access the current value of any animated property, we need to explicitly reevaluate the scene. In this case, that means adding calls to FBScene.Evaluate:

FBPlayerControl().GotoStart()
FBSystem().Scene.Evaluate()
print cube.Translation

FBPlayerControl().GotoEnd()
FBSystem().Scene.Evaluate()
print cube.Translation

Note for future reference that there are other ways of accessing animated properties at arbitrary times without having the change the current time and reevaluate the entire scene. We'll get into that in the next couple of chapters.


Corrections?

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