Introduction to Dependency Graph Plug-ins

A dependency graph plug-in is the most common type of plug-in. Developers usually create custom nodes to perform some sort of mathematical calculation or geometric operation. Before continuing, you should read the Dependency Graph Plug-ins section of the Maya documentation. It contains a lot of useful information about Maya dependency graph architecture and node creation workflow. Once you have read that information, it will be easier to follow along as we create our first dependency graph node. Our first node will be a simple node that takes an input float value, doubles it, and sets the doubled value as the output. It is a pretty useless node, but it will introduce us to the structure of node plug-ins and lay the foundation for more interesting nodes.

To create a dependency graph node, we create a class that inherits from the proxy class, MPxNode:

#ifndef DOUBLERNODE_H
#define DOUBLERNODE_H

#include 
#include 
#include 
#include 

#include 

class DoublerNode : public MPxNode {
 public:
  DoublerNode() {}
  virtual MStatus compute(const MPlug& plug, MDataBlock& data);
  static void* creator();
  static MStatus initialize();

  static MTypeId id;
  static MObject aInput;
  static MObject aOutput;
};
#endif

This should look very similar to the command that we created earlier. Instead of a doIt function, we are implementing a compute function. We also have some static member variables. The MObjects represent the attributes that the node will have. The “a” prefix is just a naming convention I follow for MObjects that represent node attributes.

The MTypeId member variable is an id value that is necessary in all nodes that you create. All nodes in Maya require a unique hexadecimal id number. From the Maya documentation:

For local testing of nodes you can use any identifier between 0x00000000 and 0x0007ffff, but for any node that you plan to use for more permanent purposes, you should get a universally unique id from Autodesk Technical Support. You will be assigned a unique range that you can manage on your own.

If you are creating nodes at a studio, you should request an id range from Autodesk. If you just use a random id value, there is a chance that it will conflict with an existing node id value and you will see undesirable behavior in Maya.

Attributes

Nodes can have input and output attributes of many different types from numerical attributes like integers, floats, and booleans, to more complex types like meshes, curves, numerical arrays, etc. When implementing a new node, we need to define all of the attributes of that node as well as which attributes will trigger dirty flag propagation. We do this in an initialization function that we specify when we register the node with Maya. Here is the initialization function for our DoublerNode:

MStatus DoublerNode::initialize() {
  MFnNumericAttribute nAttr;

  aOutput = nAttr.create("output", "out", MFnNumericData::kFloat);
  nAttr.setWritable(false);
  nAttr.setStorable(false);
  addAttribute(aOutput);

  aInput = nAttr.create("input", "in", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aInput);
  attributeAffects(aInput, aOutput);

  return MS::kSuccess;
}

Since our node contains only numeric attributes, we create them with MFnNumericAttribute. There are different classes to create different types of attributes; these include:

We will go over each of these in later sections as they are encountered.

When creating an attribute, we need to specify various options for the attribute such as whether the attribute is an input or output, whether it is keyable, an array, cached, stored when the file is saved, etc. In the above example, by setting the aOutput attribute to not writable, we are specifying that it can never be set with a setAttr command and that it cannot be used as a destination connection; basically that the attribute is an output attribute and only the node itself should set its value. By setting storable to false, we are telling Maya not to store this value in the .mb or .ma file when the scene is saved. This makes sense because since it is an output attribute, the value will get calculated anyways so there is no need to store it to disk.

The aInput attribute is then created and set to keyable. When an attribute is keyable, it appears in the channel box when the node is selected. To read about all the options you can set for an attribute, refer to the MFnAttribute documentation. Once the aInput attribute is created and added to the node, we then specify that the aInput attribute affects the aOutput attribute by calling attributeAffects. This creates the input/output relationship that tells Maya that when aInput changes, it should mark aOutput as dirty and that Maya will need to re-evaluate it the next time it is requested. You will need to call this function for every attribute dependency in your node.

Plugs

The Maya API has the notion of attributes and plugs. Scripters are probably familiar with attributes being the collection of values and options available on a node. In the API, attributes refer to the data interface that defines a node and that is shared across nodes of the same type. For example, all polySphere nodes have a radius attribute. The actual data or value of the attribute is stored in a plug. The actual C++ class is MPlug. So attributes are shared across all nodes of the same type and plugs are unique to a single instance of a node. When talking about attributes and plugs throughout this workshop, I may refer to them interchangeably, but technically speaking, there is a difference between the two.

Data Blocks and Data Handles

When creating a new node, there are two objects we need to be aware of. These are MDataBlock and MDataHandle. The data block is a nodes storage object for all the data necessary to compute an output attribute. When calculating a nodes outputs, all attribute values, inputs and outputs, are managed through the data block. This data block object is only valid during the time you are calculating a nodes outputs so you should not store pointers to the data block.

To access data inside of the data block, we use data handles. A data handle is a pointer object to data inside of the data block. The general workflow of working with data blocks and handles is:

  1. Request a data handle from the data block to a specific attribute. Maya provides the data block.
  2. Read the data from the data handle.
  3. Perform our calculation.
  4. Request an output data handle so we can store our outputs into the data block.
  5. Store our calculated data into the data block using our output data handle.

There are also cases where we are reading array data from an attribute. In these cases, we use the MArrayDataHandle object. We will learn more about this class in later sections.

When calculating an output attribute, we should only use the data provided to the node through its data block. This includes any input attributes and connections. We should never look outside of the node to get data required for our calculations. For example, say we write a node and we want normalized painted weight maps such as on a skin cluster node. In the computation of our output plugs in our node, we should not find the skin cluster node in the DG and query its weight values. If you want these weight values, you should either copy these weights into your nodes data block with a separate command, create your own painted weight normalization algorithms, or connect all the painted weight plugs to your node. If you look outside of your node while your node is computing, you may trigger unwanted node evaluations. A node should behave as a black box and only have knowledge of its inputs and outputs.

The Compute Function

With the node attribute interface defined in the initialize function, we can now implement the actual calculation of the output values. This is done in a nodes compute method. Whenever Maya requests an output value from a node and the output value is dirty, Maya will call that nodes compute function to recalculate the output value. In our node, we simply get the input value, double it, and set it as the output:

MStatus DoublerNode::compute(const MPlug& plug, MDataBlock& data) {
  if (plug != aOutput) {
    return MS::kUnknownParameter;
  }
  // Get the input
  float inputValue = data.inputValue(aInput).asFloat();

  // Double it
  inputValue *= 2.0f;

  // Set the output
  MDataHandle hOutput = data.outputValue(aOutput);
  hOutput.setFloat(inputValue);
  data.setClean(plug);
  return MS::kSuccess;
}

This compute functions performs the same workflow steps previously outlined. We get the input data handle and the input value both on the same line. We then perform our calculation, get the output data handle, and then store the result back into the data block. Once the output has been calculated and stored, we need to mark the output plug as clean so Maya knows not to calculate its value again.

The initial conditional statement checks which output plug that Maya is currently requesting. This is to ensure we do not execute unnecessary calculations and lets us filter our computations when a node has multiple output attributes. When everything is calculated and stored back into the data block, we return a success code to tell Maya that nothing went wrong.

The entire plug-in for the DoublerNode is listed below:

#ifndef DOUBLERNODE_H
#define DOUBLERNODE_H

#include 
#include 
#include 
#include 

#include 

class DoublerNode : public MPxNode {
 public:
  DoublerNode() {}
  virtual MStatus compute(const MPlug& plug, MDataBlock& data);
  static void* creator();
  static MStatus initialize();

  static MTypeId id;
  static MObject aInput;
  static MObject aOutput;
};
#endif
#include "include/DoublerNode.h"
#include 

MTypeId DoublerNode::id( 0x00000001 );
MObject DoublerNode::aInput;
MObject DoublerNode::aOutput;

void* DoublerNode::creator() { return new DoublerNode; }
MStatus DoublerNode::compute(const MPlug& plug, MDataBlock& data) {
  if (plug != aOutput) {
    return MS::kUnknownParameter;
  }
  // Get the input
  float inputValue = data.inputValue(aInput).asFloat();

  // Double it
  inputValue *= 2.0f;

  // Set the output
  MDataHandle hOutput = data.outputValue(aOutput);
  hOutput.setFloat(inputValue);
  data.setClean(plug);
  return MS::kSuccess;
}

MStatus DoublerNode::initialize() {
  MFnNumericAttribute nAttr;

  aOutput = nAttr.create("output", "out", MFnNumericData::kFloat);
  nAttr.setWritable(false);
  nAttr.setStorable(false);
  addAttribute(aOutput);

  aInput = nAttr.create("input", "in", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aInput);
  attributeAffects(aInput, aOutput);

  return MS::kSuccess;
}

MStatus initializePlugin(MObject obj) {
  MStatus status;
  MFnPlugin plugin(obj, "Chad Vernon", "1.0", "Any");

  status = plugin.registerNode("doublerNode", DoublerNode::id, DoublerNode::creator, DoublerNode::initialize);
  CHECK_MSTATUS_AND_RETURN_IT(status);

  return status;
}

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

  status = plugin.deregisterNode(DoublerNode::id);
  CHECK_MSTATUS_AND_RETURN_IT(status);

  return status;
}

And here is the corresponding Python implementation:

import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya

class DoublerNode(OpenMayaMPx.MPxNode):
    kPluginNodeId = OpenMaya.MTypeId(0x00000001)

    aInput = OpenMaya.MObject()
    aOutput = OpenMaya.MObject()

    def __init__(self):
        OpenMayaMPx.MPxNode.__init__(self)

    def compute(self, plug, data):
        if plug != DoublerNode.aOutput:
            return OpenMaya.MStatus.kUnknownParameter

        inputValue = data.inputValue(DoublerNode.aInput).asFloat()
        inputValue *= 2.0
        hOutput = data.outputValue(DoublerNode.aOutput)
        hOutput.setFloat(inputValue)
        data.setClean(plug)

        return OpenMaya.MStatus.kSuccess

def creator():
    return OpenMayaMPx.asMPxPtr(DoublerNode())

def initialize():
    nAttr = OpenMaya.MFnNumericAttribute()

    DoublerNode.aOutput = nAttr.create('output', 'out', OpenMaya.MFnNumericData.kFloat)
    nAttr.setWritable(False)
    nAttr.setStorable(False)
    DoublerNode.addAttribute(DoublerNode.aOutput)

    DoublerNode.aInput = nAttr.create('input', 'in', OpenMaya.MFnNumericData.kFloat)
    nAttr.setKeyable(True)
    DoublerNode.addAttribute(DoublerNode.aInput)
    DoublerNode.attributeAffects(DoublerNode.aInput, DoublerNode.aOutput)

def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, 'Chad Vernon', '1.0', 'Any')
    try:
        plugin.registerNode('doublerNode', DoublerNode.kPluginNodeId, creator, initialize)
    except:
        raise RuntimeError, 'Failed to register node'

def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterNode(DoublerNode.kPluginNodeId)
    except:
        raise RuntimeError, 'Failed to register node'

When the plug-in is compiled and loaded, we can test it out with the following code:

import maya.cmds as cmds
node = cmds.createNode('doublerNode')
locator = cmds.spaceLocator()[0]
cmds.connectAttr('%s.output' % node, '%s.ty' % locator)

This code creates a douberNode and connects it to the translateY value of a locator. Animating the input of the doublerNode shows the effect of our node.

8 thoughts on “Introduction to Dependency Graph Plug-ins”

Olivier October 7, 2012 at 3:17 am

Thanks !

I had an error: LNK2019: unresolved external symbol. To resolve it, I replace “public: DoublerNode();” in the .h by “public: DoublerNode() {};” as in the previous example

PJ November 12, 2013 at 10:31 pm

Looks like the header file has a slight mistake in the constructor and its missing it’s destructor.
The constructor isn’t defined as a function it needs {} and right below that should be the destructor virtual ~DoublerNode{};

class DoublerNode : public MPxNode
{
public:
    DoublerNode() {};
    virtual   ~DoublerNode() {};
    virtual MStatus compute( const MPlug& plug, MDataBlock& data );
    static  void*   creator();
    static  MStatus initialize();
 
    static MTypeId      id;
    static MObject      aInput;
    static MObject      aOutput;
};

Chad November 12, 2013 at 10:57 pm

Yes the constructor needs an implementation. However there is no dynamically allocated memory, so we do not need an explicit destructor. We can use the one that the compiler will auto generate.

Chris Penny December 11, 2014 at 1:52 pm

First let me say thanks for posting such useful information. It’s much appreciated.
It appears the MStatus was removed from OpenMaya after 2011.

We have removed the MStatus class. Python exceptions must be used instead of MStatus. See Error Conditions and Exceptions vs MStatus for more information.

Even though it’s still in their docs for 2015, it will error with the Module doesn’t have a MStatus. Thanks again for the wonderful post.

Natt June 19, 2016 at 6:22 pm

Thanks so much Chad for this course. I bought the CG Circuit api course and really love the in depth explanation. Great work.

Question though. I followed along to Chapter 3 Creating Dependency Graph Node.
I am using CMake to build my makefile, and the build and compile completed successfully without errors. But I can’t seem to load this gaussianNode into Maya. I’m getting this error:

// Error: line 1: Unable to dynamically load : /home/natt/maya/plug-ins/gaussianplugin.so
/home/natt/maya/plug-ins/gaussianplugin.so: undefined symbol: _ZN12GaussianNodeC1Ev //
// Error: line 1: /home/natt/maya/plug-ins/gaussianplugin.so: undefined symbol: _ZN12GaussianNodeC1Ev (gaussianplugin) //

I am using Maya 2016, and running it on OpenSUSE Leap 42.1 (x86_64).
Since I’m not getting any errors on the make and it successfully made the .so file, you have any suggestions as to why Maya doesn’t load the plug-in?

Thanks in advance.

Chad June 25, 2016 at 9:56 pm

Hi Natt,
Do you have multiple versions of Maya installed? Make sure you are compiling with the same version.

Natt August 18, 2016 at 10:35 pm

Hey Chad, thanks for your response, sorry I missed your reply.

I originally have Maya 2012 installed and then removed/upgrade to 2016. I don’t see a 2012 files/installation lingering around, so I assume cmake will be compiling against my 2016 version, especially when my cmake command to build is:

cmake -G “Unix Makefiles” -DMAYA_VERSION=2016

My simple command plug-in works, and loaded without issue (which included MPxCommand, MGlobal, and MObject), but this one has MPxNode and MFnNumericAttribute. Not sure what’s happening here but is it that on Linux I would need a certain version of gcc?
Mine is gcc (SUSE Linux) 4.8.5

I tried various combination of flag options in cmake and this is what I have and it didn’t help. Again, it cmake build without errors to generate the make files, and running cmake –build did not produce any errors.
set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -lOpenMayaAnim -fPIC -Wall -Werror -Wextra”)

Thanks again for your time!

Natt

Grace November 17, 2016 at 2:40 pm

Hi Chad! Thanks for the awesome tutorial!

I was working through it and I keep getting the following error:
Severity Code Description Project File Line Suppression State
Error LNK2001 unresolved external symbol “public: virtual class MStatus __cdecl MPxNode::dependsOn(class MPlug const &,class MPlug const &,bool &)const ” (?dependsOn@MPxNode@@UEBA?AVMStatus@@AEBVMPlug@@0AEA_N@Z) DependencyGraphRound2 c:\Users\Grace\documents\visual studio 2015\Projects\DependencyGraphRound2\DependencyGraphRound2\DoublerNode.obj 1

I am using Maya 2016, I would love your advice on the issue!

Thanks!

Leave A Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

<code> For inline code.
[sourcecode] For multiline code.