Tutorial on how to write a install-able Maya node

About

This is a tutorial on how to write a node that sets up features in your Maya scene without having explicit dirty propagation relationships in the dependency graph.

For this node, we're going to focus mostly on the how of writing the node itself, and thus the actual functionality will be restricted to something very basic; in this case, creating a spiral motion when a user moves the transform node in the scene that we set up the feature for.

All the source code for this example node is available here.

What exactly is this approach?

You know how you "normally" do an IK/FK switch in a rig by having 2 joint chains, constrain them both to a 3rd joint chain, and then have a bunch of connections in the Node Editor tied to some custom attribute on some transform node to blend the constraints?

Think about it for a second: rather than having this "feature" be built into the dependency graph of the rig itself, why not have it be able to be set up on-demand? There is no reason that this feature needs to stay in the rig, which creates all sorts of complications when you switch between the IK/FK chains and affect the dependency graph unnecessarily. In short, there's no need to store this feature as a fixed state in the dependency graph.

By utilizing the callback mechanism of Maya, we can avoid having to "bake" this features/state into the rig, and setup our desired rig behaviours completely independently of what the rig's graph might actually have, at runtime. We can also un-install this feature as well, all without affecting the original graph. This philosophy makes it tremendously easy to reason about components of the rig independently. (Very much like traditional software development!)

Now, it's important to note that not all problems lend themselves well to this approach; particularly, problems that are stateful (i.e. are tied to time, velocity, etc.). Those problems tend to require a little more careful thought in order to determine their suitability towards such an approach.

For more information on this approach to installing scene features on-demand, please refer to Raffaele Fragapane's Cult Of Rig series on the thought process behind this approach. He goes over it in a lot more detail.

Why not just do what Raffaele does and use a scriptNode?

The reason we're using a compiled node here instead of a scriptNode as he details in the original approach is due to the fact that the script executed from a script node in Maya has no concept of which node executed it, and thus makes it very difficult to implement in production when you reference this script node in a separate namespace without implementing a callback manager of some sort to manage the callbacks you are installing into the scene, along with managing the associated namespaces/nodes for each of those callbacks registered by the script node.

By using a compiled node instead, we can manage all of that registration/ un-registration mess within the node itself much more cleanly and reason about the state of our Maya scene a lot more easily than a script node would otherwise have allowed for.

Requirements

What you should know

Getting started

Firstly, we'll just get a basic skeleton setup of the plugin going. As a refresher, this just means that you need to create a defintion of a MPxNode that implements a creator function and an initializer function, which we will call creator() and initialize() respectively.

Thus, in callback_node.h:

/// This is a dependency node that will install a callback during its lifetime.
class CallbackNode : MPxNode
{
public:
    static void *creator();

    static MStatus initialize();

    static const MTypeId kNODE_ID;      /// The unique ID that identifies this node.
    static const MString kNODE_NAME;    /// The name of the DG node.

And callback_node.cpp, which, for now, looks pretty sparse:

#include "callback_node.h"

const MTypeId CallbackNode::kNODE_ID(0x0007ffff);
const MString CallbackNode::kNODE_NAME = "callbackNodeExample";


void *CallbackNode::creator()
{
    return new CallbackNode();
}


MStatus CallbackNode::initialize()
{
    MStatus result;
    return result;
}

We also add two extra attributes called kNODE_ID and kNODE_NAME to help us identify the dependency node later on.

Great! We've got our node now, let's write the basic plugin structure to register it. In case you needed a refresher:

In plugin_main.cpp:

#include "plugin_main.h"
#include <maya/MFnPlugin.h>

const char *kAUTHOR = "Me, the author";
const char *kVERSION = "1.0.0";
const char *kREQUIRED_API_VERSION = "Any";


MStatus initializePlugin(MObject obj)
{
    MStatus status;
    MFnPlugin plugin(obj, kAUTHOR, kVERSION, kREQUIRED_API_VERSION);

    status = plugin.registerNode(CallbackNode::kNODE_NAME,
                                 CallbackNode::kNODE_ID,
                                 &CallbackNode::creator,
                                 &CallbackNode::initialize,
                                 MPxNode::kDependNode);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return status;
}


MStatus uninitializePlugin(MObject obj)
{
    MFnPlugin plugin(obj);

    MStatus status;
    status =  plugin.deregisterNode(CallbackNode::kNODE_ID);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return status;
}

In plugin_main.h, I go ahead and setup a Single Translation Unit (STU) Build/Unity build (which is easy, since there's only one source file right now):

#include "callback_node.cpp"

MStatus initializePlugin(MObject obj);

MStatus uninitializePlugin(MObject obj);

You should be able to call the compiler command on your corresponding platform on just plugin_main.cpp and have the plugin compile correctly.

I will not go into the details regarding a STU build here, but suffice to say that I have found them much more beneficial to build times than any other compiler feature (LTO, IncrediBuild, splitting the code out into pre-compiled libs, whatever). For such a small project, it doesn't matter; you can switch back to a more traditional build setup if you prefer.

If you have issues building the plugin on your own, please refer to the included CMakeLists.txt to see how I manage my own builds. You are not required to use CMake; it is just my own preference.

If you got past all that and got a plugin building, great! We have a node that does...well, nothing.

Now we just need to make it work.

What are we even doing?

So before we jump into the node's features itself, let's take a step back and think about what we want this node to do at a high-level:

callbackNodeExample transform Transform node callback Callback node transform->callback    1. Read translateX callback->transform    2. Set new translate Y/Z in a spiral-ly fashion

Ok. So we know that we want our node to read the translate information from the transform node. However, I just promised that we wouldn't have explicit connections between our nodes in the DG. So what we'll do instead is use message attributes to do the job of making sure our nodes know about each other. Kind of like Tinder, but just for DG nodes.

Creating the message attribute

If you need a refresher on what a message attribute is:

A message attribute is a dependency node attribute that does not transmit data. Message attributes only exist to formally declare relationships between nodes. By connecting two nodes via message attributes, a relationship between those nodes is expressed.

The Maya Documentation for MFnMessageAttribute, verse 2:0

Basically, by utilizing this Maya feature, we'll be able to avoid having any data transmitted explicitly in the graph; we'll be reading it directly from the transform node itself.

Let's add this message attribute to our custom node definition:

const char *CallbackNode::kIN_TRANSFORM_ATTR_NAME = "transform";
MObject CallbackNode::inTransformAttr;


MStatus CallbackNode::initialize()
{
    MStatus result;

    MFnMessageAttribute fnMsgAttr;
    inTransformAttr = fnMsgAttr.create(CallbackNode::kIN_TRANSFORM_ATTR_NAME,
                                       CallbackNode::kIN_TRANSFORM_ATTR_NAME,
                                       &result);
    CHECK_MSTATUS_AND_RETURN_IT(result);

    addAttribute(inTransformAttr);
    return result;
}

However, let's not forget that this attribute needs to be connected to something. In this case, that something is the transform node that we're going to be moving around.

Writing the MPxCommand to setup everything

To make things easier for our end-users, let's make a MPxCommand that they can run in order to create our node automatically, along with handling the connections between it and the transform node. If you've never written such a thing before or need some reference, the command I've written is below:

apply_callback_command.h

class ApplyCallbackCommand : public MPxCommand
{
public:
    static void *creator();

    MStatus doIt(const MArgList &args);

    MStatus redoIt();

    MStatus undoIt();

    bool isUndoable() const;

    static MSyntax newSyntax();

    /// The name of the command that is meant to be run.
    static const MString kCOMMAND_NAME;

    MStatus parseArgs(const MArgList &args);

    /// Storage for the flag arguments that will be passed into the command.
    bool flagHelpSpecified = false;
    MSelectionList flagSelList;

    /// Storage for the operations that this command performs on the DG so that we
    /// can undo them if necessary.
    MDGModifier dgMod;
};

Let's go over method-by-method of how to implement each of these in apply_callback_command.cpp:

First, let's define some of the constants we'll be using:


const char *flagSelListLongName = "-node";
const char *flagSelListShortName = "-n";

const char *flagHelpLongName = "-help";
const char *flagHelpShortName = "-h";

const char *helpText = "This command will setup a callback on a given node.\n"
    "Usage:\n   applyCallback [options]\n"
    "Options:\n"
    "-h / -help     Prints this message.\n\n"
    "-n / -node     The name of the node to setup the callback example for.\n\n";

const MString ApplyCallbackCommand::kCOMMAND_NAME = "applyCallback";

The creator function will basically return a new instance of the command.


void *ApplyCallbackCommand::creator()
{
    return new ApplyCallbackCommand();
}

The newSyntax() function is one we define ourselves, and it's what sets up the actual parameters that the command will accept in Maya. We'll also implement parseArgs() to actually take the arguments we give to the command and figure out if we're just calling it with a help flag, or if we're actually passing an object in.

MSyntax ApplyCallbackCommand::newSyntax()
{
    MSyntax syntax;
    syntax.addFlag(flagHelpShortName, flagHelpLongName);
    syntax.addFlag(flagSelListShortName, flagSelListLongName, MSyntax::kSelectionItem);
    syntax.enableQuery(false);
    syntax.enableEdit(false);
    syntax.useSelectionAsDefault(true);

    return syntax;
}


MStatus ApplyCallbackCommand::parseArgs(const MArgList &args)
{
    MStatus result;
    MArgDatabase argDb(syntax(), args, &result);
    CHECK_MSTATUS_AND_RETURN_IT(result);

    if (argDb.isFlagSet(flagHelpShortName)) {
        displayInfo(helpText);
        flagHelpSpecified = true;
        return MStatus::kSuccess;
    } else {
        flagHelpSpecified = false;
    }

    if (argDb.isFlagSet(flagSelListShortName)) {
        argDb.getFlagArgument(flagSelListShortName, 0, flagSelList);
    }

    return result;
}

The doIt and redoIt functions are where the meat of the command happens; doIt basically calls redoIt (so that redos actually work correctly!). We create a new callback node, along with checking if the transform node passed into the command is valid.


MStatus ApplyCallbackCommand::doIt(const MArgList &args)
{
    setCommandString(ApplyCallbackCommand::kCOMMAND_NAME);
    clearResult();

    MStatus result = parseArgs(args);
    CHECK_MSTATUS_AND_RETURN_IT(result);

    if (this->flagHelpSpecified == true) {
        return MStatus::kSuccess;
    }

    return redoIt();
}


MStatus ApplyCallbackCommand::redoIt()
{
    MStatus result;
    if (flagSelList.length() != 1) {
        MGlobal::displayError("You need to select a single node to apply the callback to!");
        return MStatus::kInvalidParameter;
    }

    if (doesCallbackNodeAlreadyExist() == true) {
        MGlobal::displayError("The feature already exists!");
        return MStatus::kFailure;
    }

    MObject callbackNode = dgMod.createNode(CallbackNode::kNODE_ID, &result);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    result = dgMod.doIt();
    CHECK_MSTATUS_AND_RETURN_IT(result);

    MFnDependencyNode fnNode(callbackNode);
    MPlug callbackNodeMsgPlug = fnNode.findPlug(CallbackNode::kIN_TRANSFORM_ATTR_NAME,
                                                false,
                                                &result);
    CHECK_MSTATUS_AND_RETURN_IT(result);

    MObject transform;
    result = flagSelList.getDependNode(0, transform);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    if (!transform.hasFn(MFn::kDependencyNode)) {
        MGlobal::displayError("The object specified is not a valid DG node!");
        return MStatus::kInvalidParameter;
    }

    return result;
}

As for isUndoable(), we just basically tell Maya that this command is undo-able, and implement the functionality in undoIt() by deleting the node.


MStatus ApplyCallbackCommand::undoIt()
{
    dgMod.undoIt();
    return MStatus::kSuccess;
}


bool ApplyCallbackCommand::isUndoable() const
{
    if (flagHelpSpecified == true) {
        return false;
    } else {
        return true;
    }
}

It's a bit of boilerplate code to write, but it's not that complicated when you see what it's actually doing. We basically create a new CallbackNode, accept a single transform as a command argument, and do some basic sanity checking of the inputs to make sure everything's good. Nothing special, really.

(Again, if something here doesn't make sense to you, please look through some of the examples of how to write command plugins in the Maya documentation.)

There's one thing we need to do before we call this command good, though; we need to actually make the connection between the callback node and the transform node that we pass to the command:

    //... the earlier part of redoIt()
    MObject transform;
    result = flagSelList.getDependNode(0, transform);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    if (!transform.hasFn(MFn::kDependencyNode)) {
        MGlobal::displayError("The object specified is not a valid DG node!");
        return MStatus::kInvalidParameter;
    }
    result = fnNode.setObject(transform);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    if (!fnNode.hasAttribute(CallbackNode::kMSG_CXN_ATTR_NAME)) {
        MFnMessageAttribute fnMsgAttr;
        MObject msgAttr = fnMsgAttr.create(CallbackNode::kMSG_CXN_ATTR_NAME,
                                           CallbackNode::kMSG_CXN_ATTR_NAME,
                                           &result);
        CHECK_MSTATUS_AND_RETURN_IT(result);
        fnNode.addAttribute(msgAttr);
    }
    MDGModifier dgModCxn;
    MPlug msgPlug = fnNode.findPlug(CallbackNode::kMSG_CXN_ATTR_NAME, false, &result);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    result = dgModCxn.connect(msgPlug, callbackNodeMsgPlug);
    CHECK_MSTATUS_AND_RETURN_IT(result);
    dgModCxn.doIt();

We basically create a new message attribute on the transform node and connect it up (using a different MDGModifier, since our internal one is going to be reserved for deleting the callback node we created if the user chooses to undo, and deleting the node will automatically break the connections anyway)

We also need to define CallbackNode::kMSG_CXN_ATTR_NAME and CallbackNode::kIN_TRANSFORM_ATTR_NAME, which will be the names of the message attributes on the callback and transform nodes respectively. You can do that in the respective source files however you like.

Finally, we should also probably register/de-register this command so that we can actually use it proper:


MStatus initializePlugin(MObject obj)
{
    // ...previous stuff

    status = plugin.registerCommand(ApplyCallbackCommand::kCOMMAND_NAME,
                                    ApplyCallbackCommand::creator,
                                    ApplyCallbackCommand::newSyntax);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return status;
}


MStatus uninitializePlugin(MObject obj)
{
    // ...again, more previous stuff

    status = plugin.deregisterCommand(ApplyCallbackCommand::kCOMMAND_NAME);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return status;
}

Getting somewhere

Once all of that is done and your plugin is built, you should be able to run the following MEL script (or similar):

file -f -newFile;
unloadPlugin "callbackNodeExample";
loadPlugin "c:/Users/sonictk/Git/experiments/maya_node_callback_example/build/Debug/callbackNodeExample.mll";

createNode "transform";
applyCallback -n "transform1";

Which should give you something similar to the following:

callbackNodeExample transform Transform node callback node msg. attr. callback Callback node transform node msg. attr. transform:f1->callback:f1 message conn.

If you're still following along; great! Let's now focus on actually doing something useful with the nodes we've made.

A more detailed overview of the entire setup

Now that we've established the relationship between the nodes, let's think about what we need to do next. We know that we want to get the translateX value from the transform node whenever a user changes it interactively. We also know that we don't want this behaviour to be dirty propagation-based (i.e. no explicit connection in the graph).

If you paid attention at all during the stream, you'll know what the answer is: it's in the form of Maya's various callback mechanisms. However, before we jump right into writing it up, let's think a little again over what we need to do in greater detail this time:

callbackNodeExample transform Transform node callback node msg. attr. translateX attr. changed callback translateY/Z transform:f2->transform:f3 callback Callback node transform node msg. attr. installation callback callback registry transform:f1->callback:f1 message conn. transform:f3->callback:f3 callback ID is stored so that it can be un-registered on node deletion/disconnection readTranslateXCB Callback function transform:f3->readTranslateXCB 2. Calls function when translateX. changes interactively on transform node callback:f2->transform:f3 1. Registers this callback when node is created/connected readTranslateXCB->transform:f4:s 3. Sets final ty/tz values on the transform node

Ok, that's a little confusing. As a great man once said, let's break it down.

Registering the callback onto the transform node

callbackNodeExample transform Transform node callback node msg. attr. translateX attr. changed callback translateY/Z transform:f2->transform:f3 callback Callback node transform node msg. attr. installation callback callback registry transform:f1->callback:f1 message conn. transform:f3->callback:f3 callback ID is stored so that it can be un-registered on node deletion/disconnection readTranslateXCB Callback function transform:f3->readTranslateXCB 2. Calls function when    translateX. changes interactively on transform node callback:f2->transform:f3 1. Registers this callback when node is created/connected readTranslateXCB->transform:f4:s 3. Sets final ty/tz values on the transform node

We already did the message connection earlier, so we can move on.

callbackNodeExample transform Transform node callback node msg. attr. translateX attr. changed callback translateY/Z transform:f2->transform:f3 callback Callback node transform node msg. attr. installation callback callback registry transform:f1->callback:f1 message conn. transform:f3->callback:f3 callback ID is stored so that it can be un-registered on node deletion/disconnection readTranslateXCB Callback function transform:f3->readTranslateXCB 2. Calls function when    translateX. changes interactively on transform node callback:f2->transform:f3 1. Registers this callback when node is created/connected readTranslateXCB->transform:f4:s 3. Sets final ty/tz values on the transform node

Let's start here instead. We need to set-up a callback on the callback node when it is created that is responsible for setting up another callback on the transform node in order to watch for any attribute changes on it. We can do this by making use of the MPxNode::postConstructor virtual method, like so:


void CallbackNode::postConstructor()
{
    MStatus status;
    MObject thisNode = thisMObject();

    MCallbackId installId = MNodeMessage::addAttributeChangedCallback(thisNode,
                                                                      installCallback,
                                                                      NULL,
                                                                      &status);
    if (status != MStatus::kSuccess) {
        MGlobal::displayError("Unable to install example feature!");
        uninstallCallback();
        return;
    }
    callbacks.append(installId);
    MNodeMessage::addNodePreRemovalCallback(thisNode,
                                            uninstallCallback,
                                            NULL,
                                            &status);
    if (status != MStatus::kSuccess) {
        MGlobal::displayError("Unable to install example feature!");
        uninstallCallback();
        return;
    }
}

What are installCallback and uninstallCallback, you ask? Let's take a look at the documentation for both MNodeMessage::addAttributeChangedCallback and MNodeMessage::addNodePreRemovalCallback to get some hints:

MCallbackId addAttributeChangedCallback(MObject &node, 
                                        MNodeMessage::MAttr2PlugFunction func, 
                                        void *clientData = NULL, 
                                        MStatus *ReturnStatus = NULL)

MCallbackId addNodePreRemovalCallback(MObject &node, 
                                      MMessage::MNodeFunction func, 
                                      void *clientData = NULL, 
                                      MStatus *ReturnStatus = NULL)

So basically our installCallback and uninstallCallback functions need to match the signatures of a MAttr2PlugFunction and a MNodeFunction, whatever those might be. Looking at an MAttr2PlugFunction signature gives the following:


typedef void(* MAttr2PlugFunction) (MNodeMessage::AttributeMessage msg, 
                                    MPlug &plug, 
                                    MPlug &otherPlug, 
                                    void *clientData)

Basically a function pointer that takes two MPlugs. And a MNodeFunction?


typedef void(* MNodeFunction) (MObject &node, void *clientData)

Yep, a function pointer that takes a MObject node. Not terribly complicated, which is always good.

Knowing this, we can go ahead and starting writing our callback functions. The first will be uninstallCallback, which basically just un-registers all the callbacks that currently exist in the callback registry (We'll worry about this in a bit). We have an overloaded version that returns nothing, and takes an MObject& along with some arbitrary data in order to match the function pointer signature detailed above.


MStatus uninstallCallback()
{
    MStatus status = MMessage::removeCallbacks(CallbackNode::callbacks);
    MGlobal::displayInfo("Removed feature!");
    return status;
}


void uninstallCallback(MObject &node, void *data)
{
    uninstallCallback();
}

With that done, we can then go ahead and implement the callback that handles installation of the callback onto the transform node itself.


void installCallback(MNodeMessage::AttributeMessage msg,
                     MPlug &plug,
                     MPlug &otherPlug,
                     void *data)
{
    if (msg == (MNodeMessage::kConnectionBroken|
                MNodeMessage::kIncomingDirection|
                MNodeMessage::kOtherPlugSet)) {
        uninstallCallback();
    }
    if (msg != (MNodeMessage::kConnectionMade|
                MNodeMessage::kIncomingDirection|
                MNodeMessage::kOtherPlugSet)) {
        return;
    }
    // NOTE: (sonictk) We check if the node has its message connection connected
    // first to determine if we should install the real callback onto that node
    MObject callbackNode = plug.node();
    MFnDependencyNode fnNode(callbackNode);
    MPlug cxnPlug = fnNode.findPlug(CallbackNode::kIN_TRANSFORM_ATTR_NAME);
    MPlugArray connectedPlugs;
    cxnPlug.connectedTo(connectedPlugs, true, false);
    if (connectedPlugs.length() != 1) {
        return;
    }
    MObject transformNode = connectedPlugs[0].node();
    if (!transformNode.hasFn(MFn::kTransform)) {
        return;
    }
    // NOTE: (sonictk) Install the callback onto the other node and add it to the
    // registry of callbacks to track
    MStatus status;
    MCallbackId featureCallbackId = MNodeMessage::addAttributeChangedCallback(transformNode,
                                                                              featureCallback,
                                                                              NULL,
                                                                              &status);
    if (status != MStatus::kSuccess) {
        return;
    }
    CallbackNode::callbacks.append(featureCallbackId);
    MGlobal::displayInfo("Feature installed!");

}

Basically, we check the message type that Maya passes us to see if we care about the type of event that just occurred, and either remove the callbacks from a global registry that the node maintains if we detect a disconnection, or install a new callback onto the transform node otherwise. Yes, this means we also need to define what this "global callback registry" is:

static MCallbackIdArray callbacks;

That's it. No need to over-complicate matters. (Remember, in a STU build, any variable with static storage duration is effectively global!)

We'll implement featureCallback in a little bit. For now, let's look at what our overview looks like now:

callbackNodeExample transform Transform node callback node msg. attr. translateX attr. changed callback translateY/Z transform:f2->transform:f3 callback Callback node transform node msg. attr. installation callback callback registry transform:f1->callback:f1 message conn. transform:f3->callback:f3 callback ID is stored so that it can be un-registered on node deletion/disconnection readTranslateXCB Callback function transform:f3->readTranslateXCB 2. Calls function when    translateX. changes interactively on transform node callback:f2->transform:f3 1. Done! readTranslateXCB->transform:f4:s 3. Sets final ty/tz values on the transform node

Yes, we did skip a little ahead, but it was all for a good cause. Let's go ahead and implement that featureCallback now to read the translateX value off of the transform node.


void featureCallback(MNodeMessage::AttributeMessage msg,
                     MPlug &plug,
                     MPlug &otherPlug,
                     void *data)
{
    if (msg != (MNodeMessage::kAttributeSet|MNodeMessage::kIncomingDirection)) {
        return;
    }
    const char *plugName = plug.partialName(0,0,0,0,0,1).asChar();
    if (strstr("translateX", plugName) == NULL) {
        return;
    }
    double xVal = plug.asDouble();
}

As we can see, it's very similar in theory to how we implemented installCallback; the difference here being that we look for a different type of event (kAttributeSet instead), and we just get the value of the translateX plug without doing anything else.

So just like that, our overview looks like this:

callbackNodeExample transform Transform node callback node msg. attr. translateX attr. changed callback translateY/Z transform:f2->transform:f3 callback Callback node transform node msg. attr. installation callback callback registry transform:f1->callback:f1 message conn. transform:f3->callback:f3 done! readTranslateXCB Callback function transform:f3->readTranslateXCB 2. Calls function when translateX. changes interactively on transform node callback:f2->transform:f3 1. Done! readTranslateXCB->transform:f4:s 3. Sets final ty/tz values on the transform node

Fast, wasn't it?

Now that we're reading the values, we can focus on step 3: setting new values back onto the transform node.

Getting our spiral behavior working

If you have any background in high school maths at all, you probably know what comes next:

void featureCallback(MNodeMessage::AttributeMessage msg,
                     MPlug &plug,
                     MPlug &otherPlug,
                     void *data)
{
    if (msg != (MNodeMessage::kAttributeSet|MNodeMessage::kIncomingDirection)) {
        return;
    }
    const char *plugName = plug.partialName(0,0,0,0,0,1).asChar();
    if (strstr("translateX", plugName) == NULL) {
        return;
    }
    MStatus status;
    MPlug transformPlug = plug.parent(&status);
    if (status != MStatus::kSuccess) {
        return;
    }
    double xVal = plug.asDouble();
    MPlug transformYPlug = transformPlug.child(1, &status);
    if (status != MStatus::kSuccess) {
        return;
    }
    MPlug transformZPlug = transformPlug.child(2, &status);
    if (status != MStatus::kSuccess) {
        return;
    }
    double newYVal = sin(xVal);
    double newZVal = cos(transformZPlug.asDouble() + xVal);
    transformYPlug.setDouble(newYVal);
    transformZPlug.setDouble(newZVal);
}

That's right, simple trigonometry functions!

And just like that, we're done!

...Kind of. There's just a bit of cleanup to do: in the uninitializePlugin function we wrote earlier, we need to make sure that we call uninstallCallback() as well to remove all the callbacks from the global registry (since we don't want those to persist if we remove the callback node from the scene).


MStatus uninitializePlugin(MObject obj)
{
    MFnPlugin plugin(obj);
    MStatus status;
    uninstallCallback();
    status =  plugin.deregisterNode(CallbackNode::kNODE_ID);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    status = plugin.deregisterCommand(ApplyCallbackCommand::kCOMMAND_NAME);
    CHECK_MSTATUS_AND_RETURN_IT(status);

    return status;
}

Ok, now we're done.

Conclusion

Is this the right solution for everything? No, absolutely not. This method is best used for installable features onto a rig where you can control the entire behaviour of the feature within a single set of depedency nodes that you control. You also need to take care that you do not trigger unnecessary DG evaluations within your callback methods that could potentially cause cycles in the DG (which won't be caught by Maya!)

However, this method will work far better than using scriptNodes, since they will work even when referenced, thus making them far better suited to production. You also are able to reason about the current callbacks that have been registered far more easily than if you had been using script nodes and managing which ones were registered to which namespace/object combinations in the scene.

Use with a healthy dose of caution and wonder, as always!

Credits

Raffaele Fragapane: For the idea regarding this in the first place, and for being an awesome smart dude.

Ryan Porter: For discussing/confirming with me the limitations of the Maya scriptNode and for bugging me to write this up in the first place.

Siew Yi Liang: Duh, I wrote these words and the example code here. You can find more of my ramblings here.