VSFX 705 - Programming Concepts for Visual Effects
Implementing an Interface Design in MEL
Project Summary:
Extend the code developed for the matrix assignment so that the shape it creates can be modified by a user interface (UI). It is not necessary to develop a UI that will enable all aspects of the geometries that comprise your matrix to be adjusted. Determine the "core" characteristics of your matrix that it will be interesting to have under the control of a user interface.
Results:
Click on the image to the right to see a sample render. Click to see the code in it's entirety.
The Brief
Expanding upon my original matrix code with a UI began with figuring out what aspects
of it to modify and which to leave alone. Rather than delve down into the level of each
shape and its characteristics I chose to look at it as a larger construct and the WAY it
was built; the shape of the building rather than the color of the bricks, so to speak. WIth that in mind I broke down its construction into 4 main characteristics: the number of arcs, the spread between them, the spread along local x between the constituent pieces of each arc, and the spread along local y. The number of arcs could only be edited before creation but the rotation and spread of the pieces could be edited later by connecting the controls to procedures that would traverse the hierarchy and modify each item as the sliders are changed.
The Matrix Code
At right is the the new version of my original matrix geneeration proc modified to recieve information from the UI. The procedure begins with reiterating the global variables that will be passed around between each of the procs involved in the process, reminding the generator to reference those rather than create its own variables. The procedure runs thru two loops, each using its own counter; one that creates ten individual pieces gathered into an arc and another that repeats to create a certain number of arcs based on user-input.
global proc makeMatrixType(int $geoType, int $arcs, float $rot){
global string $groupList[];
global string $arcaList[];
global string $arcbList[];
global string $groupList[];
global string $arcaList[];
global string $arcbList[];
The "arrayNum" variable generates an index number for an array used to gather all the individual pieces together for modification later. The arithmetic is set up so that the cycle of $j determines the tens place and the cycle of $i the ones place. Since there will always be 10 shapes created in each arc, this assures that they are arrnaged sequentially in the array.
for ($j=1; $j<=$arcs; $j++) {
for ($i=1.0; $i<=10; $i++) {
int $arrayNum= (($j-1)*10)+($i-1);
for ($i=1.0; $i<=10; $i++) {
int $arrayNum= (($j-1)*10)+($i-1);
A simple if statement follows to determine what primitive type is used, based on the value of $geoType which is set by a radio button selection in the UI. The shape is then named based on the arc it is in and its position in said arc. It is also added to an array list in the process.
if ($geoType == 1){
polyCone;
} else if ($geoType == 2){
polyCube;
} else if ($geoType == 3){
polyHelix -c 3 -h 2 -w 2 -r 0.2;
};
$arcaList[$arrayNum] = `rename ("arca" + $j + "_" + $i)`;
polyCone;
} else if ($geoType == 2){
polyCube;
} else if ($geoType == 3){
polyHelix -c 3 -h 2 -w 2 -r 0.2;
};
$arcaList[$arrayNum] = `rename ("arca" + $j + "_" + $i)`;
The shape is then rotated a random amount on all three axes, scaled on all three axes by an amount determined by its place in the cycle, and moved from the origin outward again based on its place in the cycle. The movement formula is based on a parabolic function so that each subsequent shape is moved further and arranged in an arc up and out. The process of creating, naming, and transforming each shape then repeats with the values modified to create the second inner arc. They too are placed in a second array for later modification.
rotate -r `rand 0 180` `rand 0 180` `rand 0 180`;
scale -a (1/$i) ($i/3) (1/$i);
move -r ((($i/1.5)*($i/1.5))/2) ($i*1.7) 0;
if ($geoType == 1){
polyCone;
} else if ($geoType == 2){
polyCube;
} else if ($geoType == 3){
polyHelix -c 3 -h 2 -w 2 -r 0.2;
};
$arcbList[$arrayNum] = `rename ("arcb" + $j + "_" + $i)`;
rotate -r `rand 0 180` `rand 0 180` `rand 0 180`;
scale -a (1/$i) ($i/3) (1/$i);
move -r ((($i/2)*($i/2))/4) ($i*1.7) 0;
}
scale -a (1/$i) ($i/3) (1/$i);
move -r ((($i/1.5)*($i/1.5))/2) ($i*1.7) 0;
if ($geoType == 1){
polyCone;
} else if ($geoType == 2){
polyCube;
} else if ($geoType == 3){
polyHelix -c 3 -h 2 -w 2 -r 0.2;
};
$arcbList[$arrayNum] = `rename ("arcb" + $j + "_" + $i)`;
rotate -r `rand 0 180` `rand 0 180` `rand 0 180`;
scale -a (1/$i) ($i/3) (1/$i);
move -r ((($i/2)*($i/2))/4) ($i*1.7) 0;
}
Once the loop all of the pieces completes all those most recently built are selected and grouped into an arc that stored in a third array. This way each group of arcs can be modified as a whole and the pieces can be modified individually. The proc then selects all of the groups built thus far, makes sure their pivots are moved to the origin, and rotates them by the value set by the user. The entire process is then repeated to generate the remaining arcs.
select ("arc*" + $j + "_*");
$groupList[$j-1] = `group`;
select "group*";
xform -piv 0 0 0;
rotate - p 0 0 0 -r 0 $rot 0;
}
}
$groupList[$j-1] = `group`;
select "group*";
xform -piv 0 0 0;
rotate - p 0 0 0 -r 0 $rot 0;
}
}
The Rotation Code
This is the code that controls the rotation of each arc generated, adjusting the spread between each. The code appears as two identical procs, updateRot() and dragRot(), each one plugged into the appropriate command for the slider.It first references the global variables that hold the information it needs. The $numGroups variable uses the "size" command on the array that holds the arc group names to determine how many arcs were generated. This is used to determine how many times the following transform must be executed to adjust all the arcs.
The amount of rotation is gathered from the current value of the rotation slider, held in the variable $rotV. The setAttr command is the part that actually adjusts the rotation of each group. The value of the rotation slider is modified by the cycle of $i so that each subsequent group will rotate further, causing them to spread out.
//================
//update matrix when rotation slider is updated or dragged
//================
global proc updateRot() {
global string $rotSlider;
global string $groupList[];
$numGroups = size($groupList);
float $rotV =`floatSliderGrp -q -v $rotSlider`;
select -cl;
for ($i=0; $i<$numGroups; $i++){
setAttr ($groupList[$i] + ".rotateY") ($rotV*($i+1));
}
}
//update matrix when rotation slider is updated or dragged
//================
global proc updateRot() {
global string $rotSlider;
global string $groupList[];
$numGroups = size($groupList);
float $rotV =`floatSliderGrp -q -v $rotSlider`;
select -cl;
for ($i=0; $i<$numGroups; $i++){
setAttr ($groupList[$i] + ".rotateY") ($rotV*($i+1));
}
}
The X Position Code
This is the code that controls the local x position of each individual shape generated, adjusting the spread between each. As before, the code appears as two identical procs, updateX() and dragX(), each one plugged into the appropriate command for the slider.It first references the global variables that hold the information it needs. This time two size-based variable are needed, one for each of the arrays holding the names of the shapes in each arc. This area of the code presented a particularly puzzling problem. In previous portions of the code I was able to use the cycling value of $i to determine the repetitive movement. However, with so many arcs being generated the list of shapes could easily reach above 50 or more. This caused each shape to move further and further from the origin throughout the cycle. The first shape would remain at the origin but rather than the values returning there for the next arc group it would keep increasing along the Y, distorting the groups and the matrix. For instance, shape number 43, the third shape in the fourth arc, in the list would be translated 43 times the value of the slider up in Y rather than only three as it should. At first I tried to solve this mathematically but because of the cyclical nature of the values there was no formula that would properly subtract the necessary amount from the count to return it to a value from 0-9. Instead I used the "endString" command to return the last digit from the count and used that to multiply the slider value. That way 43 would read as 3, 57 as 7, and so on; all of the third shapes in each arc would move 3 times the slider amount, the seventh shapes would 7 times the amount, etc.
This iteratively acts on every shape within each arc group, moving each on relative to its group local pivot.The same parabolic-based formula is used to move each shape further than the last, spreading them out. The inner and outer arcs receive slightly different formulas to ensure they remain distinct as two arcs and don't jumble together.
//================
//update matrix when x position slider is updated
//================
global proc updateX() {
global string $xtranSlider;
global string $arcaList[];
global string $arcbList[];
$numArca = size($arcaList);
$numArcb = size($arcbList);
float $xtranV =`floatSliderGrp -q -v $xtranSlider`;
select -cl;
for ($i=0; $i<$numArca; $i++){
int $ones = endString ($i, 1);
setAttr ($arcaList[$i] + ".translateX")
((($ones*$xtranV)*($ones*$xtranV))/32);
}
select -cl;
for ($i=0; $i<$numArcb; $i++){
int $ones = endString ($i, 1);
setAttr ($arcbList[$i] + ".translateX")
((($ones*$xtranV)*($ones*$xtranV))/64);
}
}
//update matrix when x position slider is updated
//================
global proc updateX() {
global string $xtranSlider;
global string $arcaList[];
global string $arcbList[];
$numArca = size($arcaList);
$numArcb = size($arcbList);
float $xtranV =`floatSliderGrp -q -v $xtranSlider`;
select -cl;
for ($i=0; $i<$numArca; $i++){
int $ones = endString ($i, 1);
setAttr ($arcaList[$i] + ".translateX")
((($ones*$xtranV)*($ones*$xtranV))/32);
}
select -cl;
for ($i=0; $i<$numArcb; $i++){
int $ones = endString ($i, 1);
setAttr ($arcbList[$i] + ".translateX")
((($ones*$xtranV)*($ones*$xtranV))/64);
}
}
The Y Position Code
This code acts the same way as the X Position code does with slight modifications to retain the parabolic shape of the arcs. It too is plugged into the appropriate slider commands and gathers the variables it needs.
//================
//update matrix when x position slider is updated
//================
global proc updateX() {
global string $ytranSlider;
global string $arcaList[];
global string $arcbList[];
$numArca = size($arcaList);
$numArcb = size($arcbList);
float $ytranV =`floatSliderGrp -q -v $ytranSlider`;
select -cl;
for ($i=0; $i<$numArca; $i++){
int $ones = endString ($i, 1);
setAttr ($arcaList[$i] + ".translateY") ($ones*$ytranV);
}
select -cl;
for ($i=0; $i<$numArcb; $i++){
int $ones = endString ($i, 1);
setAttr ($arcbList[$i] + ".translateY") ($ones*$xtranV);
}
}
//update matrix when x position slider is updated
//================
global proc updateX() {
global string $ytranSlider;
global string $arcaList[];
global string $arcbList[];
$numArca = size($arcaList);
$numArcb = size($arcbList);
float $ytranV =`floatSliderGrp -q -v $ytranSlider`;
select -cl;
for ($i=0; $i<$numArca; $i++){
int $ones = endString ($i, 1);
setAttr ($arcaList[$i] + ".translateY") ($ones*$ytranV);
}
select -cl;
for ($i=0; $i<$numArcb; $i++){
int $ones = endString ($i, 1);
setAttr ($arcbList[$i] + ".translateY") ($ones*$xtranV);
}
}
The Clearing Code
After testing the UI I found that the arrays used to store arc and shape names would retain their old size, causing the modification procs to looks for items that no longer existed and parts of it to break down. I wrote this simple proc to select and delete all the current shapes and clear the arrays from memory is found below.
//================
//clear old matrices and array information to prevent conflicts
//================
global proc clearOld(){
global string $groupList[];
global string $arcaList[];
global string $arcbList[];
select "group*";
delete;
clear $groupList;
clear $arcaList;
clear $arcbList;
}
//clear old matrices and array information to prevent conflicts
//================
global proc clearOld(){
global string $groupList[];
global string $arcaList[];
global string $arcbList[];
select "group*";
delete;
clear $groupList;
clear $arcaList;
clear $arcbList;
}
The UI Code
Now on to generating the UI that will control the matrix. As before it first gathers the global variables that it will need and declares an internal variable called $buildType that will store what type of geometry the user selects for the matrix.The next portion checks to see if the matrix window is already present and, if so, deletes it and its preference settings. This makes sure the window is always up to date and visible each time the proc is called.
Next, the construction of the window begins, including the sliders for user input. Each of these is stored in a variable for easy access when called upon later by the other procs. A radio button group allows the user to pick a primitive to generate the matrix from. Upon clicking on one of the buttons the $buildType variable is altered to be passed to if statement found in the generation proc.
Two buttons follow, one to begin the matrix generation and another to clear the old matrices.
Finally a help line item is added to provide some feedback for the user and instruct them on the effects of each slider. It pulls information from the "-ann" annotation flags found in each control.
//================
//generate window UI with controls
//================
global proc testUI(){
global string $rotSlider;
global string $arcSlider;
global string $xtranSlider;
global string $ytranSlider;
global float $rotV;
global int $buildType = 1;
if (`window -exists matrixWin`){
deleteUI -window matrixWin;
};
if (`windowPref -exists matrixWin`){
windowPref -remove matrixWin;
};
window -title "Matrix Generation" -rtf 1 -wh 430 160 -s false
matrixWin;
columnLayout;
$arcSlider = `intSliderGrp
-label "Arcs"
-columnAttach 1 "left" 0
-field true
-min 1
-max 20
-value 12
-ann "Number of arcs to generate. Not live."`;
$rotSlider = `floatSliderGrp
-label "Rotation"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 360
-value 20
-ann "Rotation value for spread between arcs."
-changeCommand "updateRot()"
-dragCommand "dragRot()"`;
$xtranSlider = `floatSliderGrp
-label "X Position"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 5
-value 1
-ann "X position value for spread between shapes."
-changeCommand "updateX()"
-dragCommand "dragX()"`;
$ytranSlider = `floatSliderGrp
-label "Y Position"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 5
-value 1
-ann "Y position value for spread between shapes."
-changeCommand "updateY()"
-dragCommand "dragY()"`;
$geoRadio = `radioButtonGrp
-label "Geometry Type"
-columnAttach 1 "left" 0
-nrb 3
-sl 1
-la3 "Cone" "Cube" "Helix"
-on1 "$buildType = 1"
-on2 "$buildType = 2"
-on3 "$buildType = 3"
-ann "Type of geometry primitive used in generation.
Not live."`;
setParent..;
rowLayout -nc 2;
button -label "Generate" -command "makeMatrixType($buildType,
`intSliderGrp -q -v $arcSlider`,
`floatSliderGrp -q -v $rotSlider`)"
-ann "Generate a matrix using the current arc number,
rotation, and shape type.";
button -label "Clear matricies" -command ("clearOld()")
-ann "Clear old matrices and their array information
to prevent conflicts.";
setParent..;
frameLayout -lv false -cll false -w 430 -h 25;
helpLine;
showWindow matrixWin;
}
//generate window UI with controls
//================
global proc testUI(){
global string $rotSlider;
global string $arcSlider;
global string $xtranSlider;
global string $ytranSlider;
global float $rotV;
global int $buildType = 1;
if (`window -exists matrixWin`){
deleteUI -window matrixWin;
};
if (`windowPref -exists matrixWin`){
windowPref -remove matrixWin;
};
window -title "Matrix Generation" -rtf 1 -wh 430 160 -s false
matrixWin;
columnLayout;
$arcSlider = `intSliderGrp
-label "Arcs"
-columnAttach 1 "left" 0
-field true
-min 1
-max 20
-value 12
-ann "Number of arcs to generate. Not live."`;
$rotSlider = `floatSliderGrp
-label "Rotation"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 360
-value 20
-ann "Rotation value for spread between arcs."
-changeCommand "updateRot()"
-dragCommand "dragRot()"`;
$xtranSlider = `floatSliderGrp
-label "X Position"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 5
-value 1
-ann "X position value for spread between shapes."
-changeCommand "updateX()"
-dragCommand "dragX()"`;
$ytranSlider = `floatSliderGrp
-label "Y Position"
-columnAttach 1 "left" 0
-field true
-min 0.1
-max 5
-value 1
-ann "Y position value for spread between shapes."
-changeCommand "updateY()"
-dragCommand "dragY()"`;
$geoRadio = `radioButtonGrp
-label "Geometry Type"
-columnAttach 1 "left" 0
-nrb 3
-sl 1
-la3 "Cone" "Cube" "Helix"
-on1 "$buildType = 1"
-on2 "$buildType = 2"
-on3 "$buildType = 3"
-ann "Type of geometry primitive used in generation.
Not live."`;
setParent..;
rowLayout -nc 2;
button -label "Generate" -command "makeMatrixType($buildType,
`intSliderGrp -q -v $arcSlider`,
`floatSliderGrp -q -v $rotSlider`)"
-ann "Generate a matrix using the current arc number,
rotation, and shape type.";
button -label "Clear matricies" -command ("clearOld()")
-ann "Clear old matrices and their array information
to prevent conflicts.";
setParent..;
frameLayout -lv false -cll false -w 430 -h 25;
helpLine;
showWindow matrixWin;
}
The Final Product
Click on the images below to see examples of geometry generated with the procedure. These images show off the real strength of the procedure: the countless variations that can be generated by adjusting the settings, both before and after creation.
Improvements
Due to the type of scaling performed on the primitives they all tend to end up as the same long thorn shape at the end of each arc. while the "trunks" of the formations remain mostly unique to the type of primitive. Further expansion of the procedure could alter the case statements to perform transforms specific to each primitive type so that the outer ends remain distinctive.Currently the shapes used in the procedure are limited to three primitives. The obvious expansion there would be to increase the number of primitives available to the generator, something that would be a relatively simple change. An incredibly more challenging one would be allowing the user to choose their own model to plug in. This would immediately have to address issues of not only importing, naming, and grouping the model correctly but also ensuring that it would be in the correct coordinate space so that the transforms work correctly.
There are several other smaller tweaks that would refine the functionality. They include allowing the user to select the number of shapes within each arc, set boundaries on the random values used for the rotations and scaling of each shape, modifying the transform portions to gather the slider values before generation, or adding buttons to produce test renders of completed matricies.
One thing that I was sure I was not going to try to tackle is updating the number of arcs or the primitives used after generation. Such a change would involve a wholesale rebuilding of all the procs because it would necessitate deleting or adding new groups on the fly and changing the rest of the already-built shapes to match. Replacing each shape might involve using instancing of a "hero" primitive and placing each within its own group so that the shape could be swapped out while the transforms placed on it remain. Adding new arcs would involve storing information on the location, transforms, count, and cumulative shape of all the arcs so that future ones would be tacked onto the end of the arrangement.
