VSFX 160 - Introduction to Visual Effects Programming

MEL Scripting Culmination - Previs Tree Generator


Project Summary:
Using all of the concepts and theory practiced earlier in the class create a complex script that creates or acts as a tool. The script should help solve some problem or generate geometry or rigs useful to others in the visual effects field.

Results:
Click on the image to the right to see a demo video. Please note, due to the length and nature of the demo the quality has been turned down to save on load times. To see the code in its entirety, click here.


The Brief

My use of random and noise functions in other projects piqued my already strong curiosity in the ability to generate organic forms. That, coupled with my fondness for the efficiency of recursive functions, prompted to try my hand at building a tree generator. Once that part was solved I decided to take that one step further and create a forest of trees, again thru recursion. Knowing full well that this was first introduction to MEL scripting I knew from the outset that this would not be a photoreal kind of project and decided to aim the script towards a previz application. With that in mind, I tried to keep things as light-weight and low-poly as possible.

Stills from each of the 'seasons'. With those parameters in mind, I set to work creating a procedure containing a for loop to generate each "tree" and another to repeat that loop to create the field. Duplicating this base procedure four times and altering key characteristics of it would create fields with details distinct to each season. The only real change to the building procedure occured in the particle group attached to the top of each tree, winter was the only exception with no particles. The number of trees and the space between them would be gathered from the UI at construction time to fit with the users needs.

Each tree consists of 10 randomly placed locators, a curve whose points match and are parented to the locators' positions, a circle, an extruded surface made from the circle and the curve, and a particle field parented to the top locator used to simulate the leaves. Because of the way the script builds each component, every tree and its characteristics are editable even after the script has finished. The procedures build each tree and parents its components under a larger locator that enables the user to move the group as a whole.

Please note, the code below has line breaks dictated by space for the page.

The Tree Code

This first portion of the code declares the proc, set the first loop into motion, and prepares the variables needed to pass and link attribute values between the curve and locators. Also created is variable created to store the name of the curve so that point can be added to it as the loops progress.
//Spring building procedure
   proc buildSpring (int $iter) {
      //create locators and curve to match
      for ($i = 0; $i < 10; $i++) {
         $locXVal = "locator" + $i + ".tx";
         $locYVal = "locator" + $i + ".ty";
         $locZVal = "locator" + $i + ".tz";
         $curvePtXVal = "curveShape1" + ".controlPoints[" + $i + "].xValue";
         $curvePtYVal = "curveShape1" + ".controlPoints[" + $i + "].yValue";
         $curvePtZVal = "curveShape1" + ".controlPoints[" + $i + "].zValue";
         string $newCurve;

The next section begins by creating the first locator at the origin. It is created outside of the following loop to ensure that it remains at the origin.

All of the subsequent locators will be created at the origin and then moved to a random spot based on the value of $i. The modfications are arranged in such a way to ensure that each new locators moves up in Y further than the previous one and the tree doesn't double back on itself.

Next, the code checks to see if the curve has already been created. If not, it creates it and stores its name; if so, it adds the new point to it and places that point at the same coordinates as its matching locator.
         spaceLocator -name ("locator" + $i) -p 0 0 0;
         if (`objExists locator1`) {
            move `rand -2 2` `rand ($i*2) (($i*2)+2)` `rand -2 2`;
         };

         if (`objExists curve1`) {
            curve -a -p (`getAttr $locXVal`) (`getAttr $locYVal`)
               (`getAttr $locZVal`) $newCurve;
         } else {
            $newCurve = `curve -p 0 0 0`;
         };

         connectAttr -f $locXVal $curvePtXVal;
         connectAttr -f $locYVal $curvePtYVal;
         connectAttr -f $locZVal $curvePtZVal;
      }

Once the curve and locators are completed they are renamed so that they aren't confused with those yet to be created. The circle is then created and extruded along the curve, tapering along the way.

An expression is built based on the current loop cycles that will be attached to the particle object to give it its characteristics. The base particle is created and attached to the top locator in the chain ensuring that the leaves never move away from the top of the tree. Those attributes that can be created explicitly for the particles are and the expression is attached to drive the rest.
      //rename locators and curve
      rename curve1 ("compCurve" + $iter);
      for ($i = 0; $i < 10; $i++) {
         rename ("locator" + $i) ("comp" + $iter + "Locator" + $i);
      }

      //create circle and extrude
      circle -c 0 0 0 -nr 0 1 0 -d 3 -s 12 -ch 1 -n ("compCircle" + $iter);
      extrude -ch true -rn true -po 0 -rotation 0 -scale 0 -rsp 1 -n
         ("compExtrude" + $iter) ("compCircle" + $iter) ("compCurve" + $iter);

      //create particle and expression and link them to the end locator
      string $particleName = "compParticle" + $iter;
      string $expString = $particleName + "Shape.rgbPP = <<`rand 0.4 1`, 0,
         `rand .3 .8`>>;\n" + $particleName + "Shape.radius = .1";

      particle -jbp 0 0 0 -nj 600 -jr 7 -c 1 -n $particleName;
      connectAttr -f ("comp" + $iter + "Locator9.translateX")
         ($particleName + ".translateX");
      connectAttr -f ("comp" + $iter + "Locator9.translateY")
         ($particleName + ".translateY");
      connectAttr -f ("comp" + $iter + "Locator9.translateZ")
         ($particleName + ".translateZ");
      setAttr ($particleName + "Shape.particleRenderType") 4;
      addAttr -is true -ln "radius" -at "float" -min 0 -max 10 -dv 0.5
         ($particleName + "Shape");
      addAttr -ln "rgbPP" -dt vectorArray ($particleName + "Shape");
      addAttr -ln "rgbPP0" -dt vectorArray ($particleName + "Shape");
      dynExpression -s $expString -c ($particleName + "Shape");

In order to keep the entire tree together, an "overall" locator is created. All of the consituent pieces of the tree are selected, grouped, and parented under this locator.
      //create overall locator for entire group
      spaceLocator -n ("overLocator" + $iter);
      setAttr ("overLocator" + $iter + "Shape.localScaleX") 5;
      setAttr ("overLocator" + $iter + "Shape.localScaleY") 5;
      setAttr ("overLocator" + $iter + "Shape.localScaleZ") 5;

      //select components
      select ("compCurve" + $iter) ("compCircle" + $iter)
         ("compExtrude" + $iter) $particleName;
      for ($i = 0; $i < 10; $i++) {
         select -add ("comp" + $iter + "Locator" + $i);
      }

      //parent components under overall locator
      group -n ("compGroup" + $iter) -p ("overLocator" + $iter);
}

The Field Code

The field building procedure is much simpler in construction and in fact the four sections of it differ only in the tree building proc that they call and then reiterate until they reach the limit set by the user. After building each tree it is moved to a random spot on the grid within the users parameters and scaled a random amount. The process then repeats. I have trimmed the code down to one season here for space.
//Field building procedure, calls specific season procedures based on user input
proc buildField (int $buildType, int $buildNum, int $xSpread, int $zSpread) {
   if ($buildType == 1) {
      currentTime 1;
      for ($i = 1; $i < ($buildNum + 1); $i++) {
         buildSpring($i);
         select ("overLocator" + $i);
         move `rand ($xSpread * -1) $xSpread` 0 `rand ($zSpread * -1) $zSpread`;
         $randScale = `rand .5 1`;
         setAttr ("overLocator" + $i + ".scaleX") $randScale;
         setAttr ("overLocator" + $i + ".scaleY") $randScale;
         setAttr ("overLocator" + $i + ".scaleZ") $randScale;
      };
      print "Build Spring";
   } . . .
   . . .
   select -cl;
}

The UI Code

The UI building code begins with two if statements that check to see if the window already exists so that, if it does, it and its corresponding prefs into is deleted and rebuilt. This is done to ensure that it is current and visible if it has been accidentally moved out of the way. Next the necessary global variables are accessed so that information can pass to and from the other procs.

Next, the window itself is called and three sliders are added and stored within a variable. These control the number of trees and the spread along the X and Y axes. Also added is a radio button group to allow the user to choose which season they would like to build. Clicking one of buttons changes the value of a variable called $buildType that the field building proc will evaluate to determine the correct season. Finally, a button contains the proc call with information gathered from the controls to pass as arguements.
//procedure to build the UI
global proc buildBoxUI() {
   if(`window -exists buildBox`){
      deleteUI buildBox;
   }
   if(`windowPref -exists buildBox`){
      windowPref -remove buildBox;
   }

   global string $countSlider;
   global string $spreadXSlider;
   global string $spreadZSlider;
   global int $buildType = 3;

   window -title "8-bit Tree Generator" -rtf true buildBox;
   columnLayout;
   $countSlider = `intSliderGrp -cat 1 left 0 -min 1 -fmn 1 -max 200
      -value 10 -label "Tree Count" -field true countSlider`;
   $spreadXSlider = `floatSliderGrp -cat 1 left 0 -pre 2 -min 5 -fmn 5 -max 50
      -fmx 50 -value 10 -label "Field X-Spread" -field true spreadXSlider`;
   $spreadZSlider = `floatSliderGrp -cat 1 left 0 -pre 2 -min 5 -fmn 5 -max 50
      -fmx 50 -value 10 -label "Field Z-Spread" -field true spreadZSlider`;
   setParent..;
   columnLayout;
   radioButtonGrp -cal 1 left -nrb 4 -label "Season"
      -labelArray4 "Spring" "Summer" "Fall" "Winter"
   -on1 "$buildType = 1" -on2 "$buildType = 2"
      -on3 "$buildType = 3" -on4 "$buildType = 4";
   setParent..;
   rowColumnLayout -nc 1;
   button -label "Build Field" -command "buildField($buildType,
      `intSliderGrp -q -value $countSlider`,
      `floatSliderGrp -q -value $spreadXSlider`,
      `floatSliderGrp -q -value $spreadZSlider`)";
   showWindow;
};