Nuts and Bolts

robot.png

I really meant to have an initial version of the ephemeral rig-aware breakdown tool this week, but apparently life had other ideas! So instead we’re going to investigate how node callbacks work.

This post has code snippets, and anyone who finds them useful should feel free to swipe them for their own work. However, I am not a developer, so I make no claims as to the actual quality of this code. Basically, use it at your own risk.

First a couple of foundational ideas behind this particular ephemeral system. It actually uses two rigs, a deformation rig and a control rig. The deformation rig has keyframes but is never manipulated by the animator. The control rig can be manipulated but has no keyframes. Every control in the control rig has a precisely corresponding transform in the deformation rig.

Here you can see a deformation rig target (the locator) being synced up to a control ephemerally. The target transform has keys, although they appear yellow in the channel box because I am using a character set, but no other incoming connections.

It also has, for lack of a better word, two “modes.” In “interaction” mode the control rig takes control of the deformation rig using node callbacks and the API. This mode allows the user to control the deformation rig ephemerally by manipulating the control rig. “Play” mode is active during playback, scrubbing, or at any other time the user is interacting with the timeline and changing the current frame. In play mode the control rig has no effect--very important, since it has no keys and therefore no animation! Instead, the deformation rig is allowed to play back normally, and then when the user stops changing the current frame the control rig is conformed to the current state of the deformation rig and interaction mode is reactivated.

A bunch of the complexity in the ephemeral system comes from the need to switch modes smoothly and automatically, so that the animator never needs to notice or care about them. But the two modes themselves are not actually that complicated. In this post we’ll look at how interaction mode works.

Dealing with node callbacks means we will need to get into the Maya API, something I hadn’t done personally before I began building this system, but that’s a lot less daunting than it used to be. OpenMaya 2 means you can code for the API with Python in a way that’s performant enough for this purpose, and basically treat it as just another way to script. Like many TDs of the old school, I’m not a real developer, and I have no experience with C++ or compilers, so this is pretty useful!

The API often requires you to jump through a bunch of hoops, frequently by creating a bunch of additional objects, in order to do anything. I tried to wrap this up as much as possible. For instance, here’s a function that gets an MObject for a node from the node’s name, and then one that gets a given plug from an MObject.

import maya.api.OpenMaya as om2

def getMObj(name):
    tempList = om2.MSelectionList()
    tempList.add(str(name))
    return tempList.getDependNode(0)

def getPlug(mObj, plugName):
    mfnDep = om2.MFnDependencyNode(mObj)
    return mfnDep.findPlug(plugName, False)

For those like me who come from a purely pyMEL/cmds module background, MObjects are objects that point to and manipulate Maya nodes. And for our purposes at least, plugs are basically synonymous with attributes. So if I wanted to, for instance, get the value of a float attribute through the API, I could do this:

attrValue = getPlug(getMObj("nameOfObject"), "nameOfAttribute").asFloat()

Which isn’t really all that much more complicated than the pyMEL...

pm.PyNode("nameOfObject").nameOfAttribute.get()

...that I might have used otherwise.

The biggest problem we’ll face in understanding node callbacks is a lack of documentation. The reference docs for the Maya API are fine--if you want to find out what kind of methods are available to a MNodeMessage object, that’s easy enough. But there’s very little out there explaining how you’d actually use the object.

Most of what I know about the use of node callbacks I got from watching Cult of Rig, Raffaele Fragapane's rigging stream. Cult of Rig is worth watching for a lot of reasons; Raff (he has two fs, I have one) is really thinking about rigging in a much more structured and well-constructed way than most riggers are. But the most relevant point for me is that he actually uses node callbacks in a real-world situation, and explains why he is doing so.

The API lets you attach a node callback to a node in Maya. After that, whenever a specific event occurs--an attribute of the node changes, or the node is dirtied, for example--it will fire the callback. You can attach a function of your own to the callback, which it will run whenever it fires.

def createEphCallback(node, data):
    om2.MNodeMessage.addNodeDirtyPlugCallback(node, callbackFunc, data)

This creates a callback that fires whenever the node is dirtied, which includes when nodes further up the DAG hierarchy are manipulated--very important since we want the callback to fire no matter what the node is parented to. The data argument is whatever the callback function needs to receive to do whatever it does. The callback will automatically pass three arguments to the callback function--the data argument will be the third--and you need to write it to receive those arguments. You don’t necessarily need to do anything with the first two though.

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

Here, the data argument is a list that contains all the data I want the function to have--basically it’s what I would pass to the function as arguments if I was calling it normally. Since the callback will only pass one custom argument to the function, here I give it one list of all the arguments I want it to have and turn it back into individual variables on the other side.

Two of these are plugs, the value of which the function checks to see if it should do the ephemeral rig matching at all--this will be very important when we discuss the different modes the system works under. If the answer is yes, it uses the matchUsingTransformMatrix function to match the world space translate, rotate, and scale of the target object (the deformation rig node being controlled) to the world matrix of the source object (the corresponding control being manipulated by the animator). This matching is ephemeral because the function simply sets the plugs on the destination node to do this, without creating any connections in the node graph.

To get the appropriate data to pass into the callback function, I get the world matrix plug from an MObject:

def getTransformMatrixPlug(mObj):
    mfnDep = om2.MFnDependencyNode(mObj)
    return mfnDep.findPlug('worldMatrix', False).elementByLogicalIndex(0)

This matrix can be decomposed into translate, rotate, and scale values (going through a bunch of other API objects in the meantime):

def decomposeMatrix(matrixObj):
    mMatrix = om2.MFnMatrixData(matrixObj.asMObject()).matrix()
    transformMatrixObj = om2.MTransformationMatrix(mMatrix)
    translation = transformMatrixObj.translation(om2.MSpace.kWorld)
    radianRot = transformMatrixObj.rotation()
    scale = transformMatrixObj.scale(om2.MSpace.kWorld)
    return [[translation.x, translation.y, translation.z],[radianRot.x, radianRot.y, radianRot.z], scale]

Naturally, since it’s the control object’s world matrix, the TRS values will be in world space. For the moment that’s fine--at some point I will insert an additional step here to multiply this matrix by something first so that the whole system can be relative to something other than world space, but for the test I’m doing at the moment that’s not necessary.

I also need to be able to get the plugs to put these values on off of the target object:

def getTransformPlugs(mObj):
    mfnDep = om2.MFnDependencyNode(mObj)
    transformAttrs = [['tx','ty','tz'], ['rx', 'ry', 'rz'], ['sx', 'sy', 'sz']]
    return [[getPlug(mObj, attrName) for attrName in listOfAttrs] for listOfAttrs in transformAttrs]

Note that it returns the plugs as a list of lists. This will be important in a moment when I set the plugs.

The callback function passes the destination plugs and the source matrix plug to matchUsingTransformMatrix, which then decomposes the matrix and sets the destination plugs:

def matchUsingTransformMatrix(sourceMatrixPlug, targetPlugs):
    sourceVals = decomposeMatrix(sourceMatrixPlug)
    for sourceXYZVals, targetXYZPlugs in zip(sourceVals, targetPlugs):
        for sourceVal, targetPlug in zip(sourceXYZVals, targetXYZPlugs):
            targetPlug.setFloat(sourceVal)

It uses nested for loops like that because what the decomposeMatrix returns is a list of three lists (translate, rotate, scale), each list containing three values (x, y, z). The getTransformPlugs function returns the plugs in the same format so that they can be zipped together cleanly. (I’m pretty sure there’s a more elegant way to do this then using a nested for loop, but I couldn’t be bothered to figure one out when I wrote this.)

Last but definitely not least, you need a way to kill the callbacks, because if you don’t this will happen:

brooms.jpg

Luckily it is really easy to find out what callbacks you have on a given node and destroy them.

def killCallbacks(mObj):
    for cb in om2.MMessage.nodeCallbacks(mObj): om2.MMessage.removeCallback(cb)
Take that!

Take that!

And fundamentally, that’s how interaction mode works.