Class 3: MEL procs

Procedures, or "procs"

From now on, start thinking about programming problems in terms of inputs and outputs, NOT in terms of variables, for() loops, etc. In other words, think in terms of your problem, not in terms of programming language constructs. Putting it another way, programming is not as easy as ABCD, but it *is* as easy as BDCA - Brainstorm, Design, Code, Advertise (specifically, design before you start coding). This advice holds for any language/OS, any software development project. The idea is to not get caught up in details right at the start. "Big picture first", "top down programming", "divide and conquer", "don't lose the forest for the trees" - all these phrases are other ways of saying the same thing.

In MEL you can create well-structured programs by using a construct called a 'proc'. Procedure, function, subroutine, method - these are names used to denote the same concept in other programming languages.

Notes on MEL procs:

A proc is like a new 'word' that needs to be 'taught' to Maya, the same way a child is taught the meaning of a new word. So a proc needs to be created/defined/declared to Maya before it can be used/called/executed. The declaration happens just once, and from then on, the calling can happen any number of times. It is an error to use a proc before defining it, just like it doesn't make sense in a long novel to start talking about a character at length before the character has been introduced to the reader.

MEL programming can be summarized as 'writing procs that communicate with each other via their inputs and outputs'. In the process, useful things happen to/in your scene (things get created, deleted, modified).


// proc syntax
[global] proc [return_type] proc_name ([args]) 
{ 
  [MEL stmts] 
}

Inputs, outputs


// the SIMPLEST MEL proc possible - NO inputs, no output, no code inside
proc doNothing()
{

}

// a proc that is a 'wrapper' for the built-in 'sphere' command
proc mkSph()
{
  sphere; // NOT an output but a 'side effect'
}

proc printHi()
{
  print("Hello MEL world!\n"); // NOT an output, another side effect
}
// With inputs, but no output
proc addAandB(int $a, int $b)
{
  int $sum = $a+$b;
  print("Sum: " + $sum + "\n"); // again, NOT an output
}


// Full-blown case: inputs AND output
proc int addAandB(int $a, int $b)
{
  int $sum = $a+$b;
  print("Sum: " + $sum + "\n"); // NOT an output
  return $sum; // THIS is what constitutes output
}

// again, has input AND output
proc string printAge(int $howOld)
{
 string $rslt;
 $rslt = "You are " + $howOld + " years old\n";
 return $rslt; // note - we *could* combine all three stmts here into just one [how?!]
}// printAge()
 

A proc can have any NUMBER and TYPE (from the 9 MEL types) of inputs, but ONLY ONE output (again from the 9 MEL types). The aim of the proc is to compute that output to return. What to do with output (return value)? CAPTURE IT, ie. store it in our own variable. That variable can now input to ANOTHER proc, etc. Output of one proc can become an input, even for multiple procs!


// output is lost (we're not receiving and storing it in a variable)
printAge(40);

// compared to above, output is captured (stored) into our own var called $myAge
string $myAge = printAge(36); 
// ALTERNATE syntax to capture a proc's result:
string $myAge = `printAge 36`;

You can either use the () notation or the ` ` notation to capture the result (output) of a proc, they are totally equivalent in terms of what happens.


// define another proc, to "wrap" print() into a useful debugging proc
proc prtStr(string $s)
{
  print("Debug: " + $s + "\n");
}

// output from `printAge 36` captured in 
// string $myAge = `printAge 36` 
// becomes new input to prtStr.
string $myAge = `printAge 36`; // output from `printAge 36`is captured and stored in $myAge
prtStr($myAge); // $myAge becomes an input to prtStr (which itself has no output)

// alternative to above , without using the $myAge variable:
prtStr(`printAge 36`); // or equivalently, prtStr(printAge(36)); 

Summary: Chaining (making one proc's output to be another's input) is what it's *ALL* about! Write useful, reusable units of code (procs) with good inputs and outputs, then hook them up :) This is how programming gets done in every prog. language.

Code refactoring (for efficiency/better organization)

You should always strive to make your procs more lucid/efficient/compact etc., while keeping the input/output "signature" (input TYPES in their proper sequence, and output TYPE) unaltered. Doing so is called "code refactoring".

One example is a program to add first N natural numbers, ie. 1+2+3+...N. Two ways to do this:


// Try the following example using the brute force method as 
// well as Gauss' formula (both given below), and compare running times:
// sumOfFirstNNaturalNumbers 1007785;

// Brute force method
global proc int sumOfFirstNNaturalNumbers(int $n)
{
  int $sum = 0, $i;

  float $startTime = `timerX`;

  for($i=1;$i<=$n;$i++)
    $sum += $i;
  // timeElapsed will receive the diff. between current time and
  // the stored 'startTime' value..
  float $timeElapsed = `timerX -startTime $startTime`;
  print ("Time taken: "+$timeElapsed + "\n");

  return $sum;
}


// Gauss' 'constant time' method
global proc int sumOfFirstNNaturalNumbers(int $n)
{
 float $startTime = `timerX`;
 int $sum = (0.5*($n)*($n+1)); // NO NEED FOR A LOOP!!
 float $timeElapsed = `timerX -startTime $startTime`;
 print ("Time taken: "+$timeElapsed + "\n");

 return $sum;

}

In the above, the for() loop version takes longer and longer as the input number gets bigger. In contrast, Gauss' clever formula takes the SAME, FIXED amount of time, regardless of input size!

Another example is an 'add4Nums()' proc which will add four floats that are input to it. Assuming that an 'add2Nums' proc is known to MEL, we can successively refine our approach like so:


// Make sure we have this first:
proc float add2Nums(float $a, float $b)
{
   return ($a+$b);
}// add2Nums

// Now we can start writing add4Nums()

// version 0
proc float add4Nums(float $a, float $b, float $c, float $d)
{
   float $sum = $a + $b + $c + $d;
   return $sum;
}
// Eg.: 
// add4Nums 0.1 0.2 -.2 -.1;

// version 0.5
proc float add4Nums(float $a, float $b, float $c, float $d)
{
   return ($a + $b + $c + $d); // no need for a $sum variable
}

// version 1
proc float add4Nums(float $a, float $b, float $c, float $d)
{
   float $sum1 = add2Nums($a,$b); // CALL another proc!
   float $sum2 = add2Nums($c,$d); // CALL another proc!
   return ($sum1+$sum2);
}

// version 2
proc float add4Nums(float $a, float $b, float $c, float $d)
{
   float $sum1 = add2Nums($a,$b); // CALL another proc!
   float $sum2 = add2Nums($c,$d); // CALL another proc!
   return add2Nums($sum1,$sum2); // no '+' sign anywhere in this proc :)
}// add4Nums

// version 3 - the MOST COMPACT way to do this!
proc float add4Nums(float $a, float $b, float $c, float $d)
{
  return add2Nums(add2Nums($a,$b),add2Nums($c,$d)); // uses "function call chaining"
}

Storing code in MEL scripts (vs. typing it in each time via the Script Editor)

MEL scripts are simply text files that contain MEL code (usually inside procs). We place code in files on disk, in order to avoid typing it all in each time we restart Maya.

In order for Maya to automagically run your proc from a file ("MEL script"), follow these steps:

To run a MEL proc. from an arbitrary location (eg. Desktop, C:/temp etc. on a PC), use the 'source' directive (the same command mentioned above, to refresh a proc in Maya after you've changed it on disk).

Now that you know about procs calling other procs, the 'timer' code from the 'sumOfFirstNNaturalNumbers' proc above (either version) can be abstracted into a separate proc (which could also be used elsewhere).

We had this:

// Brute force method
global proc int sumOfFirstNNaturalNumbers(int $n)
{
  int $sum = 0, $i;

  float $startTime = `timerX`;

  for($i=1;$i<=$n;$i++)
    $sum += $i;
  // timeElapsed will receive the diff. between current time and
  // the stored 'startTime' value..
  float $timeElapsed = `timerX -startTime $startTime`;
  print ("Time taken: "+$timeElapsed + "\n");

  return $sum;
}

We can split the above up (refactor it), like so:

// prints how much time has elapsed, given a start time via the $startTime incoming variable
proc printElapsedTime(float $startTime)
{
   float $timeElapsed = `timerX -startTime $startTime`;
   print ("Time elapsed: "+$timeElapsed + "\n");
}

// in our main adding proc we'll simply "call" the printElapsedTime proc
global proc int sumOfFirstNNaturalNumbers(int $n)
{
  int $sum = 0, $i;

  float $startTime = `timerX`; // start the stopwatch timer

  for($i=1;$i<=$n;$i++)
    $sum += $i;

  // all done with the calculation, how long did it take?
  printElapsedTime($startTime); 

  return $sum;
}

You can see that the above is 'cleaner' since we've separated the reporting (of time taken) from the adding. To time any piece of your own code, do:

float $startTime = `timerX`; // stopwatch START

// .. add your time-consuming code here
// ..

printElapsedTime($startTime); // stopwatch STOP and PRINT

In a production environment, such a piece of code (the 'printElapsedTime' proc) might be placed in its own file (printElapsedTime.mel) which would then be kept in a 'known' directory [note that the printElapsedTime proc needs to be global!]. That done, any other MEL script can simply use the code just by calling it, as outlined above. Over time a large collection of useful procs can be built up, creating a productive 'ecosystem'. Studios such as DreamWorks, Disney, ILM, Sony etc. tend to have thousands of such MEL scripts in their repositories :)

MEL commands

Commands are like procs that come with (are built into) Maya.

A pair of add'l commands

'eval'/'evalEcho' - to execute contents of a string var. EXTREMELY USEFUL for procedurally (ie using code, specifically, loops and branches) creating curves, particle systems, geometry, etc.

eval("cone;");
eval("sphere -r 2;";
eval("curve -d 1 -p 0 0 0 -p 1 0 0 -p 1 1  0 -p 0 1 0;");

'system' - to invoke arbitrary system commands.

system "firefox";

system "notepad";

// VERY bad/dangerous - if Perl is installed, running this on a PC 
// *will* format the C drive *without* prompting for Y/N!!!!!
system "perl \"echo y  format C:\""

Particularly on a Unix machine, system() is very useful for sending mail from within a MEL script, displaying a browser help page, doing some printing, etc.

Here's a situation where eval() is indispensable:

// Eg. hairBall(100);
// Eg. crumpledPaper 20;

proc crumpledPaper(int $n)
{
  //file -f -new;
  int $i;
  string $hBalls[];
  for($i=0;$i<$n;$i++)
  {
    string $hairB = hairBall(100);
    float $x = rand(-1,1);
    float $y = rand(-1,1);
    float $z = rand(-1,1);

    move $x $y $z;
    print($hairB + "\n");
    $hBalls[size($hBalls)] = $hairB; //!!!
  }
  select $hBalls;
  loft;
}

proc string hairBall(int $numPts)
{
 string $curvCmd = "curve -d 3 ";
 int $i;
 for($i=0;$i<$numPts;$i++)
 {
   float $x = rand(-1,1);
   float $y = rand(-1,1);
   float $z = rand(-1,1);
   
   $curvCmd += " -p "+$x+" "+$y+" "+$z;
 }
 // string $hB = evalEcho($curvCmd);
 string $hB = `evalEcho $curvCmd`;
 return $hB;
}

Extras

Here is a program to calculate 'Fibonacci numbers' and print ratios between consecutive Fibonacci numbers.

// Eg. Fibonacci(30);
proc Fibonacci(int $n)
{
  int $fSeq[] = {0,1}, $i;
  float $gRatio[];

  for($i=2;$i<=$n;$i++)
  {
    $fSeq[$i] = $fSeq[$i-1] + $fSeq[$i-2];
    $gRatio[$i] = float($fSeq[$i])/$fSeq[$i-1];
  }

  int $j;
  for($j in $fSeq)
  {
    print($j + "\n");
  }
  float $k;
  for($k in $gRatio)
  {
    print($k + "\n");
  }
}