Hacking the Maya Animation System

threeKeyframesInATrechcoat.png

The previous post focused on the “interact” mode of the ephemeral rig, and how node callbacks are used to create the ephemeral rig behavior. This post is about the “playback” mode, and how the system switches back and forth between the modes.

While the user is scrubbing or playing back, playback mode literally does nothing. The deformation rig (which will not normally be manipulated by the user) has stepped keyframes, and those keyframes drive its motion exactly as one would expect. In an ideal world, I’d use an animation system based on poses through time, rather than conventional keyframes, but while implementing that in Maya would probably be possible, it would be a pretty significant overhaul of the way Maya looks at animation, probably with unintended consequences for the animation workflow. So while the ephemeral rig presents to the animator as if it were simply poses through time, on the deformation rig keyframes do in fact exist--they are simply never set by the animator.

What playback mode does need to do is to recognize when the user has stopped on a given frame, so that it can conform the control rig (which is in whatever state the user left it in, probably not the same pose as the current frame) to the deformation rig before turning interact mode back on. To do that I ended up using Maya’s other way of triggering arbitrary code based on something changing in the scene, the rather justifiably maligned “scriptJob.”

ScriptJobs are...well, they’re very MEL. For instance, while you can pass a function to them to execute just like you can with callbacks, the scriptJob doesn’t let you pass any data through to the function. This necessitates using a function that doesn’t need to receive any arguments to know what to operate on, which is irritating and complicates the code.

Another common problem with scriptJobs is that they don’t fire while the Maya scene is evaluating--they fire the next time the system is idle. So you could never use them for the kind of thing I’m using callbacks for here--if you did, the deformation rig transform would only update AFTER you’d moved the control rig around and released the mouse.

In this case though, that turns out to be a hidden advantage. For playback mode, something that fires after the user does something (ie. changes the current time) but not while they are doing so is precisely what I needed. ScriptJobs are slow compared to callbacks, but they’re still fast enough that the scriptJob can fire after the user releases the mouse, conform the control rig to the deformation rig, and switch interact mode back on before the user can click on anything else.

After consulting with Brian Kendall, I ended up deciding to use a system where an attribute in the scene defines what the current pose is for the purposes of the ephemeral rig system. This attribute is step-keyed right along with the the deformation rig, and on the pose that the animator is currently manipulating it’s value is 1. On all other poses it’s value is zero.

The ephCurrentPose attribute is always 1 on the current pose, and 0 on all others. When the value drops to 0 because you scrubbed past the current pose, it triggers a scriptJob that sets the new current pose to 1 after you release the mouse.

The ephCurrentPose attribute is always 1 on the current pose, and 0 on all others. When the value drops to 0 because you scrubbed past the current pose, it triggers a scriptJob that sets the new current pose to 1 after you release the mouse.

As you may recall from the previous post, each callback checks a plug called “watchPlug” to see if it should do the ephemeral matching:

def callbackFunc(msg, node, data):
    sourceMatrixPlug, targetPlugs, watchPlug, activePlug = data
    if watchPlug.asFloat() == 1 and activePlug.asFloat() == 1:
        matchUsingTransformMatrix(sourceMatrixPlug, targetPlugs)

When the value isn’t 1, the callback function does nothing, effectively disabling interaction mode. But this attribute is also being watched by a scriptJob, and when the attribute changes value--because the user has changed the current frame--it fires, causing the control rig matchback and resetting all the watchAttr’s keyframes to 0 except for the one associated with the current pose. Now we’re back to where we started, just on a different pose.

Here’s the code that creates the scriptJob:

pa.scriptJob(compressUndo = True, attributeChange = onWatchAttrChange)

This calls a function that performs a "matchback" from the target to the control of each ephControl, simply setting the control to the transformation of the target using pyMEL:

def matchBack(self):
    self.ephControl.setRotation(self.target.getRotation(), space='world')
    self.ephControl.setTranslation(self.target.getTranslation(), space='world')
    self.ephControl.scale.set(self.target.scale.get())

And then resets the ephCurrentPose attribute so that the pose the timeline is now on becomes the current pose, therby turning interact mode back on. To do that first I need to have a way to recognize the extents of a pose from the keyframe information I have:

poseBeginEndCode.PNG

The formatting of my blog completely mangled this code, so I did a screenshot from Sublime Text instead--not the best way to share a code example, I know! Basically it first figures out if you are or are not on the first frame of a pose, then uses findKeyframe to get you the first and last frame. Then:

setCurrentPose.PNG

One advantage to watching an attribute, as opposed to having the scriptJob fire on frame change itself, is that anything that changes the timing of the system’s keyframes still functions. It’s possible that I may want scripts and tools that change keyframes, and potentially shift a different pose onto the current frame, without changing the current time. This system behaves correctly whether or not the current time has been changed--the only relevant question is whether or not the pose on the current frame is the previous current pose, or a different pose that requires a control rig conform and switch back to interact mode.

Of course, this only works if you have a way to bundle up all the keys associated with the deformation rig and the watchAttr, and ensure that any operations you do to the keys affect all of them at once, since, despite being keys, they’re supposed to represent poses. Luckily we do have such a method, though, just like scriptJobs, it’s apt to my purpose but also weird and irritating: character sets.

Character sets are an outgrowth of the Trax editor. The Trax editor used to be Maya’s nonlinear animation tool. An ancient order of TDs sealed the Trax editor behind the Windows/Animation Editors menu, there to remain hidden for all time. Opening it will unleash its horror yet again upon an unsuspecting world, so you should probably not do that.

Character sets were the Trax-editor’s way of interacting with keyframes in Maya. They insert themselves between an animCurve node and the attribute it drives, with the intent to do a bunch of Trax editor-related stuff that I don’t care about. What I do care about is that it allows you to have the timeline display the keys of an arbitrary set of attributes, instead of anything related to your selection. All keyframe operations--making keys, copying them, moving them around, etc--happen to these attributes at once. In other words, it lets us treat a whole bunch of unrelated keys as if they were indeed one pose.

A character set gracelessly inserts itself between a transform node and it's keyframes.

A character set gracelessly inserts itself between a transform node and it's keyframes.

I wish there was a way to do this that wasn’t character sets, but I haven’t found another method yet, short of writing my own timeline. After I get the ephemeral rig system rock solid, I’m thinking about trying to do my own timeline in QT, and seeing if that’s a reasonable thing to do. There are so many features--character-based tracks, markers, regular beat markers, the list goes on--that I’d like to have in a Maya timeline, and don’t. But I guess that’s going to have to wait a while.