Maya API Programming

Check out my Introduction to the Maya API video series at CGCircuit:

Introduction

This workshop is geared towards individuals wanting to learn how to extend and customize Maya with the Maya API. Individuals should have existing C++ and/or Python experience as well as an intermediate to advanced level of knowledge of Maya. A good understanding of Object Oriented Programming (OOP) is extremely helpful as the Maya API makes heavy use of OOP. You will not learn everything about the Maya API from this workshop. The purpose of this workshop is not to make you expert Maya programmers, but to give you a solid foundation from which to further your Maya API studies.

The techniques and code included in this workflow may not be perfect or accepted by elite Maya API programmers as the best way to utilize the API. The code and knowledge presented here is based off of my experience in creating dozens of nodes, deformers, and tool sets using the Maya API in both C++ and Python in the production of animated and effects heavy feature films a large studios. If you think something is incorrect, let me know.  Maya API learning resources are limited so hopefully these notes will help you add to your tool set.

What is the Maya API?

The Maya API is a C++/Python API that lets programmers and scripters access the internal libraries of Maya. With the Maya API, programmers can customize Maya with new technology and create tools to help integrate the software into a studios production pipeline. Tasks written with the Maya API execute several times faster than the same tasks written in MEL.

What Can Be Implemented with the Maya API?

The Maya API is traditionally used to make plug-ins, which are dynamic libraries loaded by Maya at runtime. Plug-ins contain implementations of many different types of objects you want to add to Maya. When I refer to “objects” here, I am referring to objects in the Object Oriented Programming sense. The Maya API provides several base classes that programmers will inherit from and fill in the desired implementations. Some possible object types are:

  • Rendering viewports.
  • Texture baking engines.
  • Commands
  • Constraints
  • Deformers
  • Particle and fluid emitters
  • Shaders
  • IK solvers
  • Dependency nodes
  • OpenGL locators
  • File exporters
  • Tools

Starting with Maya 8.5, the Maya API became accessible via Python. With Python, not only can we make plug-ins as described above, but we can also access API commands in scripts, which adds a significant performance gain to existing toolsets.

C++ vs. Python

Plug-ins can be made with both C++ and Python. So which one should you use? Both are useful but there are situations where one should be used over the other.

Anything complex or that works with larger data sets like deformers should probably be made with C++ for speeds sake. For simple nodes that are not performance critical, Python works fine. Anything dealing with OpenGL such as viewports and locators should be made with C++ as I have seen significant slowdowns with Python implementations.

Also, some API calls in Python are a total pain syntax-wise because the API expects a lot of C++ pointers and references, which is wrapped with a very cryptic module in Python (MScriptUtil) which is not documented very well.

I have used both C++ and Python when developing plug-ins. When I am writing a new node, I sometimes start with Python to work out algorithm details. Since Python isn’t compiled like C++, the iteration time is faster. There’s also less of a chance to crash Maya with array index and memory errors.

Most of the time, I stick with C++ just because that is what the end product is going to be. However, I use the API a lot with Python in scripts so it is still worth learning. All the API calls are the same; it is just a difference in syntax.

Setting Up Your Development Environment

The first step in learning the API is setting up your build environment. The Maya API is packaged as a set of libraries that you will need to access. From the documentation, these libraries are:

  • OpenMaya - Contains fundamental classes for defining nodes and commands and for assembling them into a plug-in.
  • OpenMayaUI - Contains classes necessary for creating new user interface elements such as manipulators, contexts, and locators.
  • OpenMayaAnim - Contains classes for animation, including deformers and inverse kinematics.
  • OpenMayaFX - Contains classes for Autodesk Dynamics.
  • OpenMayaRender - Contains classes for performing rendering functions.

Python

If you are using Python, accessing these libraries is as simple as importing them into your code:

import maya.OpenMaya
import maya.OpenMayaUI
import maya.OpenMayaAnim
import maya.OpenMayaFX
import maya.OpenMayaRender

C++

I recommend using CMake to help create your build environment on Windows, Linux, and OSX. You can view a video series I created going over how to use CMake with Maya. The corresponding CMake modules can be found on my github project.

The Maya Dependency Graph

The rest of this page describes the older pre-Maya 2016 evaluation model with dirty propagation.

When writing most Maya plug-ins, knowledge of the Maya Dependency Graph architecture is necessary.  Explanations of the Maya DG can be found in most Maya books and the documentation.  It is basically a node network that passes information from one node to the next.  The most important aspects for a developer to know about the DG are how and when Maya recalculates and propagates data through the graph.

Nodes have a set of inputs and outputs.  The outputs depend on the values of the inputs.  These outputs are said to be dependents of their corresponding inputs and the inputs are said to affect the corresponding outputs.   As an optimization, Maya’s DG is designed to only calculate data when it needs to.  It accomplishes this with a dirty flag on a node’s inputs and outputs.  When an input value is changed, any dependent output is marked dirty.  The output is not actually recalculated.  Any connection to this output is marked dirty and the dirty propagation continues until the end of the graph is reached.  No data recalculation has occurred yet; only the dirty flag has been pushed through the DG.  When Maya requests a DG reevaluation, such as in a screen refresh, Maya will check if a node output is dirty.  If it is, Maya tells the node to evaluate itself.   When that node evaluates, it sees that its inputs are dirty and so asks any connected input node to reevaluate.  This process occurs until the data in the graph is reevaluated and the inputs and outputs are marked clean.

This knowledge is useful when trying to figure out why your node is not being computed.  If a node is not being computed, its outputs are not being requested.

Since descriptions of the Maya DG architecture are already discussed in depth else where, I will not reproduce it here.  I recommend reading more about Maya's dependency graph architecture in the documentation and in various other Maya books and white papers.

Introduction to the Maya API

Object Types

The Maya API consists of four types of C++ objects: wrappers, objects, function sets, and proxies.

Wrappers

A wrapper is simply a convenience class for a data or mathematical structure. The Maya API includes wrappers for arrays, vectors, matrices, quaternions, etc. Wrappers also include utility tools such as selection lists, DAG paths, viewports, etc. These classes start with the “M” prefix. Examples include MIntArray, MFloatArray, MMatrix, MVector, MQuaternion, MPlane, MPointArray, MSelectionList, etc.

Wrappers also include iterators, which are classes used to traverse over sequences of data. There are iterators to traverse Maya's dependency graph, to iterate over a meshes vertices, edges, and faces, to iterate over a surfaces cvs, etc. Iterator classes are prefixed with “MIt” and include MItMesh, MItDependencyGraph, MItKeyFrame, MItMeshEdge, MItSurfaceCV, etc.

Objects

An object, known as an MObject, is the generic base class that represents all Maya objects (curves, surfaces, DAG nodes, DG nodes, lights, shaders, textures, IK solvers, dynamics fields, etc). This common base allows many different types of MObjects to be passed through the API and lets API developers take advantage of the inheritance structure of all the nodes in Maya. Each instance of an MObject represents either a node or an attribute on a node. However, an MObject is not really the actual node or attribute, but rather a handle to the node or attribute. This is so Maya can maintain ownership of the nodes and attributes and make sure it runs all the necessary routines to keep Maya running smoothly. Because an MObject is a pointer to an internal Maya object, you should not hang on to MObjects between calls to your plug-in as Maya may move these internal objects around in memory making your MObject invalid. To access specific functionality of the data that an MObject represents, we use function sets.

Function Sets

Because MObjects are the base class for all nodes, it is not practical to include all the functions necessary to interact with the node in the MObject class. The functions that operate on a specific type of node are broken into C++ classes called function sets. For example there are function sets for cameras, skin clusters, IK handles, meshes, surfaces, transforms, etc. Function sets operate on MObjects. The general workflow for objects and function sets is to determine what function set an object is compatible with, using the MFn::Type enumeration, and then attaching that function set to the object:

if (obj.hasFn(MFn::kMesh)) {
  MFnMesh fnMesh(obj);
}
if obj.hasFn(OpenMaya.MFn.kMesh):
    fnMesh = OpenMaya.MFnMesh(obj)

Often, you will already know the type of data that an MObject represents and won’t need to explicitly check the type of data. Function set classes are prefixed with “MFn”. Examples include MFnMesh, MFnNurbsCurve, MFnDagNode, MFnLatticeDeformer, MFnRenderPass, etc.

The object/function set workflow creates a separation of data vs. functionality, which differentiates from classic OOP where a class usually encapsulates both data and functionality to operate on the data. In Maya, data is represented with MObjects and functionality is achieved with function sets.

Proxies

Proxies are the abstract classes that plug-in developers use to implement new types of Maya objects such as custom nodes, deformers, ik solvers, commands, transforms, etc. When developing a new object, we create a new class that inherits from one of these proxy classes and then implement all the required functions. Proxy classes are prefixed with “MPx” and include MPxNode, MPxDeformerNode, MPxConstraint, MPxCommand, MPxIkSolver, etc.

Error Checking

Error checking in C++ is done with the MStatus class. Most functions in the API accept or return an MStatus variable. We can query this variable to determine if a specific function succeeded or not. If a function fails, the MStatus variable contains information about why the function failed. It is important to check for these errors because when functions fail in the Maya API, they fail silently. There will be no warning, the rest of the code may just not work or Maya might just crash. Therefore, you should always check if a function succeeded with MStatus.

// oMesh is an MObject for a mesh object and so can’t be used with
// the NURBS surface function set
MFnNurbsSurface fnSurface(oMesh, &status);
if (status.error()) {
  std::cerr << "nAPI error detected in " << __FILE__ << " at line " << __LINE__ << endl;
  status.perror("Something went wrong!");
}

The Maya API comes with a couple of useful macros for checking MStatus variables. These are:

CHECK_MSTATUS(status);
CHECK_MSTATUS_AND_RETURN_IT(status);

The CHECK_MSTATUS macro performs the same functionality as above. The CHECK_MSTATUS_AND_RETURN_IT macro is the same as CHECK_MSTATUS except it returns the status if it failed.

MStatus is not available with Python. When using the Maya API with Python, a failed function will throw an exception and you can use Python’s built-in error handling to deal with it.

try:
    fnSurface = OpenMaya.MFnNurbsSurface(oMesh)
except:
    print("Something went wrong!")

Your First Plug-in

As an introduction to the Maya API, we will create a simple command plug-in.  This will show you template code that is common in all plug-ins as well as display various Maya API concepts discussed earlier.  The command will simply print text into the script editor.

In all plug-ins, you need to implement 3 common functions:

  • initializePlugin – Called when the plug-in is loaded.  Used to register new commands, tools, nodes, etc with Maya.
  • uninitializePlugin – Called when the plug-in is unloaded.  Used to de-register whatever was registered in initializedPlugin.
  • creator – Maya calls this to create a new instance of the object, such as when createNode is called or the command is called.

These functions look like the following in our HelloWorld command:

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

MStatus initializePlugin(MObject obj) {
  MFnPlugin plugin(obj, "Chad Vernon", "1.0", "Any");
  MStatus status = plugin.registerCommand("helloWorld", HelloWorld::creator);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  return status;
}

MStatus uninitializePlugin(MObject obj) {
  MFnPlugin plugin(obj);
  MStatus status = plugin.deregisterCommand("helloWorld");
  CHECK_MSTATUS_AND_RETURN_IT(status);
  return status;
}

This is boiler plate code that is usually copied and pasted into all of my plug-ins and altered just slightly to include all the nodes/commands/deformers included in the plug-in.  The code simply registers and deregisters any new nodes, commands, etc. that our plug-in contains.  This is also where we tell Maya what type of node we are creating, such as if it is a normal dependency node, a deformer, a custom locator, an ik solver, a constraint, etc.

You can read about the specifics of these functions in the documentation, but this is where we can specify a plug-in version number, a required Maya version, plug-in author, etc.

Since we are implementing a new command, we need to create a class that inherits from the proxy class, MPxCommand:

#ifndef HELLOWORLD_H
#define HELLOWORLD_H

#include <maya/MArgList.h>
#include <maya/MObject.h>
#include <maya/MGlobal.h>
#include <maya/MPxCommand.h>

class HelloWorld : public MPxCommand {
public:
  HelloWorld() {};
  virtual MStatus doIt(const MArgList&);
  static void* creator();
};
#endif

The above code shows the header declaration for our HelloWorld command.  We begin by including the necessary header files.  Each class in Maya has its own header file, so if you use a specific class in your plug-in, you need to include its header file in order to use it.

The documentation for MPxCommand lists many different functions that we can choose to implement. Since this is our first plug-in, we will only implement the one necessary function for commands: the doIt function.  When we end up calling our new command in Maya, Maya will create our new HelloWorld object and then call the doIt function of that object.  The doIt function looks like this:

MStatus HelloWorld::doIt(const MArgList&) {
  MGlobal::displayInfo("Hello World!");
  return MS::kSuccess;
}

MPxCommand::doIt accepts an MArgList object, which is used for passing in command arguments.  We will learn how to use that in a later section.  The only line of interest in this function is the MGlobal::displayInfo call.  MGlobal is a class that provides lots of useful functionality such as printing text and selecting objects.  Here, we are simply printing some text.

Once the plug-in is compiled and loaded, we can call the command with:

import maya.cmds as cmds
cmds.helloWorld()

The entire plug-in looks like this:

// HelloWorldCmd.h

#ifndef HELLOWORLD_H
#define HELLOWORLD_H

#include <maya/MArgList.h>
#include <maya/MObject.h>
#include <maya/MGlobal.h>
#include <maya/MPxCommand.h>

class HelloWorld : public MPxCommand {
public:
  HelloWorld() {};
  virtual MStatus doIt(const MArgList&);
  static void* creator();
};
#endif
// HelloWorldCmd.cpp

#include "include/HelloWorldCmd.h"
#include <maya/MFnPlugin.h>
void* HelloWorld::creator() {
  return new HelloWorld;
}

MStatus HelloWorld::doIt(const MArgList& argList) {
  MGlobal::displayInfo("Hello World!");
  return MS::kSuccess;
}

MStatus initializePlugin(MObject obj) {
  MFnPlugin plugin(obj, "Chad Vernon", "1.0", "Any");
  MStatus status = plugin.registerCommand("helloWorld", HelloWorld::creator);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  return status;
}

MStatus uninitializePlugin(MObject obj) {
  MFnPlugin plugin(obj);
  MStatus status = plugin.deregisterCommand("helloWorld");
  CHECK_MSTATUS_AND_RETURN_IT(status);
  return status;
}

One thing to keep in mind is that maya/MFnPlugin.h should only be included once in your plug-in.  This header file is necessary to register your plug-in with Maya.  If you include it in multiple source files in your plug-in, you’ll get an error.  If your plug-in has multiple nodes and commands, you’ll probably put the initializePlugin and uninitializePlugin functions in a separate file, so just include MFnPlugin in that file.

The identical Python implementation of the command is:

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

class HelloWorld(OpenMayaMPx.MPxCommand):
        
    def doIt(self, argList):
        print("Hello World!")
        
def creator():
    return OpenMayaMPx.asMPxPtr(HelloWorld())

def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, "Chad Vernon", "1.0", "Any")
    try:
        plugin.registerCommand("helloWorld", creator)
    except:
        raise RuntimeError("Failed to register command")

def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterCommand("helloWorld")
    except:
    raise RuntimeError("Failed to unregister command")

In Python, we can either use the MGlobal class to print text or we can just use the built-in Python print command.

Now that we have introduced the Maya API, we can start learning how to implement more interesting and useful plug-ins.

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:

// DoublerNode.h

#ifndef DOUBLERNODE_H
#define DOUBLERNODE_H
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MStatus.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MPxNode.h>
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. You can request a block of node ids here

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:

// DoublerNode.h

#ifndef DOUBLERNODE_H
#define DOUBLERNODE_H
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MStatus.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MPxNode.h>
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
// DoublerNode.cpp

#include "include/DoublerNode.h"
#include <maya/MFnPlugin.h>
 
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('{}.output'.format(node), '{}.ty'.format(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.

Deformers

Deformers are a subset of dependency graph nodes that perform deformation algorithms on input geometry and outputs the deformed geometry to the output attribute of the deformer.  When you think of an algorithm to move points or cvs around on mesh or surface, you usually want to create a deformer.  Example deformers that I have implemented in the past include wrap, jiggle, displacement, blend shape, and skin sliding deformers.  To create a deformer node, we create a class that inherits from the proxy class, MPxDeformerNode.

Since deformers are a subset of dependency nodes, they can be created with the createNode command just like all other nodes, however, deformers have a lot of special functionality that depends on additional connections made to the deformer node.  These connections get made for you when you use the deformer command.  This functionality includes membership editing and reordering of deformation history.

A deformer has the same structure as a normal dependency node except we implement a deform function instead of a compute function.  To show some of the process involved with making deformers, we will create a simple blend shape node that blends one mesh to another mesh.

As inputs to our deformer, we will need a target mesh and a blend weight value:

MStatus BlendNode::initialize() {
  MFnTypedAttribute tAttr;
  MFnNumericAttribute nAttr;
     
  aBlendMesh = tAttr.create("blendMesh", "blendMesh", MFnData::kMesh);
  addAttribute(aBlendMesh);
  attributeAffects(aBlendMesh, outputGeom);
  
  aBlendWeight = nAttr.create("blendWeight", "bw", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aBlendWeight);
  attributeAffects(aBlendWeight, outputGeom);
 
  // Make the deformer weights paintable
  MGlobal::executeCommand( "makePaintable -attrType multiFloat -sm deformer blendNode weights;" );
 
  return MS::kSuccess;
}

Above is our deformers initialize function where we specify all the attributes for our deformer.  To create a mesh attribute, we use MFnTypedAttribute.  A typed attribute is used to create most attributes of non-simple types like meshes, surfaces, curves, arrays, etc.  After the mesh attribute is added, we set it to affect the output geometry attribute, outputGeom.  All deformers have an outputGeom attribute which is part of the MPxDeformerNode class.  Once the mesh attribute is finished, we create the blend weight attribute which is identical to the float attributes we added in the dependency node example.

The last code of interest in the initialize function is the makePaintable call.  makePaintable is a MEL (and Python) command used to make a particular attribute paintable.  All deformers come with a per-vertex weight attribute so we are simple activating the paintability of that attribute.   With our deformer initialized, we can move on to the deform function.

MStatus BlendNode::deform(MDataBlock& data, MItGeometry& itGeo,
                          const MMatrix &localToWorldMatrix,
                          unsigned int mIndex) {
  MStatus status;
 
  // Get the envelope and blend weight
  float env = data.inputValue(envelope).asFloat();
  float blendWeight = data.inputValue(aBlendWeight).asFloat();
  blendWeight *= env;
 
  // Get the blend mesh
  MObject oBlendMesh = data.inputValue(aBlendMesh).asMesh();
  if (oBlendMesh.isNull()) {
    // No blend mesh attached so exit node.
    return MS::kSuccess;
  }
 
  // Get the blend points
  MFnMesh fnBlendMesh(oBlendMesh, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  MPointArray blendPoints;
  fnBlendMesh.getPoints(blendPoints);
 
  MPoint pt;
  float w = 0.0f;
  for (; !itGeo.isDone(); itGeo.next()) {
    // Get the input point
    pt = itGeo.position();
    // Get the painted weight value
    w = weightValue(data, mIndex, itGeo.index());
    // Perform the deformation
    pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w;
    // Set the new output point
    itGeo.setPosition(pt);
  }
 
  return MS::kSuccess;
}

The deform function is where all the deformation implementation code occurs.  The workflow is similar to normal dependency nodes where we:

  1. Get our inputs from the data block
  2. Perform the deformation
  3. Store the deformed geometry back into the data block.

Our deform function begins with getting the envelope and blend weight input values from the data block.  The envelope is a built-in deformer attribute that you can use as a magnitude multiplier for your deformation.  After we grab the numeric attribute values, we get the blend target mesh.  We need to check if the data is valid because if there is no target mesh connected to the deformer, the MObject will be null and we cannot perform the deformation.  Once we have a valid MObject representing our target mesh, we need to extract its point positions.  We do this by attaching an MFnMesh function set to the MObject.  MFnMesh is the function set used to query, edit, and create polygonal meshes.  Now that we have all the inputs, we can begin the deformation algorithm.

The deform function comes with an MItGeometry parameter that is used to iterate over the components of the input mesh.  If you want to take advantage of deformer set membership, you must use this iterator as it only iterates over the vertices or cvs included in the deformer membership.  You can query the current vertex id with MItGeometry::index().

As we iterate over each vertex, we grab the painted weight value for that vertex.  MPxDeformerNode has a built-in convenience function, weightValue, which we can use to query each painted weight value.  The multiIndex attribute passed into this function is the index of the input geometry.  Some deformers can affect multiple meshes at the same time, each having their own painted weight value map.  This index specifies which index to use.  Most of the time, this is 0.  It is for this reason that all paintable attributes need a parent compound array attribute, which we will learn about in later sections.

After we have the weight value, we can perform the actual deformation, which is on one line.  The blend shape deformation is a simple weighted vector delta addition to the current vertex.  With the new point position calculated, we put it back into the geometry iterator.  Notice we do not have to do any setClean calls.  Unless we add any custom outputs to a deformer, MPxDeformerNode automatically handles that for us when we use the deform function.

You’ll notice that out of all that code, the actual deformation algorithm is just one line.  This is the common case when making plug-ins in the Maya API.  Most of the code is usually all the node setup, event handling, and clean up.  Coming up with the algorithm is almost the easy part.

And here is the full code listing for the blendNode deformer.  Notice when we register the node with Maya, we also specify that it is a deformer node.

// BlendNode.h

#ifndef BLENDNODE_H
#define BLENDNODE_H
 
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MGlobal.h>
#include <maya/MItGeometry.h>
#include <maya/MMatrix.h>
#include <maya/MPointArray.h>
#include <maya/MStatus.h>
 
#include <maya/MFnMesh.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFnTypedAttribute.h>
 
#include <maya/MPxDeformerNode.h>
 
class BlendNode : public MPxDeformerNode {
 public:
  BlendNode() {};
  virtual MStatus deform(MDataBlock& data, MItGeometry& itGeo,
                         const MMatrix &localToWorldMatrix, unsigned int mIndex);
  static void* creator();
  static MStatus initialize();
 
  static MTypeId id;
  static MObject aBlendMesh;
  static MObject aBlendWeight;
};
#endif
// BlendNode.cpp

#include "include/BlendNode.h"
#include <maya/MFnPlugin.h>
 
MTypeId BlendNode::id(0x00000002);
MObject BlendNode::aBlendMesh;
MObject BlendNode::aBlendWeight;
 
void* BlendNode::creator() { return new BlendNode; }
 
MStatus BlendNode::deform(MDataBlock& data, MItGeometry& itGeo,
                          const MMatrix &localToWorldMatrix, unsigned int mIndex) {
  MStatus status;
 
  // Get the envelope and blend weight
  float env = data.inputValue(envelope).asFloat();
  float blendWeight = data.inputValue(aBlendWeight).asFloat();
  blendWeight *= env;
 
  // Get the blend mesh
  MObject oBlendMesh = data.inputValue(aBlendMesh).asMesh();
  if (oBlendMesh.isNull()) {
    // No blend mesh attached so exit node.
    return MS::kSuccess;
  }
 
  // Get the blend points
  MFnMesh fnBlendMesh(oBlendMesh, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  MPointArray blendPoints;
  fnBlendMesh.getPoints(blendPoints);
 
  MPoint pt;
  float w = 0.0f;
  for (; !itGeo.isDone(); itGeo.next()) {
    // Get the input point
    pt = itGeo.position();
    // Get the painted weight value
    w = weightValue(data, mIndex, itGeo.index());
    // Perform the deformation
    pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w;
    // Set the new output point
    itGeo.setPosition(pt);
  }
 
  return MS::kSuccess;
}
 
MStatus BlendNode::initialize() {
  MFnTypedAttribute tAttr;
  MFnNumericAttribute nAttr;
   
  aBlendMesh = tAttr.create("blendMesh", "blendMesh", MFnData::kMesh);
  addAttribute(aBlendMesh);
  attributeAffects(aBlendMesh, outputGeom);
  
  aBlendWeight = nAttr.create("blendWeight", "bw", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aBlendWeight);
  attributeAffects(aBlendWeight, outputGeom);
 
  // Make the deformer weights paintable
  MGlobal::executeCommand("makePaintable -attrType multiFloat -sm deformer blendNode weights;");
 
  return MS::kSuccess;
}
 
MStatus initializePlugin(MObject obj) {
  MStatus status;
  MFnPlugin plugin(obj, "Chad Vernon", "1.0", "Any");
 
  // Specify we are making a deformer node
  status = plugin.registerNode("blendNode", BlendNode::id, BlendNode::creator,
                               BlendNode::initialize, MPxNode::kDeformerNode);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  return status;
}
 
MStatus uninitializePlugin(MObject obj) {
  MStatus     status;
  MFnPlugin plugin(obj);
 
  status = plugin.deregisterNode(BlendNode::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
import maya.cmds as cmds
 
class BlendNode(OpenMayaMPx.MPxDeformerNode):
    kPluginNodeId = OpenMaya.MTypeId(0x00000002)
     
    aBlendMesh = OpenMaya.MObject()
    aBlendWeight = OpenMaya.MObject()
     
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)
 
    def deform(self, data, itGeo, localToWorldMatrix, mIndex):
        envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope
        env = data.inputValue(envelope).asFloat()
        blendWeight = data.inputValue(BlendNode.aBlendWeight).asFloat()
        blendWeight *= env
 
        oBlendMesh = data.inputValue(BlendNode.aBlendMesh).asMesh()
        if oBlendMesh.isNull():
            return OpenMaya.MStatus.kSuccess
 
        fnBlendMesh = OpenMaya.MFnMesh(oBlendMesh)
        blendPoints = OpenMaya.MPointArray()
        fnBlendMesh.getPoints(blendPoints)
 
        while not itGeo.isDone():
            pt = itGeo.position()
            w = self.weightValue(data, mIndex, itGeo.index())
            pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w
            itGeo.setPosition(pt)
            itGeo.next()
 
        return OpenMaya.MStatus.kSuccess
 
def creator():
    return OpenMayaMPx.asMPxPtr(BlendNode())
 
def initialize():
    tAttr = OpenMaya.MFnTypedAttribute()
    nAttr = OpenMaya.MFnNumericAttribute()
     
    BlendNode.aBlendMesh = tAttr.create('blendMesh', 'bm', OpenMaya.MFnData.kMesh)
    BlendNode.addAttribute( BlendNode.aBlendMesh )
     
    outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
    BlendNode.attributeAffects(BlendNode.aBlendMesh, outputGeom)
 
    BlendNode.aBlendWeight = nAttr.create('blendWeight', 'bw', OpenMaya.MFnNumericData.kFloat)
    nAttr.setKeyable(True)
    BlendNode.addAttribute(BlendNode.aBlendWeight)
    BlendNode.attributeAffects(BlendNode.aBlendWeight, outputGeom)
 
    # Make deformer weights paintable
    cmds.makePaintable('blendNode', 'weights', attrType='multiFloat', shapeMode='deformer')
 
def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, 'Chad Vernon', '1.0', 'Any')
    try:
        plugin.registerNode('blendNode', BlendNode.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)
    except:
        raise RuntimeError('Failed to register node')
 
def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterNode(BlendNode.kPluginNodeId)
    except:
        raise RuntimeError('Failed to deregister node')

The only difference besides syntax here is the access of the built-in static variables, outputGeom and envelope, of the proxy class, MPxDeformerNode.  We cannot just use self.outputGeom or self.envelope; instead, we can use similar code:

outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope

You can read more about scripted plug-in workflow in the Maya Python API section of the documentation.

Getting the Input Geometry MObject

Often when we are writing a deformer, we want access to the input geometry MObject in order to get information such as vertex normals, uvs, etc. In the MPxDeformerNode::deform function, all we have available is the MItGeometry iterator which only gives us positional information. We can get the MObject from the datablock but we have to be careful not to trigger any unnecessary dependency graph calculations.

MStatus SomeDeformer::deform( MDataBlock& data, MItGeometry& itGeo, const MMatrix &localToWorldMatrix, unsigned int geomIndex )
{
    MStatus status;
    MArrayDataHandle hInput = data.outputArrayValue(input, &status);
    CHECK_MSTATUS_AND_RETURN_IT( status )
    status = hInput.jumpToElement(geomIndex);
    CHECK_MSTATUS_AND_RETURN_IT(status)
    MObject oInputGeom = hInput.outputValue().child(inputGeom).asMesh();
    MFnMesh fnInputMesh(oInputGeom);
}
def deform(self, data, itGeo, localToWorldMatrix, geomIndex ):
    inputAttribute = OpenMayaMPx.cvar.MPxDeformerNode_input
    inputGeom = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom
    hInput = data.outputArrayValue(inputAttribute)
    hInput.jumpToElement(geomIndex)
    oInputGeom = hInput.outputValue().child(inputGeom).asMesh()
    fnInputMesh = OpenMaya.MFnMesh(oInputGeom)

In MPxDeformerNode, the compute method is already implemented for us. The compute method gets the input geometry for us, creates the geometry interator, and calls the deform method, which is what we implement. Notice when I get the data handles, I use outputArrayValue and outputValue. This prevents Maya from triggering a dirty propagation. If I were to use inputArrayValue and inputValue, Maya would recalculate the input geometry, causing an unnecessary graph evaluation since this was already done in the compute method.

Attribute Editor Templates

Often when we create a node, we want to customize the attribute editor display of the node to be more user friendly. We do this through attribute editor templates. Attribute editor templates are MEL files describing the attribute editor interface for your node. By default, Maya will automatically arrange the attributes of a node in the attribute editor. Attribute editor templates allow us to customize this display. To create an attribute editor template, create a MEL file called AE{nodeName}Template.mel with an AE{nodeName}Template function inside and place the file in your MAYA_SCRIPT_PATH. The AE{nodeName}Template function contains editorTemplate commands that instruct the attribute editor how to alter the default layout for the attributes in the node. Below is a sample attribute editor template for an imaginary node with 4 attributes, one of which is a ramp attribute.

global proc AEsampleNodeTemplate( string $nodeName )
{
    editorTemplate -beginScrollLayout;
 
    editorTemplate -beginLayout "Sample Node Attributes" -collapse 0;
        editorTemplate -addControl "magnitude";
        editorTemplate -addControl "offset";
        editorTemplate -addControl "distance";
 
        AEaddRampControl ($nodeName + ".rampAttribute");
 
    editorTemplate -endLayout;
 
    AEdependNodeTemplate $nodeName;
 
    editorTemplate -addExtraControls;
    editorTemplate -endScrollLayout;
}

MScriptUtil

MScriptUtil is the cumbersome class we must use when using the Maya API with Python. Since the Maya API is designed as a C++ library, it has many pointers and references that are passed into and returned from various functions. Since Python has no pointers or references to simple types, we must use MScriptUtil when we encounter these in the Maya API. Note if you use the Python API 2.0, you don't have to use MScriptUtil. The documentation contains useful information about general usage of MScriptUtil, so I will not reproduce it here.  What I will show are various code samples that demonstrate how to use MScriptUtil in various situations since at the time of this writing, the code examples for MScriptUtil are quite limited.  Luckily, I don't need to use MScriptUtil often, but when I do encounter it, I will put a snippet on this page to build up a useful reference.

Pass by Reference

int

# MStatus MItMeshPolygon::setIndex(int index, int& prevIndex)
itPoly = OpenMaya.MItMeshPolygon(pathShape)
util = OpenMaya.MScriptUtil()
util.createFromInt(0)
pInt = util.asIntPtr()
itPoly.setIndex(faceId, pInt)
# MStatus MImage::getDepthMapSize(unsigned int& width, unsigned int& height) const

utilWidth = OpenMaya.MScriptUtil()
utilWidth.createFromInt(0)
ptrWidth = utilWidth.asUintPtr()
utilHeight = OpenMaya.MScriptUtil()
utilHeight.createFromInt(0)
ptrHeight = utilHeight.asUintPtr()
mimage.getDepthMapSize(ptrWidth, ptrHeight)
width = OpenMaya.MScriptUtil.getUint(ptrWidth)
height = OpenMaya.MScriptUtil.getUint(ptrHeight)

float2

# MStatus MItMeshPolygon::getUVAtPoint(MPoint &pt, float2& uvPoint, MSpace::Space space=MSpace::kObject, const MString*uvSet=NULL)

util = OpenMaya.MScriptUtil()
util.createFromList([0.0, 0.0], 2)
uvPoint = util.asFloat2Ptr()
itPoly.getUVAtPoint(closestPoint, uvPoint, OpenMaya.MSpace.kWorld)
u = OpenMaya.MScriptUtil.getFloat2ArrayItem(uvPoint, 0, 0)
v = OpenMaya.MScriptUtil.getFloat2ArrayItem(uvPoint, 0, 1)

Accessing Arrays

# MMatrix::operator[] (unsigned int row)

#Doesn't work!
matrix[3][1] = 2.2
 
# Do this instead
OpenMaya.MScriptUtil.setDoubleArray(matrix[3], 1, 2.2)
# float* MImage::depthMap(MStatus* ReturnStatus=NULL) const

ptrDepthMap = mimage.depthMap()
OpenMaya.MScriptUtil.getFloatArrayItem(ptrDepthMap, index)

MRampAttribute

MRampAttribute allows you to create an adjustable curve or color attribute where users can insert and adjust the interpolation of points along the ramp.

To create ramp attributes, we call the convenient classes contained in MRampAttribute:

MStatus RampAttributeDeformer::initialize() {
  // Create the curve ramp attribute
  aCurveRamp = MRampAttribute::createCurveRamp("curveRamp", "cur");
  addAttribute(aCurveRamp);
  attributeAffects(aCurveRamp, outputGeom);
 
  // Create the color ramp attribute
  aColorRamp = MRampAttribute::createColorRamp("colorRamp", "cor");
  addAttribute(aColorRamp);
  attributeAffects(aColorRamp, outputGeom);
  
  return MS::kSuccess;
}

To access the ramp attribute values inside a node or deformer:

MStatus RampAttributeDeformer::deform(MDataBlock& data, 
                          MItGeometry& itGeo, 
                          const MMatrix &localToWorldMatrix, 
                          unsigned int geomIndex) {
  MStatus status;
 
  // Get the ramp attributes
  MObject oThis = thisMObject();
  MRampAttribute curveAttribute(oThis, aCurveRamp, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  MRampAttribute colorAttribute(oThis, aColorRamp, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  float rampPosition = 0.25f, curveRampValue;
  MColor color;
 
  // Get the corresponding value on the curve ramp attribute
  curveAttribute.getValueAtPosition(rampPosition, curveRampValue, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  // Get the corresponding value on the color ramp attribute
  colorAttribute.getColorAtPosition(rampPosition, color, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  // Do your calculation with the values
 
  return MS::kSuccess;
}

You will also need to make sure the attribute is set correctly in your attribute editor template for the node:

global proc AErampDeformerTemplate( string $nodeName )
{
    editorTemplate -beginScrollLayout;
 
        editorTemplate -beginLayout "Ramp Deformer Attributes" -collapse 0;
            AEaddRampControl( $nodeName + ".curveRamp" );
            AEaddRampControl( $nodeName + ".colorRamp" );
        editorTemplate -endLayout;
 
    editorTemplate -addExtraControls;
    editorTemplate -endScrollLayout;
}

Additional Resources

Below are additional resources to learn the Maya API.