Class 5 - Misc. topics
Odds and ends..
1. How to make a proc "return" multiple outputs?

You know that a proc can only return a single output, of particular type (eg. an int, int array, string, float array etc. - any one of the 9 built-in types). Eg. here is a program that takes two ints and returns the result of an integer division:

proc int intDiv(int $a, int $b)
  int $rslt = $a/$b;
  return $rslt;
Say we wanted to modify the proc to output TWO values - an integer division result AND a floating point division result (and in general, any number of outputs of any (of the 9 types) type). How would we do that? The official 'return' mechanism is not the way to do this. The solution is to pass in (through the arg list) array variables (int or float or string or vector), and SET them (fill them) inside the proc!

If you try to overwrite an atomic (non-array) argument inside the body of a proc, your overwriting will be lost, ie. not be available when the call returns. But changes to an array variable inside a proc will "stick". This is because arrays come into MEL procs as "references", that is, pointers to the block of memory allocated for those arrays. Because they are pointers to original memory (not copies), any change made inside a proc is permanent. So we can do our divisions this way:

proc intAndFloatDivs(int $a, int $b, int $outInt[], float $outFloat[])
  $outInt[0] = $a/$b; // SET the array's first elem. to be our first result
  $outFloat[0] = ((float)$a/$b); // likewise, SET second result

// Test that the above works
int $x[];
float $y[];
intAndFloatDivs(7,4,$x,$y); // pass in $x and $y for the proc to fill!
print("Int result: " + $x[0] + "\n");  // cool!
print("Float result: " + $y[0] + "\n"); // very cool!
Using this trick you can 'pack' int, float, string, vector arrays with whatever values you want to output from a proc, and read them off the arrays when the proc returns.
2. How to add an arbitrary directory to the script path list?

When you type the name of a proc/command, MEL will search through a list of "known" directories for a program of the same name as the proc and source and run the proc (assuming it is global) if it finds such a program.

The list of directories to search in, is held in an environment variable called MAYA_SCRIPT_PATH. 'getenv' and 'putenv' are commands you can use to manage this dir. list.

// This proc can be used to add (append) a new directory to an existing script path. 
// Once a dir. is added to MAYA_SCRIPT_PATH, you can place your MEL scripts 
// in that dir. so Maya can 'automagically' find it..
// Sample usage: addDirToScriptPath "C:/temp" "PC"; 
global proc addDirToScriptPath(string $dirName, string $platform)
  string $existingPath = `getenv "MAYA_SCRIPT_PATH"`;
  print("Existing MAYA_SCRIPT_PATH: " + $existingPath + "\n");
  string $sepChar=":";
  string $newPath = $existingPath+$sepChar+$dirName; // append

  putenv "MAYA_SCRIPT_PATH" $newPath; // modify
  print(".N.e.w.. MAYA_SCRIPT_PATH: " + $newPath + "\n");
3. How to invoke non-Maya programs from within MEL programs?

Occasionally there is a need to run an external program (external to Maya, that is) from inside your MEL program. The system command lets you do this.

global proc getDate()
  string $tmp = `system "date"`;
  // The above returns a string like so:
  // The current date is: Fri 09/16/2005 
  // Enter the new date: (mm-dd-yy)  
  // The above string has eleven tokens separated by spaces, 
  // and the actual date is token #5 (counting from 0)
  string $toks[];
  tokenize $tmp " " $toks;
  print("Today's date: " + $toks[5] + "\n"); // print the date

You can also use the system command to open a web browser or text editor, send email, etc.

4. How to read and write disk files?

As you can imagine, reading and writing (r/w) files is a VERY useful thing. Here are the relevant commands:

You can use the following pair of programs as templates for r/w.

// Writes out a bunch of random points within a specified 
// radius, into a named file.
// Eg. genRndPts 1000 "C:/temp/test2.dat" 25.0;
global proc genRndPts(int $n, string $outFile, float $rad)
  int $fileId=`fopen $outFile "w"`; // obtain file "handle"
   vector $r = sphrand($rad);
   fprint $fileId ($r.x + " " + $r.y + " " + $r.z + "\n");
  fclose $fileId;

// Reads (x,y,z) points from a named text (ASCII) data file. You can use the result to create a 
// motion curve, set an aim vector, place spheres at the locations, etc. In our case, we'll
// connect the points using straight lines via a single 'polyline' curve.
// Eg. readXYZ "C:/temp/test.dat";
global proc readXYZ(string $inFile)
  string $cmd = "curve -d 1 ";
  // open the file 
  int $fileId=`fopen $inFile "r"`; 
  string $nextLine = `fgetline $fileId`; 
  while ($nextLine != "") 
   int $numTokens; 
   string $tokens[]; 
   $numTokens = `tokenize $nextLine " " $tokens`; 
    float $xpos = (float)$tokens[0]; 
    float $ypos = (float)$tokens[1]; 
    float $zpos = (float)$tokens[2];
	$cmd += ("-p " + $xpos + " " + $ypos + " " + $zpos + " ");
   $nextLine = `fgetline $fileId`;  // more data
  }// next line
  // create the curve
}// readXYZ

The file I/O feature is useful for bringing data into Maya (camera curves, motion capture data, curves, surfaces, meshes..) and exporting similar data out from Maya for use with other programs such as external viewers, 3D printers, game platforms, etc.

Here is data for a pretty curve, to use with the above (x,y,z) reading program.

Following is a very useful program for reading in .bvh motion capture files and animating a Maya skeleton chain with them. Try loading these data files using the program:

BVH file import. Builds a skeleton under bvh_import group and imports the motion.
Sets up zero initial position at the frame -20. Works with Maya 4.0.
Just run the script and pick a bvh file.
NOTE: New Curve Default option (uder Preferences/Settings/Keys) MUST be "Independent Euler-Angle Curves"
Written by Sergiy Migdalskiy , comments and suggestions are welcome.
Originally based on the bvh_import.mel script by sung joo, Kang (Gangs) / sjkang(at), gangs2(at)

Small mods by Saty [at the beginning and end of bvh_import()]

// Usage: source procs below and do either
// bvh_import (""); // brings up a file browser
// or 
// bvh_import "C:/temp/testdata.bvh"; // directly open specified file, no browser needed

// not used
proc float Turn180Degrees (float $fAngle)
  float $gPi = 3.1415926535897932384626433832795;
  return $fAngle >= 0 ? $fAngle - $gPi : $fAngle + $gPi;
// not used
// mirrors the rotation: returns pi-fAngle
// normalized to the given normalized [-pi,pi) angle
proc float Mirror180Degrees (float $fAngle)
  float $gPi = 3.1415926535897932384626433832795;
  return $fAngle > 0 ? $gPi - $fAngle: -$gPi - $fAngle;
proc setRotation (string $strObject, float $fRotate[])
  float $fRotateX, $fRotateY, $fRotateZ;
  $fRotateX = $fRotate[0];
  $fRotateY = $fRotate[1];
  $fRotateZ = $fRotate[2];
  rotate -r -os 0 0 $fRotateZ $strObject;
  rotate -r -os $fRotateX 0 0 $strObject;
  rotate -r -os 0 $fRotateY 0 $strObject;
  setKeyframe -at "rotateX" $strObject;
  setKeyframe -at "rotateY" $strObject;
  setKeyframe -at "rotateZ" $strObject;

global proc bvh_import(string $fn)
  string $filename;

    $filename=`fileDialog -dm "*.bvh"`; // -dm is a "directory mask"
    $filename=$fn; // just copy over user's input
  $fileId=`fopen $filename "r"`;
    error("Specify a valid file!"); // terminate
  select -cl;
  int $joint_name_val = 0;
  string $joint_name[];
  float $offset_joint_x[], $offset_joint_y[], $offset_joint_z[];
  string $index_joint[];
  int $index = 0;
  int $index_ch = 0;
  string $index_channel[];
  string $make_joint_cmd;
  string $ch_tmp;
  float $frame_interval;
  string $temp_buff[];
  string $name, $name_temp;
  clear $joint_name $offset_joint_x $offset_joint_y $offset_joint_z $index_joint $index_channel;
  $name = `group -em -n bvh_import`;
  tokenize $name "bvh_import" $temp_buff;
  if (size($temp_buff) == 0 ) {
    $name_temp = "";
  else {
    $name_temp = $temp_buff[0];
  string $nextWord = `fgetword $fileId`;
  float $offsetx = 0;
  float $offsety = 0;
  float $offsetz = 0;
  int $frames;
  float $time_count = 0;
  string $last_joint_name_val = "";
  while (  size($nextWord) >0 )   {
    if ($nextWord == "ROOT")        {
      $jointname = `fgetword $fileId`;
      $joint_name[0] = $jointname+$name_temp;
      $index_joint[$index] = $jointname+$name_temp;
      joint -n $joint_name[0] -p 0 0 0;
    if (($nextWord=="JOINT") || ($nextWord=="End"))    {
      // find Joint name
      $jointname = `fgetword $fileId`;
      $joint_name[$joint_name_val] = $jointname+$name_temp;
      $index_joint[$index] = $jointname+$name_temp;
    if ($nextWord == "{")   {
      $nextWord = `fgetword $fileId`;
      if ($nextWord == "OFFSET" )
                                // find Joint offset data
	  float $offset_x=`fgetword $fileId`;
	  float $offset_y=`fgetword $fileId`;
	  float $offset_z=`fgetword $fileId`;
	  $offset_joint_x[$joint_name_val] = $offset_x;
	  $offset_joint_y[$joint_name_val] = $offset_y;
	  $offset_joint_z[$joint_name_val] = $offset_z;
	  $offsetx = $offsetx + $offset_joint_x[$joint_name_val];
	  $offsety = $offsety + $offset_joint_y[$joint_name_val];
	  $offsetz = $offsetz + $offset_joint_z[$joint_name_val];
	  if ($joint_name_val != 0)
	      if ($joint_name[$joint_name_val] == "Site")
		$joint_name[$joint_name_val] = "Effector" + $joint_name[$joint_name_val-1];
	      $last_joint_name_val = $joint_name_val;
	      $make_joint_cmd = "joint -n "+ $joint_name[$joint_name_val]+ " -p " + $offsetx + " " + $offsety + " " + $offsetz;
	      $sel_joint_cmd = "select -r " + $joint_name[$joint_name_val-1];
	      $ord_joint_cmd = "setAttr " + $joint_name[$joint_name_val-1] + ".rotateOrder 2";
      $joint_name_val ++;
    if ($nextWord == "}")   {
      $joint_name_val --;
      $offsetx = $offsetx - $offset_joint_x[$joint_name_val];
      $offsety = $offsety - $offset_joint_y[$joint_name_val];
      $offsetz = $offsetz - $offset_joint_z[$joint_name_val];
    if ($nextWord == "CHANNELS") {
      int $tmp = `fgetword $fileId`;
      for ($i = 1; $i <= $tmp; $i++)  {
	string $tmp2 = `fgetword $fileId`;
	switch ($tmp2)  {
	case "Xposition" :
	  $ch_tmp = "translateX";
	case "Yposition" :
	  $ch_tmp = "translateY";
	case "Zposition" :
	  $ch_tmp = "translateZ";
	case "Xrotation" :
	  $ch_tmp = "rotateX";
	case "Yrotation" :
	  $ch_tmp = "rotateY";
	case "Zrotation" :
	  $ch_tmp = "rotateZ";
	$index_channel[$index_ch] = $index_joint[$index] + "." + $ch_tmp;
      $index ++;
    if ($nextWord == "MOTION") {
      $nextWord = `fgetword $fileId`;
      if  ($nextWord == "Frames:") {
	$frames = `fgetword $fileId`;
      $nextWord = `fgetword $fileId`;
      $nextWord = `fgetword $fileId`;
      if ($nextWord == "Time:") {
	$frame_interval = `fgetword $fileId`;
      $nextWord = `fgetword $fileId`;
      float $fRotation[3];
      for ( $k = 1; $k <= $frames; $k++) {
	currentTime $k;
	print ("currentTime " + $k + "\n");
	for ($chan = 0; $chan < size($index_channel); $chan++)
	    setAttr $index_channel[$chan] 0;
	for ($j=1; $j<$index_ch; $j++)
	    float $value = $nextWord;
	    string $buffer[];
	    tokenize $index_channel[$j-1] "." $buffer;
	    switch ($buffer[1])
	      case "translateX":
	      case "translateY":
	      case "translateZ":
		setAttr $index_channel[$j-1] $value;
		setKeyframe -at $buffer[1] $buffer[0];
	      case "rotateX":
		$fRotation[0] = $value;
	      case "rotateY":
		$fRotation[1] = $value;
	      case "rotateZ":
		$fRotation[2] = $value;
		  case "rotateX":
		  rotate -r -os $value 0 0 $buffer[0];
		  setKeyframe -at "rotate" $buffer[0];
		  case "rotateY":
		  rotate -r -os 0 $value 0 $buffer[0];
		  setKeyframe -at "rotate" $buffer[0];
		  case "rotateZ":
		  rotate -r -os 0 0 $value $buffer[0];
		  setKeyframe -at "rotate" $buffer[0];
	    switch ($buffer[1])
		//case "rotateX":
	      case "rotateY":
		//case "rotateZ":
		setRotation ($buffer[0], $fRotation);
	      if ($k >= 40 && $k <= 50 )
	      print ("\tsetAttr " + $index_channel[$j-1] + "  " + $value + ";\n");
	    $nextWord =`fgetword $fileId`;
	$time_count += ($frame_interval*30);
	$nextWord = `fgetword $fileId`;
    currentTime -20;
    // make up the initial pose
    for ($chan = 3; $chan < size($index_channel); $chan++)
	string $buffer[];
	tokenize $index_channel[$chan] "." $buffer;
	setAttr $index_channel[$chan] 0;
	setKeyframe -at $buffer[1] $buffer[0];
    $nextWord = `fgetword $fileId`;
  select -cl;
  fclose $fileId;

  // frame the joints, set playback endframe, start playback
  select -r "bvh_import*";
  FrameSelected; fitPanel - selected;
  playbackOptions -e -max $frames;
  play -st 1; 
}// bvh_import()
5. How to make contents of a UI (settings) persist on disk between invocations?

[Note - this was also discussed in Class 4]

It is quite helpful for users of your GUI programs if the UI can "somehow" remember the settings of its various controls (sliders, textfields, checkboxes..), so that if a user closes the program and reopens it later, the settings for the controls magically set themselves to their values from the previous session. Eg. for a facial animation UI with dozens of slider controls, such a feature is practically a necessity!

optionVar is the MEL command that you'd use to make values (int, float, string) "persist" between program invocations and even Maya sessions.It does its magic by storing values in a single file, 'userPrefs.mel'. This happens to be the same file where Maya stores all your user prefs.

Consider the following toy UI:

window myWin;
 textFieldGrp -l "Text";
 floatFieldGrp -l "Float";
 intFieldGrp -l "Int";
 button -l "Close" -c "deleteUI myWin";
If you enter some values in the three fields, close the program and re-run it, the UI seems to have "forgotten" what you typed. What we need to do is this. When the controls are created, we need to see if previously-stored values exist for them, and if yes, retrieve the values, populate the controls with the values. Likewise, on exit, we need to store the values of the controls, for retrieval during next invocation.

Here's the optionVar-enabled version of the above program:

global proc closeMyWin()
  // stores values of our widgets

  string $currText = `textFieldGrp -q -text myWin_text`;
  optionVar -sv "myWin_textVal" $currText;

  float $currFloat = `floatFieldGrp -q -v1 myWin_float`;
  optionVar -fv "myWin_floatVal" $currFloat;

  int $currInt = `intFieldGrp -q -v1 myWin_int`;
  optionVar -iv "myWin_intVal" $currInt;

  deleteUI myWin;

window myWin;
  textFieldGrp -l "Text" myWin_text;
  if(`optionVar -exists myWin_textVal`)
      string $stored_myWin_textVal = `optionVar -q myWin_textVal`;
      textFieldGrp -e -text $stored_myWin_textVal myWin_text;

  floatFieldGrp -l "Float" myWin_float;
  if(`optionVar -exists myWin_floatVal`)
      float $stored_myWin_floatVal = `optionVar -q myWin_floatVal`;
      floatFieldGrp -e -v1 $stored_myWin_floatVal myWin_float;

  intFieldGrp -l "Int" myWin_int;
  if(`optionVar -exists myWin_intVal`)
      int $stored_myWin_intVal = `optionVar -q myWin_intVal`;
      intFieldGrp -e -v1 $stored_myWin_intVal myWin_int;
  button -l "Close" -c "closeMyWin";
showWindow myWin;
6. How to write Maya plugins?!

As you know, this is a ***huge*** topic!! Here we'll barely scratch the surface..

There are several resources for learning Maya plugin programming. You can start with the devkit notes that ship with Maya itself (part of Maya docs). Then there is the 'Developer's Conference' conducted by Alias, David Gould's two books, Brian Ewert's website, etc. As with any other programming, the best way is to study existing code, understand what's going on, then start modifying things.

Here are the steps in creating and using a plugin:

As an example, consider the circleNode.cpp file that ships with the plugin 'devkit' (in the Maya distribution area). We have the source, next we need to compile it. On Windows, if you have the circleNode.dsp project file (which also ships with Maya), you can simply open it in Visual C++, click on Build->Build circleNode.mll to compile the plugin. As the following snapshot shows, this successfully creates circleNode.mll.

Alternately, on Linux (and even on Windows/Mac with a little more work), you can compile the plugin "by hand" using individual calls to convert .cpp->.o and then .o->.so. The following two calls work on Red Hat Linux on an HP.

# Here's how you'd create a plugin "by hand". All you need are 'g++' (open source freeware C++ compiler) 
# and your source (C++) file(s) 

# compile circleNode.cpp into circleNode.o 
g++ -m32 -Wa,--32 circleNode.cpp -o circleNode.o -c -pthread -Wall -W -Wcast-align -Wcast-qual -Wswitch -Wunused -Wno-unused-parameter -Wreturn-type -Woverloaded-virtual -Wreorder -Wsign-promo -Wdisabled-optimization -Wno-system-headers -fPIC -pipe -DMODULE_NAME=\"circleNode\" -g -DDEBUG -D_REENTRANT=1 -DFORCE_ASSERTIONS -D_BOOL -DREQUIRE_IOSTREAM -DMAYA_PTHREADS -DAW_TEMPLATE_IOSTREAMS -I/rel/third_party/maya/ 

# turn circleNode.o into a 'shared object' 
g++ -m32 -Wa,--32 -pthread -Wall -W -Wcast-align -Wcast-qual -Wswitch -Wunused -Wno-unused-parameter -Wreturn-type -Woverloaded-virtual -Wreorder -Wsign-promo -Wdisabled-optimization -Wno-system-headers -fPIC -pipe -DMODULE_NAME=\"circleNode\" -g -DDEBUG -D_REENTRANT=1 -DFORCE_ASSERTIONS -D_BOOL -DLINUX -DCOMPILER=\"gccMaya6\" -DREQUIRE_IOSTREAM -DMAYA_PTHREADS -DAW_TEMPLATE_IOSTREAMS -DMAYA_VERSION=\"\" circleNode.o -Wl,-rpath,/opt/lib/gccMaya6 -L/opt/lib/gccMaya6 -Wl,-rpath,/opt/gccMaya6/lib -Wl,-Bsymbolic -Wl,--fatal-warnings -L/lib -L/usr/lib -L/rel/third_party/maya/ -lOpenMayaFX -lOpenMayaRender -lOpenMayaUI -lOpenMayaAnim -lOpenMaya -lFoundation -luuid -lm -pthread -lz -luuid -lm -pthread -lz -shared -o 

The next two snapshots show that the plugin is successfully loaded into Maya. As an alternative to loading the plugin through the UI, you can use the 'loadPlugin' MEL command:

loadPlugin "";

To exercise the circleNode plugin, run the following MEL commands.

  // Create a circle node dependency object called "circleNode1"
  // this is possible because of our new circleNode.mll!
  createNode circle -n circleNode1;
  // Create a sphere called "sphere1"
  sphere -n sphere1 -r 1;

  // Connect the sine output attribute of "circleNode1"
  // to the X position of "sphere1"
  connectAttr circleNode1.sineOutput sphere1.translateX;

  // Connect the cosine output attribute of "circleNode1"
  // to the Z position of "sphere1"
  connectAttr circleNode1.cosineOutput sphere1.translateZ;

  // Connect the output of the time slider, "time1", in frames
  // to the input of "circleNode1".
  connectAttr time1.outTime circleNode1.input;

  play -st 1; // GO!

  // "circleNode1" will now compute the X and Z positions of "sphere1"
  // as a function of the frame in the current animation. Play the scene,
  // watch the sphere move along an invisible circle.

Our circleNode plugin, as the name implies, is a "node" plugin. This means it permits the creation of a new node type, with input(s), output(s) and custom computations that occur inside a node of that type. The inputs to the node can come from other nodes, can possibly be keyed, etc. As this hypergraph snap shows, the circleNode has one input attr (circleNode.input) and two output attrs (circleNode.cosineOutput, circleNode.sineOutput). time1's 'outTime' attr is used to drive our node, and our node's twin outputs in turn drive the sphere's 'tx' and 'tz' attrs.

Can circleNode.cpp be compiled without Visual C++ (eg. using mingw, cygwin, etc.) on PCs? Possibly. Maya plugins make references to API header files and library files. If you know where these are and are comfortable with command-line compilation or using a makefile, you can try compiling plugins w/o using VC++. Note that you're not creating an executable, just a 'shared object'.