23 May 2009

Maya Programming #1

[Edit: fixed some bugs in the MEL code]

Maya is an incredibly complex beast, but it is also an incredibly powerful beast. The C++ API fully exposes this complexity to the outside (even more then the MEL API), and after about 10 years of working with the Maya SDK I still have some sort of love/hate relationship with the C++ API (and a thorough hate/hate relationship with MEL).

Much of the complexity stems from the fact that even a simple Maya scene is a huge network of interconnected tiny nodes (the dependency graph), and while the API tries its best to provide shortcuts for finding the relevant data, one often has to write several (sometimes a lot) lines of code to get to the actually wanted piece of data. Typical Maya code (at least typical exporter-plugin code) often consists of cascades of calls to getters and iterators just to find and read a single data element from some object hidden deeply in the dependency graph. Things have gotten better over time though, almost every new Maya version provided shortcuts for getting typical data (like triangulated mesh geometry, vertex tangents, or the shader nodes connected to the polygons of a shape node). I think the Maya team needed a little while before they got a feel for the needs of the gaming industry. Since Maya has more of a movie industry background, and (at least in Europe) is still a bit exotic compared to its (ugly) step-brother 3DMax this is understandable.

When programming a Maya plugin the actual API documentation is actually relatively useless because it’s mainly a pure class interface documentation and doesn’t explain how the objects of a Maya scene relate to each other or what’s the best practice to solve a specific task (and worse, there are often a lot of different ways to do something, and its up to the programmer to find the easiest, or cleanest, or fastest way). Instead of relying on the documentation its often better to explore the object relationships directly in Maya through the Script Editor, and only when one has a clear understanding of how the data is organized in Maya, lookup the API docs to find out how it’s done in C++.

Before starting to explore a Maya scene through the script editor one only needs to understand this:

The entire Maya scene is a single Dependency Graph, which is built from Dependency Nodes connected through Plugs (think of it as a brain made of interconnected neurons). Every time the Maya user manipulates the scene he may add or remove nodes from the graph, connect or disconnect plugs, or feed some new data into some input plugs). After parts of the dependency graph have been manipulated, it is in a dirty state and must be brought uptodate before being displayed (or examined by an exporter plugin). But instead of evaluating the entire graph (which would be very slow in a complex Maya scene with thousands of nodes), only the dirty parts of the graph which actually need updating will be evaluated. This is where the dependency stuff comes into play: every dependency node depends only on the nodes connected to its input plugs. Only input plugs marked as “dirty” need to be evaluated. If some data is changed at one of the input plugs it will propagate its dirty state “upward” in the graph, and in turn an “evaluation wave” will propagate “downwards” through the dirty nodes.

This system might seem overkill for simple 3D scenes, but as soon as animation, expressions and the construction history come into play it all makes sense, and its actually a very elegant design.

Now lets go on with the exploration:

Some important MEL commands for this are listAttrs, getAttr, setAttr and connectionInfo. Let’s start with a simple illustration of a dependency chain.

First open Maya’s script editor and create a polygon cube at the origin:

polyCube [Ctrl+Return]

This creates a transform node, and a child shape node in Maya (pCube1 and polyCube1). Let’s list the attributes of the transform node (for the sake of simplicity… these are equivalent with plugs):

listAttr pCube1

This produces dozens of attribute names (a first indication of how complex even a simple Maya scene is). Somewhere in this mess is the translateX attribute which defines the position on the X axis. Let’s have a look at its content:

getAttr pCube1.translateX

This should return 0, since we created the cube at the origin. Let’s move it to x=5.0:

setAttr pCube1.translateX 5.0

When the command executes, the cube in the 3D view should jump to its new position.

So far so good. Lets create a simple dependency by adding a transform animation. Just type this into Maya script editor (start a new line with Return, and execute the whole sequence with Ctrl+Return):

setKeyframe pCube1;
currentTime 10;
setAttr pCube1.translateX -5.0;
setKeyframe pCube1;

This sets an animation key at the current position (time should be 0 and the cube’s x position should be 5), then sets the current time to 10, moves the cube to x=-5 and sets another animation key.

Now grab the time slider and move it between frame 0 and 10, the cube should now move on the X axis. Now lets try to read the translateX attribute at different points in time. Move the time slider to frame number 3 and execute:

getAttr pCube1.translateX

This should return 2.777778. Now move the time slider to frame 6 and get the same attribute:

The result should now be –0.555556. The previously static attribute value now changes over time since we added an animation to the object. Obviously some other node manipulates the translateX attribute on pCube1 whenever the current time changes. Let’s see who it is:

connectionInfo -sourceFromDestination pCube1.translateX

This yields: pCube1_translateX.output

So there’s an object called pCube1_translateX, which has a plug called output, which feeds the translateX attribute of our cube with data. Now let’s check what type of object this pCube1_translateX is:

nodeType pCube1_translateX

The result: animCurveTL.

Now let’s check what the docs say about this node type. Open the Maya docs, go to “Technical Documentation –> Nodes”, and type animCurveTL into the “By substring” search box. This is what comes up:

This node is an "animCurve" that takes an attribute of type "time" as input and has an output attribute of type "distance". If the input attribute is not connected, it has an implicit connection to the Dependency Graph time node.

Interesting! Let’s double check: If the output plug of the animation curve is connected to the translateX attribute, it should return the same value for a specific time… Moving the time slider to frame 3 should yield a value of 2.77778, and indeed, a

getAttr pCube1_translateX.output

returns the expected value.

Now lets try something crazy: what if we feed the current value of translateX into the translateZ attribute of our cube and thus make the dependency chain a bit more interesting? The result should be that the cube moves diagonally when the time slider is moved even though only the translateX attribute is animated by an animation curve:

connectAttr -force pCube1.translateX pCube1.translateZ

Now I cheated a bit by using the –force argument. The translateZ attribute was already automatically connected to another animation curve when we executed the setKeyframe command (same thing that happened for our translateX attribute), we need to break this connection before connecting to another plug, and the –force just does that.

Lets see if it works by moving the time slider. And indeed… the cube moves diagonally on the X/Z plane as expected. Cool shit.

So that’s it. That’s how everything in Maya works. The only difference to a really complex scene is that there are hundreds or thousands of dependency nodes connected through even more plugs.

3 comments:

joew said...

This is an extremely good posting :) I've only worked with the API on a basic level and it would have been nice to have this post way back then! As you stated I found the documentation fairly useless compared to playing with Mel

fanfan said...
This comment has been removed by the author.
fanfan said...

Thanks for this good post.
You may be interested by this Dependency Graph library writen in C# and available with it's source code.