Prototyping OpenGL with MEL

Not only is MEL a great multi-purpose way to customize Maya to your needs, it's also a great platform for prototyping other 3d graphics applications and components. This is espically true when working with creating geometry, as you can focus solely on creating the geometry while getting the windowing and interaction for free. But to make it really worthwhile, it's important to take an approach (with the script) that mirrors the approach you'll have to take with the environment (language and libraries) that you'll ultimately want to use.

As an example, let's say that you want to use MEL to mock up an approach using OpenGL and C++. To create a 5 by 5 unit quad at the origin and lying in the XZ plane with OpenGL, you would need to do something like the following:

glBegin(GL_QUADS);
	glVertex3f(0.0f,0.0f,0.0f);
	glVertex3f(5.0f,0.0f,0.0f);
	glVertex3f(5.0f,0.0f,5.0f);
	glVertex3f(0.0f,0.0f,5.0f);
glEnd();

On the other hand, if you wanted to create the same face with MEL, the code would look like:

polyCreateFacet -p 0 0 0 -p 5 0 0 -p 5 0 5 -p 0 0 5;

There are a number of differences between the two approaches, but nothing that can't be overcome. The first big difference is that with OpenGL, one adds vertices one at a time, while the polyCreateFacet command requires that you specify all of the vertices at once. Luckily, its easy enough to get around that by constructing a largish string and pointing it at the eval command.

Once the string is started off with the initial polyCreateFacet, vertices can be added by tacking on additional "-p X Y Z" to the string. So, once you've settled on some math to use to calculate the positions of the vertices, the overall process looks something like the following (assuming an 8-sided face):

// create the command
string $command = "polyCreateFacet ";

// create three temporary float variables
float $posX, $posY, $posZ;

for ($i=0;$i<8;$i++)
{
	// do whatever's necessary to calculate position
	$posX = calculateXposition();
	$posY = calculateYposition();
	$posZ = calculateZposition();

	$command += ("-p " + $posX + " " + $posY + " " + $posZ + " ");
}

// create the face from the  combined string
eval($command);

While the above example may seem a bit roundabout, but it has the benefit of being easily divided into the three components seen with OpenGL, namely:
  1. Initialize the face (with the glBegin commmand)
  2. Add some number of vertices (with glVertex3f)
  3. Finialize the face (with glEnd)
Re-phrasing the above in MEL yeilds the following:
  1. Start off the string with "polyCreateFacet"
  2. Create individual faces by alternating between adding vertices and using the eval command
  3. Finialize the mesh by joining all of the individual faces
Note that the second step (analagous to the calls to glVertex3f) is where the eval command is invoked, rather than in the last step. That's to allow for the creation of multiple faces. Without stopping every so often to use the eval command to make a face, the behavior is limited to what is found with GL_POLYGON. In order to get behavior like GL_TRIANGLES or GL_QUADS, we'll need to use the eval command every so often (either every 3 or every 4 vertices, respectively) to create the individual faces. Doing that produces a number of faces, all separate.

To join the faces together, we'll need to do two things:
  1. Use the combine command to create a single object
  2. Use the merge vertices command to join overlapping vertices
Having identified some potential similarities in approach between MEL and OpenGL, we can now graft OpenGL syntax onto MEL by creating custom procedures that just so happen to be named glBegin, glVertex3f, and glEnd.

Overall setup and global variables

Over the course of using the three procedures that we're about to make, we'll need a number of global variables in order to easily pass data around between them. First off, we'll need a string (let's call it $command) to hold the collection of vertices as we make them. As we evaluate and reset the $command string to create faces, we'll likely want to add them into a single group to make the combining and merging easier. For that, we'll need a global string to hold the name of the group (let's call it $objGroup). Finally, we'll need two global integers to use in differentiating between GL_TRIANGLES, GL_QUADS, and GL_POLYGONS. One of them (let's call it $vTarget) is responsible for holding the number of vertices needed for a face- either 3 for GL_TRIANGLES, 4 for GL_QUADS, or -1 (or some other placeholder value) for GL_POLYGONS. The other (let's call it $vCount) is responsible for counting the number of vertices added so far. That's how we can decide when to invoke the eval command and reset the $command string.

So, that gives us:

Global Variable Purpose
$command Holds the string that will be eval(uated) to create the face
$objGroup Holds the name of the group to add the newly made faces to
$vTarget Either the number of vertices per face (in the case of GL_TRIANGLES and GL_QUADS), or some other placeholder value (like -1)
$vCount Holds the number of vertices added since the most recent call to eval

Now let's look at the procedures themselves...

The glBegin() procedure

There's not a whole lot to the glBegin procedure. All it needs to do is to initialize the global variables and determine what kind of primitives are being created (triangles, quads, or polygons- we'll look at fans and strips later).

Since it needs to take a string as input, and shouldn't produce any output, the procedure declaration looks like:

global proc glBegin(string $primitive_type)
{
	// import all of the global variables
	global string $command;
	global string $objGroup;
	global int $vTarget;
	global int $vCount;

First off, let's get the $command string ready to have vertices added on to it by setting it equal to "polyCreateFacet ":

	$command = "polyCreateFacet ";

Notice the extra space after polyCreateFacet. This is important, since at this point, we're just creating a long string. When tacking strings (both literals and variables) together to form long, complex commands, it's really easy to forget to add in appropriate spaces. If we were to leave off the extra space after the polyCreateFacet command, we would have the first point declaration right up against the command, resulting in a syntax error.

Next, we'll create an empty group to hold the faces as they are made, and point the result at the $objGroup variable

	$objGroup = `group -empty`;

Now we need to determine what kind of primitives are being requested, and set $vTarget appropriately.

	if ($primitive_type == "GL_TRIANGLES")
	{
		$vTarget = 3;
		print "\nMaking Triangles\n";
	}
	else if ($primitive_type == "GL_QUADS")
	{
		$vTarget = 4;
		print "\nMaking Quads\n";
	}
	else
	{
		$vTarget = -1;
		print "\nMaking Polygons\n";
	}

Finally, we'll need to set $vCount to 0, and close off the procedure:

	$vCount = 0;
}

Having finished the glBegin() procedure, let's get ready to add some vertices with the glVertex3f procedure.

The glVertex3f() procedure

The glVertex3f procedure needs to take in three float values, and produces no output, giving us the following declaration:

global proc glVertex3f(float $xPos, float $yPos, float $zPos)
{
	global string $command;
	global string $objGroup;
	global int $vTarget;
	global int $vCount;

The main task of this procedure is to add on another vertex with the specified coordinates to the $command string. It should also add one to the vertex count. Neither task is all that complicated. The only potential hangup lies with remembering to add in spaces in between the three variables. It ends up looking like the following:

	$command += ("-p " + $xPos + " " + $yPos + " " + $zPos + " ");
	$vCount++;

That completes the task of adding in a new vertex to the $command string, but that's not all we need to do. We also need to check to see whether or not we need to complete a face (a triangle or a quad). That will be the case any time that $vTarget is not equal to the GL_POLYGONS value (-1, here), and $vCount is equal to $vTarget. If we need to complete a face, we'll need to do a few things: Rolling all that into the code looks like:

	if (($vTarget != -1) && ($vCount == $vTarget))
	{
		string $temp[] = `eval($command)`;
		parent $temp[0] $objGroup;
		$vCount = 0;
		$command = "polyCreateFacet ";
	}
}

Adding the newly-made face to the group takes a bit of shuffling, and a temporary string array variable. In order to parent the new object (the single face), it's necessary to have the name of the object. The polyCreateFacet command will return the name of the object it creates (the fact that it's being run via a string and the eval command changes nothing about that), so we just need to use backticks to store the returned value to a variable. However, the polyCreateFacet command, like all of the MEL commands that generate geometry, actually outputs two different nodes- a shape node and a transform. Therefore, we have to use a string array to hold the result, and use the first element ($temp[0]) to parent the transform of the face to the group.

That finishes off glVertex3f, leaving only the glEnd() procedure.

The glEnd() procedure

The glEnd procedure is where we'll finish off the object by combining the parts and merging vertices. It's also where we'll finially invoke the $command string when using GL_POLYGONS mode.

The procedure takes no input and returns nothing, so the declaration looks like:

global proc glEnd()
{
	global string $command;
	global string $objGroup;
	global int $vTarget;

We don't actually need the $vCount variable, since we're not adding any new vertices here, but we do need the $vTarget variable in order to check for GL_POLYGONS. So let's do that first. If we are in GL_POLYGONS mode, all we have to do is to evaluate the $command string. If we're in either GL_TRIANGLES or GL_QUADS, there's a bit more to do, but let's start with the easy stuff:

	if ($vTarget == -1)
	{
		eval($com);
	}

If we're not in GL_POLYGONS mode ($vTarget != -1), we'll need to combine all of the parts in the $objGroup group into a single polygonal object and merge the vertices. That ends up looking like the following:

	else
	{
		string $temp[] = `polyUnite -ch 0 $objGroup`;

That will combine all of the objects contained in the $objGroup group into a single polygonal object. Note the inclusion of the -ch (Construction History) flag. By setting that to 0, the command won't maintain construction history. This is a good thing in this case, as otherwise you end up with a significant collection of empty transforms, which gets messy. What we want is just to have the created mesh as a single object. Note that this does commit you to only creating a single object with each glBegin()/glEnd() pairing. If you want separate objects you can either skip this step or (more elegant) just break up the list of vertices into multiple glBegin()/glEnd() pairings.

The result of the polyUnite command is stored in a variable so that it can be easily selected when everything is done. This is included to interface with standard Maya operation wherein any command that creates something leaves the thing it created selected upon completion.

Once all of the objects have been combined with the polyUnite command, they are seen as a single object. However, there are multiple vertices stacked on top of each other, which is both inefficient and dangerous if you want to alter the model using Maya's modeling tools. The solution lies in selecting all of the vertices of the new model and running the merge vertices command. The easiest way to select all of the vertices is to convert the selection over to vertices with the PolySelectConvert command, as in:

		PolySelectConvert 3;
		polyMergeVertex -distance 0.001 -ch 0;

The PolySelectConvert command is one of the more useful undocumented MEL commands- it takes an integer which specifies the kind of component to convert the selection to, with 3 signifying vertices. If an object is selected prior to running PolySelectConvert 3, it results in all of the vertices of the model being selected. Once all of the vertices are selected, the polyMergeVertex command can be used to merge the vertices. Note that the -ch (Construction History) flag is used again to make sure that construction history is turned off.

Once the pieces have been combined into one, and the vertices have been merged with a low thresehold, all that remains is to select the resulting object:

		select -replace $temp[0];
	}
}

And that's that! Now we're ready to use the new procedures in an actual script.

Putting it all to use

The goal of the collection of procedures is to allow the user to forget they exist, writing code as if you were working with OpenGL. So, if you want to create a 5x5 quad streching away from the origin and lying in the XZ plane with MEL, you can use the following code:

global proc oGL_example()
{
	glBegin("GL_QUADS");

		glVertex3f(0,0,0);
		glVertex3f(5,0,0);
		glVertex3f(5,0,5);
		glVertex3f(0,0,5);

	glEnd();
}

Look familiar? It's almost the same as straight OpenGL, with the only difference being that GL_QUADS must be in quotes, since it's being passed into glBegin() as a string. Unfortunately, there's no real way around that, but it's close enough to make moving code from MEL to C++ pretty straightforward.

Resources

BACK