[alink='2']Maya API Basics[/alink]
[alink='3']Vertices[/alink]
[alink='4']Polygons[/alink]
[alink='5']Materials[/alink]
[alink='6']Animations[/alink]
Perhaps you have just been asked to write a data exporter for Maya. Odds are you are not working for one of those huge long term game projects that can afford to have several people dedicated 100% to writing export tools, but rather you have a few days to put together a usable tool that will rip some animated character models out of a .mb file. You went to look at all 5000 manuals that ship with Maya, and surprise! None of them appear to be Maya programming manuals.
If you have not done it already you should install a copy of Maya on your system. Install everything the artists will be using. At first I thought maybe all I needed to install was the "developers kit" part of Maya. You should save yourself the trouble and install everything, including the dev kit. Make sure to install all the documentation too. The API is not documented in the paper manuals, but is in the help files.
In this article I have put together all the things you need to know to get that tool done fast. You can worry about the finer points of the Maya API later.
Maya data exporting tools can be written either as plug-ins or as external programs. With a plug-in you could easily make a module that presents an artist with a button right inside of Maya that they simply press and a data file pops out of Maya in a format compatible with your game engine. The other way is to have the artists save everything as a native Maya file, and to write an external tool that can be run later, maybe as part of your make system, to convert the Maya (.mb or .ma ) files to your own format. Most of the Maya programming examples are of the plug-in type. This article will show you how to write an "application"-style exporter.
Maya is essentially a set of DLLs that modify a specialized database. On top of that there are a whole bunch of plugins and .mel scripts. The Maya program itself appears to be a thin user interface veneer to control the real system. Writing a stand alone application means simply linking with the same Maya DLLs as the Maya program itself uses.
Using Maya's own DLL's to interpret Maya files is the best way. Don't attempt to write your own Maya file parser from scratch. It's tempting at first, when you don't understand the seemingly bizarre Maya API, and its weird data structures, and the data you want it tantalizingly easy to see in a Maya ASCII file. ( You can save any file in an ASCII version and just browse through the data structures and sure enough you will find all your vertices, transforms and meshes all pretty clearly expressed in the file ). The problem with writing your own file parser is that once you do that you will have to keep expanding your file parser forever to support more and more of Maya's features, and your artists are sure to be frustrated if they can't use a feature simply because your homegrown file reader is not yet ready to process that data type. Besides it's a complete waste of time, because even though the API is bizarre, it's not that hard to understand and is pretty forgiving. The downside is that by doing this, any computer that needs to run the exporter must have a Maya license. ( Hey Alias|Wavefront, how about a low cost license to Maya for those workstations that only need to run an exporter? )
[size="5"][aname='1']A Simple Example
It's easy to write a small program that tells Maya to load a file and let you wander through its data structures:
// initialize Maya, before any Maya operations are performed.
// You might do this in main for example:
void main()
{
MStatus stat = MLibrary::initialize ( "myExporter" );
if ( !stat )
return false;
// figure out the file name to open
char* fileName = getFileNameToLoad();
// prepare Maya to read a new scene file
MFileIO::newFile( true );
// read the scene file
stat = MFileIO::open( fileName );
if ( !stat )
return false;
// remove any temporary data created by Maya while loading
// things like the undo list which we won't be using
stat = MGlobal::executeCommand( "delete -ch" );
if ( !stat )
return false;
// iterate through all the nodes in the DAG, and print out their names
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
cerr << "Found DAG Node: "
<< dagPath.fullPathName().asChar()
<< endl;
}
// now shut down Maya, or if you want to process another file,
// just make another call to MFileIO::newFile(), and MFileIO::open()
MLibrary::cleanup();
}
Foundation.lib OpenMaya.lib
[size="5"][aname='2']Maya API Basics
Great, you finished the entire input side of your exporter! Now you can goof-off the rest of the week because you already got half your work on the exporter done, right? Ok, no, you can't. Look at that crazy API. ( You can find the Maya API documentation in the help files under maya/docs/en_US/html/DevKit/Dev_Tools_TOC.html ) It's not even clear where you start. How do you even get a reference to the scene graph, or its root object? To understand these things you should take the time to read the Maya API White Paper. It's kind of long and boring and you might be tempted not to read it, but trust me, you should read it. It's actually not that long, about 10 pages, and everything is much clearer after that.
But you didn't go read it did you? Ok, I'll tell you whats in it, but then go back and read it later.
The central data structure in Maya is the DAG, or Directed Acyclic Graph. It is what is known to almost all programmers as a "tree". Much of the data you are interested in is expressed as nodes of data on this tree. Each node has a type. Each one has a parent node ( except for the root node ), and each node may have an arbitrary number of children. There are hundreds of types of nodes, and at least in theory, any node can be the parent or the child of a node of any type. The DAG is guaranteed to be a tree. So there are no cycles in it. If you visit the nodes using an orderly tree walking algorithm you can visit every node, and you won't have to worry about infinite loops. Not all of the data is stored in the DAG, but vertices, meshes, transforms, and a model's skeletal system are stored in the DAG.
The other data structure, far scarier, is the DG or Dependency Graph. Like the DAG, this is a set of nodes that refer to each other, but here there are no restrictions. DG nodes can refer to any other node, and cycles are possible. This is where the textures, materials, and animation information is stored. Technically the DAG is a subset of the DG, but getting data out of the DG is somewhat more complicated than getting stuff out of the DAG, so it's convenient to think about nodes that are in the DAG, and then all the other nodes.
Finally, the way the API works is unlike anything you are probably used to. There are two major object types in the Maya API: dependency nodes and function sets. There are a large number of "dependency node" objects, each is a different type of handle to a node in the DG or DAG. Think of them as pointers into the DG. The different kinds allow you to refer to nodes with greater or lesser specificity and to iterate over the nodes in different ways. ( MItDag iterates over the DAG, MItDepenencyNodes iterates over all the nodes in the DG - including all the DAG nodes ).
The function set objects represent interfaces to the nodes. You construct these interfaces by calling that function set's constructor with a node reference as an argument to its constructor. Not every node supports every function set, but it's legal to construct any function set using any node, the function set object will just be created in an invalid state which should be checked. A typical function set is MFnMesh, which allows you to extract mesh information ( vertices, polygons etc. ) from a node that contains this kind of information.
Writing an exporter requires you to pretty much just iterate over the DG or DAG in various ways tracking down nodes, and then using them to create function sets that allow you to extract the data.
It's also good to remember that almost all of the objects that you can create through the Maya API are handles to the real objects that are linked into the DG and that Maya manages itself. You normally create all these objects as temporaries and let them get cleaned up automatically when they go out of scope. Simple.
[size="5"][aname='3']Extracting Vertices
So let's write a quick function to find all the vertices in a model. We will assume that we are interested in extracting all the vertices in the file:
void extractVertices()
{
// we assume here that Maya has been initialized and the file in
// question has already been loaded.
MStatus stat;
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
if ( stat )
{
MFnDagNode dagNode( dagPath, &stat );
// this object cannot be intermediate, and it must be a mesh
// and it can't be a transform.
// Intermediate objects are special meshes
// that are not drawn used for mesh morphs or something.
if ( dagNode.isIntermediateObject()) continue;
if ( !dagPath.hasFn( MFn::kMesh )) continue;
if ( dagPath.hasFn( MFn::kTransform )) continue;
MFnMesh fnMesh( dagPath );
// get the vertices that are part of the current mesh
MPointArray vertexList;
fnMesh.getPoints( vertexList, MSpace::kWorld );
// iterate through all the vertices
for ( u32 i = 0; i < vertexlist.length(); i++ )
{
vertexlist.cartesianize();
mpoint point = vertexlist;
// here is your data... now go do whatever you want with
// it. if you need a unique identifier for this vertex,
// use it's index in the mesh, and some kind of mesh id.
// these stay constant while exporting ( so long as the file is
// not edited )
processvertex( point.x, point.y, point.z );
}
}
}
}
[size="5"][aname='4']Extracting Polygons
Extracting polygons is similarly straight-forward:
void extractPolygons()
{
MStatus stat;
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
if ( stat )
{
MFnDagNode dagNode( dagPath, &stat );
if ( dagNode.isIntermediateObject()) continue;
if ( !dagPath.hasFn( MFn::kMesh )) continue;
if ( dagPath.hasFn( MFn::kTransform )) continue;
// get the mesh and all its vertices
MFnMesh fnMesh( dagPath );
MPointArray vertexList;
fnMesh.getPoints( vertexList, MSpace::kWorld );
// now iterate over all the polygons in the mesh
MItMeshPolygon piter( dagPath, comp );
for ( ; !piter.isDone(); piter.next())
{
// for each polygon you can get the indices of
// each of its vertices in the vertex list above
MIntArray vertexIdxs;
piter.getVertices( vertexIdxs );
if ( vertexIdxs.length() == 3 )
{
// process a triangle
MPoint point0 = vertexList[vertexIdxs[0]];
MPoint point1 = vertexList[vertexIdxs[1]];
MPoint point2 = vertexList[vertexIdxs[2]];
processTriangle( point0, point1, point2 );
}
}
}
}
}
[size="5"][aname='5']Extracting Materials
Getting a single material associated with a polygon face is a little trickier than the code above but still not too bad. The trick is that this time you can't just iterate over the DAG to find the polygons, but rather you have to iterate over the whole DG to find all the materials first, and then use the materials to find all the polygons each material is associated with.
bool extractMaterials()
{
MStatus stat;
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
if ( stat )
{
MFnDagNode dagNode( dagPath, &stat );
if ( dagNode.isIntermediateObject()) continue;
if ( !dagPath.hasFn( MFn::kMesh )) continue;
if ( dagPath.hasFn( MFn::kTransform )) continue;
MFnMesh fnMesh( dagPath );
// Here is the trick, get all the nodes connected to this
// mesh, whether they are on the DAG or not. This will
// include all the materials.
unsigned instanceNumber = dagPath.instanceNumber();
MObjectArray sets;
MObjectArray comps;
fnMesh.getConnectedSetsAndMembers( instanceNumber, sets,
comps, true );
// iterate over all the connected sets and look for materials
{ for ( u32 i = 0; i < sets.length(); i++ )
{
mobject set = sets;
mobject comp = comps;
mfnset fnset( set );
mfndependencynode dnset( set );
mobject ssattr = dnset.attribute( mstring( "surfaceShader" ) );
mplug ssplug( set, ssattr );
mplugarray srcplugarray;
ssplug.connectedto( srcplugarray, true, false );
if ( srcplugarray.length() == 0 ) continue;
// this object contains a reference to a shader, or
// material, so we might call our own function to write
// that material to our own data structure for later export,
// and return a material index so we can attach that index
// to all the polygons we are going to extract below.
mobject srcnode = srcplugarray[0].node();
u32 matidx = makematerial( srcnode );
// we might want to stop processing this node right now if
// it is a material our exporter does not support
if ( material_is_not_supported( matidx )) continue;
// otherwise, let's iterate over all the polygons that
// are colored with this material
mitmeshpolygon piter( dagpath, comp );
for ( ; !piter.isdone(); piter.next())
{
mintarray vertexidxs;
piter.getvertices( vertexidxs );
// jump to our own code to export a polygon with a material
writepolygon( vertexidxs, matidx );
}
}
}
}
}
void makeMaterial( MObject& srcNode )
{
if ( srcNode.hasFn( MFn::kPhong ))
{
MFnPhongShader phong( srcNode );
cerr << "Found phong shader: \""
<< phong.name().asChar() << "\" << endl;
// extract all the phong parameters:
MColor glowColor = phong.incandescence();
MColor diffuseColor = phong.color() * phong.diffuseCoeff();
MColor specularColor = phong.specularColor();
Double cosinePower = phong.cosPower();
MColor transColor = phong.transparency();
// now build a material, and write it out..
}
}
[size="5"][aname='6']Extracting Animation Data
Getting animation information out of Maya involves a little trickery also. Essentially you want to extract information out of the DG at a given time, and then you want to alter the time parameter for each frame. There are several problems. First is how to determine where the keyframes are. Your data will either
- have been animated in Maya with keyframes carefully set up by your animator for each game frame, in which case you just need to find the time for each keyframe.
- Or the data has been animated without keyframes, and you only need to evaluate it at a the desired frequency ( e.g. 60 fps ) over the length of the animation.
- Another way of course is that you will be modeling motion in your game in a way similar to how Maya does it and you just want to extract Maya's own data representations for motion directly ( for example you may just want to grab the motion's actual spline parameters ).I can only help you with the first two ways. The following code will find all the key frames, and the length of an animation:
maxKeyTime hold the last keyframe time, and the length of an animation. storeKeyTime is a call to your own code to store a given keyframe time so you can evaluate the animation at that time later. Remember that probably many duplicate times will be inserted into that array. There are quite likely many MFnAnimCurve nodes in the DG with the same keyframe times to animate different parts of the model. You can either chose to use the keyframe times as they are stored, or you can chose to use the last maxKeyTime and iterate over fixed steps from 0 to that time. This is something you should probably discuss with those people responsible for making the animations.void findFrames()
{
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
MTime maxKeyTime = 0;
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
MFnDagNode dn( dagPath );
// find all the motion nodes
MItDependencyGraph dgIter( dagPath.node(),
MFn::kAnimCurve,
MItDependencyGraph::kUpstream,
MItDependencyGraph::kBreadthFirst,
MItDependencyGraph::kNodeLevel,
&stat );
if ( stat )
{
// get all the keyframe times
for ( ; !dgIter.isDone(); dgIter.next())
{
MObject anim = dgIter.thisNode( &stat );
MFnAnimCurve animCurve( anim, &stat );
// found some keyframe information
if ( MS::kSuccess == stat )
{
u32 numKeys = animCurve.numKeyframes( &stat );
for ( u32 currKey = 0; currKey < numkeys; currkey++ )
{
// truncating values here...
mtime keytime = animcurve.time( currkey, &stat );
// store this key frame time somewhere.
storekeytime( keytime );
// and store the maximum frame time
if ( time > maxKeyTime )
maxKeyTime = time;
}
}
}
}
}
}
The last part of the problem is how to evaluate a model at a given time, to extract all the vertices, or transforms for a given animation. For this you use the handy MAnimControl object, like so:
And there you have it, everything you need to know to extract basic model information out of Maya.void processAnimationFrame( MTime& time )
{
// create a Dag iterator
MStatus stat;
MItDag dagIter( MItDag::kBreadthFirst, MFn::kInvalid, &stat );
// here is the magic animation time setting object. Set it to the
// desired time, and presto...
MAnimControl animctrl;
animctrl.setCurrentTime( time );
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
stat = dagIter.getPath( dagPath );
if ( stat )
{
MFnDagNode dagNode( dagPath, &stat );
if ( dagNode.isIntermediateObject()) continue;
if ( !dagPath.hasFn( MFn::kMesh )) continue;
if ( dagPath.hasFn( MFn::kTransform )) continue;
// your mesh object... and it should be
MFnMesh fnMesh( dagPath );
// get the vertices that are part of the current mesh
MPointArray vertexList;
fnMesh.getPoints( vertexList, MSpace::kWorld );
// iterate through all the vertices
for ( u32 i = 0; i < vertexlist.length(); i++ )
{
vertexlist.cartesianize();
mpoint point = vertexlist;
// here is your data, but this time it should be different
// for each time at which you evaluate it. you should be
// able to get tranform information over time in a similar
// way.
processvertex( point.x, point.y, point.z );
}
}
}
}
I have not found very many Maya API programming tutorials on the net. There is at least one really great site with Maya programming tutorials run by Bryan Ewert
Maya is a Registered Trademark of Alias|wavefront.