Python Scripting in MotionBuilder

08 – Decomposition and Library Code

 

Part Eight describes how to create a library to contain common utility functions, and why we'd want to have such a thing in the first place.

Watch on YouTube | Watch in a full window

Key Points


Code

Create a group containing the selected models, brutishly

# Create a group from the selected models
group = FBGroup('InitialSelection')
selectedModels = FBModelList()
FBGetSelectedModels(selectedModels)
for model in selectedModels:
    group.Items.append(model)

somemodule (somemodule/__init__.py)

'''
somemodule
A module containing utility functions for MotionBuilder.
If you make your own, please give it an actual name.
'''

import group as Group; reload(Group)
import selection as Selection; reload(Selection)

somemodule.group (somemodule/group.py)

'''
group.py
Utility functions for working with FBGroup.
'''

import pyfbsdk

def Create(longName, components = []):
    '''
    Creates a new group with the given long name, and optionally populates it
    with the specified list of components. Returns the new group.
    '''
    group = pyfbsdk.FBGroup(longName)
    for comp in components:
        group.Items.append(comp)
    return group

somemodule.selection (somemodule/selection.py)

'''
selection.py
Utility functions for working with component selection.
'''

import pyfbsdk

def List():
    '''
    Returns a list of all components in the scene that are currently selected.
    '''
    return [c for c in pyfbsdk.FBSystem().Scene.Components if c.Selected]

def ListModels(sortBySelectOrder = False):
    '''
    Returns a list of all models in the scene that are currently selected.
    '''
    selectedModels = pyfbsdk.FBModelList()
    pyfbsdk.FBGetSelectedModels(selectedModels, None, True, sortBySelectOrder)
    return selectedModels

Create a group containing the selected models, urbanely

import somemodule as mb; reload(mb)

mb.Group.Create('InitialSelection', mb.Selection.ListModels())

Notes & Errata

Why Me? Why Bother?

I wrote this chapter with the artist-turned-programmer in mind. If you're one of them — and hi there, I'm one too — perhaps you're wondering whether all this belabored discussion of computer science concepts is a bit overkill for what you're doing. After all, you probably have a much broader skillset than just programming, and writing scripts with the MotionBuilder SDK may just be a means to an end — something that helps you solve some bigger problem. Maybe you've got a bit more interest in solving that bigger problem than in determining whether function f belongs in module a or module b.

It may even be that you're the only MotionBuilder scripter in the building. Perhaps the script you're writing is purpose-built for a project that will be wrapped up for good in a week's time. Should you really concern yourself with maintainability and readability in a situation like this?

Yes. Yes, you should.

Even if you're the only person who will ever see what you've written, you are an unfamiliar reader of your own code. If you've ever written a moderately complex script without adding any comments, only to come back to it after a week or two, you're probably familiar with the experience of staring blankly at a jumbled mess of syntax and gasping to yourself, "what the hell was I thinking?"

All of these little things, from choosing meaningful names to factoring common or complex functionality into separate functions, work together to ensure that you don't have to ask yourself that question. They provide you with an immediate benefit as you write the code, not just when you review it later on. A moderately complex program will not fit inside your brain. Effective programming is all about organizing your code in such a way that you can reason about its behavior in small, well-defined chunks.

It's certainly possible to take these things too far, and I discuss those more nuanced considerations below. In general, though, putting deliberate effort into the design and organization of your code will benefit you and your work immensely, regardless of your job title. You're writing software, whether you like it or not.


The Module Search Path

Before you can import a module, it needs to be in one of the directories in the module search path, which you can access and edit via the list sys.path. But manually appending your directory to this list isn't a permanent solution, as it only stays in effect for the current session. To make the change permanent, you have a few options:


The Limits of Decomposition

A fancier name for the type of decomposition we're talking about here is hierarchical decomposition. That is, if you have a large problem, you first break it down into smaller problems. Then, for each of those smaller problems, where appropriate, you factor out another, lower-level series of procedures. It's a recursive process — you keep doing this until it no longer makes sense to do so. And then, for God's sake, you stop.

Consider this snippet of code. It's very straightforward: anyone who's ever written a single MotionBuilder script can instantly understand what it does.

cube = FBModelCube('my_cube')
cube.Show = True

But maybe you don't like having to change the Show property whenever you create a cube. Maybe you've decided that it'd make more sense if cubes were shown by default, not hidden. So, in order to compress these two statements into one, you define a function:

def CreateCube(longName, show = True):
    cube = FBModelCube(longName)
    cube.Show = show
    return cube

This isn't horrible style, but there are some drawbacks to be aware of. Your code is indeed marginally shorter:

cube = CreateCube('my_cube')

But looking at this line by itself, another MotionBuilder programmer can no longer instantly know the state of the new cube. Is it shown or not? Has anything else been done to it? You've redefined the semantics of something that the API already does perfectly well, without really adding anything new.

You could certainly use a function like this and it wouldn't be the end of the world. The more important point is that there are limits to how far you should take this idea, but there are no hard-and-fast rules dictating where to stop. But so long as you're still in the domain of the original problem, and your function actually abstracts away some lower-level details or offers some useful parameterization, it's probably a safe bet.


The Limits of Decomposition, Continued

I'll give one more example to hopefully prove that I'm not contradicting myself. Let's say that for part of a script, we need to create and attach a cube model to every top-level model in the scene. Each new cube should have a name that's derived from the original model's, and it should be parented to the original model and positioned directly on top of it. (Don't ask me how this could possibly be useful. It's a million-dollar trade secret.)

We could simply plunk this code right into the body of our script:

for model in FBSystem().Scene.RootModel.Children:

    cube = FBModelCube('%s_Cube' % sourceModel.LongName)
    cube.Show = True
    m = FBMatrix(); sourceModel.GetMatrix(m)
    cube.SetMatrix(m)
    cube.Parent = sourceModel

But, in contrast to our earlier example, here's an alternative version of this script that I'd argue is unequivocally better:

def AttachCube(sourceModel):
    ''' Creates a cube and positions it at the given model. '''
    cube = FBModelCube('%s_Cube' % sourceModel.LongName)
    cube.Show = True
    m = FBMatrix(); sourceModel.GetMatrix(m)
    cube.SetMatrix(m)
    cube.Parent = sourceModel

def AttachCubesToModels(sourceModels):
    ''' Attaches a cube to every model in the given list. '''
    for model in sourceModels:
        AttachCube(model)

AttachCubesToModels(FBSystem().Scene.RootModel.Children)

Finally, in an attempt to find the line (and cross right over it), here's a completely asinine version:

def CreateModel(modelClass, longName, show = True):
    ''' Creates a model of the given class. '''
    model = modelClass(longName)
    model.Show = show
    return model

def CreateCube(longName, show = True):
    ''' Creates a cube, making it visible by default. '''
    return CreateModel(FBModelCube, longName, show)

def GenerateName(sourceComponent, suffix):
    ''' Returns the suffixed long name of the component. '''
    return '%s_%s' % (sourceComponent.LongName, suffix)

def CopyTransformation(model, sourceModel):
    ''' Applies the source model's transformation to model. '''
    sourceTransformation = FBMatrix()
    sourceModel.GetMatrix(sourceTransformation)
    model.SetMatrix(sourceTransformation)
    
def SetParentModel(model, parentModel):
    ''' Makes model a child of parentModel. '''
    model.Parent = parentModel

def AttachCube(sourceModel):
    ''' Creates a cube and positions it at the given model. '''
    cube = CreateCube(GenerateName(sourceModel, 'Cube'))
    CopyTransformation(cube, sourceModel)
    SetParentModel(cube, sourceModel)

def AttachCubesToModels(sourceModels):
    ''' Attaches a cube to every model in the given list. '''
    for model in sourceModels:
        AttachCube(model)

AttachCubesToModels(FBSystem().Scene.RootModel.Children)

All three of these scripts do exactly the same thing. But the second one is by far the easiest to write, the easiest to read and understand, and the easiest to modify without unintended side effects.


Corrections?

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