Dependency Graph Plug-ins

 
 
 

In the following topic, we outline the basic code structure of a Dependency Graph plug-in.

Sample Dependency Graph Plug-in

The following Python code is a sample Dependency Graph plug-in. This sample defines a utility (utility/general) Hypershade node named myNodeName. It has one input attribute which accepts a float (decimal number), and one output attribute which outputs the inputted float. This node can be created using cmds.createNode( 'myNodeName' ) or cmds.shadingNode( 'myNodeName', asUtility=True ). If it is created with the later command, or manually via the Maya Hypershade user interface (filter by typing 'My Node Name' in the text box), an instance of this node will appear in the Window > Rendering Editors > Hypershade > 'Utilities' Tab.

# sampleDGNode.py

import sys
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
# ... additional imports here ...

# Plug-in information:
kPluginNodeName = 'myNodeName'              # The name of the node.
kPluginNodeClassify = 'utility/general'     # Where this node will be found in the Maya UI.
kPluginNodeId = OpenMaya.MTypeId( 0x87EFE ) # A unique ID associated to this node type.

# Default attribute values
sampleDefaultValue = 1

##########################################################
# Plug-in 
##########################################################
class myNode(OpenMayaMPx.MPxNode):
    # Static variables which will later be replaced by the node's attributes.
    sampleInAttribute = OpenMaya.MObject()
    sampleOutAttribute = OpenMaya.MObject()
    
    def __init__(self):
        ''' Constructor. '''
        OpenMayaMPx.MPxNode.__init__(self)
        
    def compute(self, pPlug, pDataBlock):
        '''
        Node computation method.
            - pPlug: A connection point related to one of our node attributes (could be an input or an output)
            - pDataBlock: Contains the data on which we will base our computations.
        '''
        
        if( pPlug == myNode.sampleOutAttribute ):
            
            # Obtain the data handles for each attribute
            sampleInDataHandle = pDataBlock.inputValue( myNode.sampleInAttribute )
            sampleOutDataHandle = pDataBlock.outputValue( myNode.sampleOutAttribute )
            
            # Extract the actual value associated to our sample input attribute (we have defined it as a float)
            sampleInValue = sampleInDataHandle.asFloat()
            
            
            # ... perform the desired computation ...
            
            # Set the output value.
            sampleOutDataHandle.setFloat( sampleInValue ) #  As an example, we just set the output value to be equal to the input value.
            
            # Mark the output data handle as being clean; it need not be computed given its input.
            sampleOutDataHandle.setClean()
             
        else:
            return OpenMaya.kUnknownParameter

##########################################################
# Plug-in initialization.
##########################################################
def nodeCreator():
    ''' Creates an instance of our node class and delivers it to Maya as a pointer. '''
    return OpenMayaMPx.asMPxPtr( myNode() )

def nodeInitializer():
    ''' Defines the input and output attributes as static variables in our plug-in class. '''
    
    # The following MFnNumericAttribute function set will allow us to create our attributes.
    numericAttributeFn = OpenMaya.MFnNumericAttribute()
    
    #==================================
    # INPUT NODE ATTRIBUTE(S)
    #==================================
    global sampleDefaultValue
    myNode.sampleInAttribute = numericAttributeFn.create( 'myInputAttribute', 'i', 
                                                          OpenMaya.MFnNumericData.kFloat,
                                                          sampleDefaultValue )
    numericAttributeFn.setWritable( True )
    numericAttributeFn.setStorable( True ) 
    numericAttributeFn.setHidden( False )
    myNode.addAttribute( myNode.sampleInAttribute ) # Calls the MPxNode.addAttribute function.
    
    #==================================
    # OUTPUT NODE ATTRIBUTE(S)
    #==================================
    myNode.sampleOutAttribute = numericAttributeFn.create( 'myOutputAttribute', 'o',
                                                           OpenMaya.MFnNumericData.kFloat )
    numericAttributeFn.setStorable( False )
    numericAttributeFn.setWritable( False )
    numericAttributeFn.setReadable( True )
    numericAttributeFn.setHidden( False )
    myNode.addAttribute( myNode.sampleOutAttribute )
    
    #==================================
    # NODE ATTRIBUTE DEPENDENCIES
    #==================================
    # If sampleInAttribute changes, the sampleOutAttribute data must be recomputed.
    myNode.attributeAffects( myNode.sampleInAttribute, myNode.sampleOutAttribute )
    
    
def initializePlugin( mobject ):
    ''' Initialize the plug-in '''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator,
                              nodeInitializer, OpenMayaMPx.MPxNode.kDependNode, kPluginNodeClassify )
    except:
        sys.stderr.write( 'Failed to register node: ' + kPluginNodeName )
        raise
    
def uninitializePlugin( mobject ):
    ''' Uninitializes the plug-in '''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.deregisterNode( kPluginNodeId )
    except:
        sys.stderr.write( 'Failed to deregister node: ' + kPluginNodeName )
        raise

##########################################################
# Sample usage.
##########################################################
''' 
# Copy the following lines and run them in Maya's Python Script Editor:

import maya.cmds as cmds

cmds.loadPlugin( 'sampleDGNode.py' )
cmds.createNode( 'myNodeName' )
# ...

'''

Maya Plug-in Entry and Exit Points

Dependency Graph plug-ins require the same entry and exit point functions as Command Plug-ins, namely: initializePlugin() and uninitializePlugin(). These two functions are respectively invoked when Maya loads and unloads the plug-in. Your plug-in will fail to load if these two functions are absent in your file. Dependency Graph plug-ins are used to create new node types by inheriting from MPxNode or one of its many subclasses. In the sample contained within this topic, we are inheriting directly from MPxNode, so we must register our node via MFnPlugin.registerNode().

def initializePlugin( mobject ):
    ''' Initialize the plug-in '''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator,
                              nodeInitializer, OpenMayaMPx.MPxNode.kDependNode, kPluginNodeClassify )
    except:
        sys.stderr.write( 'Failed to register node: ' + kPluginNodeName )
        raise
    
def uninitializePlugin( mobject ):
    ''' Uninitializes the plug-in '''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.deregisterNode( kPluginNodeId )
    except:
        sys.stderr.write( 'Failed to deregister node: ' + kPluginNodeName )

You will notice that our call to MFnPlugin.registerNode() contains six parameters. We describe these parameters as follows:

  1. Node Name - The name of the node. We specify this value in the arbitrarily named variable kPluginNodeName. This variable is also used in uninitializePlugin(), as well as in our error-reporting calls to sys.stderr.write().

    kPluginNodeName = 'myNodeName'              # The name of the node.

    By registering this node name, we can create an instance of node using the following Python command:

    import maya.cmds as cmds
    cmds.createNode( 'myNodeName' )
  2. Node ID - A unique identification number to represent our node's type (MTypeId) among all the other node types registered in Maya. We specify this value with the arbitrarily named variable kPluginNodeId.

    kPluginNodeId = OpenMaya.MTypeId( 0x87EFE ) # A unique ID associated to this node type.

    It is important that this MTypeId value be unique; when Maya attempts to save and load files containing our nodes, it will label our nodes using this unique ID. If the unique ID of loaded nodes is not registered in Maya (perhaps because our plug-in isn't loaded), these nodes will fail to appear in the Dependency Graph. As explained in more detail in the MTypeId API documentation, specific numeric ranges of MTypeId are reserved for different purposes. We present a brief summary of these ranges in the table below:

    Reserved MTypeId Range Description
    0x00000 - 0x7ffff Plug-ins which are not intended to be widely distributed. This range is suitable for internally-developed plug-ins.
    0x80000 - 0xfffff Example plug-ins provided with Maya and its API documentation.
    NoteIf you intend to publically distribute a Dependency Graph plug-in you have written, it will require a globally unique ID. The Autodesk Developer Network will provide such id's in blocks of 64, 128, 256 or 512. You will be assigned one or more 24-bit prefixes. Once this has been obtained, use the MTypeId constructor which takes two unsigned int parameters. The prefix goes in the first parameter, and you are responsible for managing the allocation of the IDs that go into the second parameter.
  3. Node Creation Function Reference - A reference to the arbitrarily named nodeCreator() function. This function is responsible for creating an instance of our node and returning it as a valid Maya object pointer.

    def nodeCreator():
        ''' Creates an instance of our node class and delivers it to Maya as a pointer. '''
        return OpenMayaMPx.asMPxPtr( myNode() )
    
  4. Node Attribute Initialization Function Reference - A reference to the arbitrarily named nodeInitializer() function. This function is responsible for creating the node's set of input and output attributes.

    def nodeInitializer():
        ''' Defines the input and output attributes as static variables in our plug-in class. '''
        
        # The following MFnNumericAttribute function set will allow us to create our attributes.
        numericAttributeFn = OpenMaya.MFnNumericAttribute()
        
        #==================================
        # INPUT NODE ATTRIBUTE(S)
        #==================================
        global sampleDefaultValue
        myNode.sampleInAttribute = numericAttributeFn.create( 'myInputAttribute', 'i', 
                                                              OpenMaya.MFnNumericData.kFloat,
                                                              sampleDefaultValue )
        numericAttributeFn.setWritable( True )
        numericAttributeFn.setStorable( True ) 
        numericAttributeFn.setHidden( False )
        myNode.addAttribute( myNode.sampleInAttribute ) # Calls the MPxNode.addAttribute function.
        
        #==================================
        # OUTPUT NODE ATTRIBUTE(S)
        #==================================
        myNode.sampleOutAttribute = numericAttributeFn.create( 'myOutputAttribute', 'o',
                                                               OpenMaya.MFnNumericData.kFloat )
        numericAttributeFn.setStorable( False )
        numericAttributeFn.setWritable( False )
        numericAttributeFn.setReadable( True )
        numericAttributeFn.setHidden( False )
        myNode.addAttribute( myNode.sampleOutAttribute )
        
        #==================================
        # NODE ATTRIBUTE DEPENDENCIES
        #==================================
        # If sampleInAttribute changes, the sampleOutAttribute data must be recomputed.
        myNode.attributeAffects( myNode.sampleInAttribute, myNode.sampleOutAttribute )

    We use the MFnNumericAttribute function set to create instances of MObject which encapsulate numeric attributes. These attributes require a long and short name, as well as a type and an optional default value. We assign the created attributes as static variables of the myNode class, for example: myNode.sampleInAttribute. Assigning these objects as static variables allows us to refer to them in the myNode.compute() method.

    The created attributes are also added to the node's base class via MPxNode.addAttribute(). This makes the node's attributes accessible in the Dependency Graph.

    The MFnNumericAttribute.setWritable() function allows the attribute to be connected as a destination to an input source. The MFnNumericAttribute.setStorable() allows the value associated with the attribute to be saved to a file. The MFnNumeric.setReadable() function allows the value associated with this attribute to be used as input to another node's writable attribute(s). The MFnNumeric.setHidden() attribute hides the attribute in the user interface so it cannot be mistakenly connected.

    The MPxNode.attributeAffects() function ensures that if the value associated with the given input changes, the node's compute() function will be called to recompute the value of the output attribute. For more information, see The Dependency Graph.

  5. Node Type - The node type defined by the plug-in. By default, this is set to MPxNode.kDependNode, but other types, such as MPxNode.kLocatorNode, MPxNode.kDeformerNode, MPxNode.kSurfaceShape, etc, can be specified to define different behaviors in the Dependency Graph.

  6. Node Classification - The node classification is a string which determines how the node will be categorized in various rendering-related user interfaces, such as the Create Render Node and the HyperShade windows. If no string is supplied, the node will not be considered as a rendering node, and will therefore not be displayed in these user interfaces.

    kPluginNodeClassify = 'utility/general'     # Where this node will be found in the Maya UI.

    The following table presents a set of valid rendering node classifications:

    Category Classification String
    2D Textures 'texture/2d'
    3D Textures 'texture/3d'
    Env Textures 'texture/environment'
    Surface Materials 'shader/surface'
    Volumetric Materials 'shader/volume'
    Displacement Materials 'shader/displacement'
    Lights 'light'
    General Utilities 'utility/general'
    Color Utilities 'utlity/color'
    Particle Utilities 'utility/particle'
    Image Planes 'imageplane'
    Glow 'postprocess/opticalFX'

Node Behavior

The behavior of our Dependency Graph node (myNode) is defined in a class which inherits from MPxNode.

class myNode(OpenMayaMPx.MPxNode):
    # Static variables which will later be replaced by the node's attributes.
    sampleInAttribute = OpenMaya.MObject()
    sampleOutAttribute = OpenMaya.MObject()
    
    def __init__(self):
        ''' Constructor. '''
        OpenMayaMPx.MPxNode.__init__(self)
        
    def compute(self, pPlug, pDataBlock):
        '''
        Node computation method.
            - pPlug: A connection point related to one of our node attributes (could be an input or an output)
            - pDataBlock: Contains the data on which we will base our computations.
        '''
        
        if( pPlug == myNode.sampleOutAttribute ):
            
            # Obtain the data handles for each attribute
            sampleInDataHandle = pDataBlock.inputValue( myNode.sampleInAttribute )
            sampleOutDataHandle = pDataBlock.outputValue( myNode.sampleOutAttribute )
            
            # Extract the actual value associated to our sample input attribute (we have defined it as a float)
            sampleInValue = sampleInDataHandle.asFloat()
            
            
            # ... perform the desired computation ...
            
            # Set the output value.
            sampleOutDataHandle.setFloat( sampleInValue ) #  As an example, we just set the output value to be equal to the input value.
            
            # Mark the output data handle as being clean; it need not be computed given its input.
            sampleOutDataHandle.setClean()
             
        else:
            return OpenMaya.kUnknownParameter

The node attributes added with MPxNode.addAttribute() in nodeInitializer() are used by Maya to define the input and output connection points of our node when it is instantiated in the Dependency Graph.

These connection points are known as plugs, and are represented by instances of MPlug. The call to MPxNode.attributeAffects() in nodeInitializer() determines the causal input > output link between these plugs. Ultimately, the Dependency Graph keeps track of the directed edges of connected plugs among all the nodes. During Maya's ongoing Dependency Graph evaluation process, if data associated to a plug is changed, this plug is marked as dirty, meaning that its cached value is stale and must be recomputed. The plug hierarchy is recursively traversed to mark the remaining affected plugs as dirty.

Maya then calls the compute() method on each node containing a dirty plug, passing an instance of MPlug as well as MDataBlock. The related attribute of the passed MPlug should be checked to determine what to compute. This is done using MPlug's overloaded equality operator '=='.

The passed MDataBlock instance contains MDataHandle objects, which store the values associated to their respective attributes. The float input value is obtained as follows:

sampleInDataHandle = pDataBlock.inputValue( myNode.sampleInAttribute )
sampleInValue = sampleInDataHandle.asFloat()

The compute() method should conclude by setting a newly computed value to the correct output MDataHandle. The call to MDataHandle.setClean() removes the dirty flag from its corresponding plug.

If the plug passed into the compute() function is not related to an output attribute we can compute, the function should return OpenMaya.kUnknownParameter. This lets Maya know that the plug's value should be computed using one of our base class's compute() functions.

Creative Commons License Except where otherwise noted, this work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License