N-Gon Parametric Shape
Good day budding ASL gurus. Today we'll learn how to make a parametric shape plugin! This tutorial assumes you've already completed the
Hello World! tutorial, so if you haven't done it yet and have never scripted before, you should complete that tutorial first.
What is a Parametric Shape?In Anim8or, a parametric shape is a 3D mesh whose form and structure changes based on the parameters set by the user. This could be as simple as scaling a pre-made mesh or as complex as a generating a planet! There are a lot of awesome things that someone with a little scripting know-how can accomplish.
In ASL, parametric shapes are made much like any other script. You have your header with any comments, the directive(s), the variable declarations, and then the code to create the shape.
Fleshing out the frameworkWe'll be creating an N-Gon, much like Anim8or's native N-Gon shape, except already filled in as a mesh with UV coordinates.
So let's create ourselves a quick framework for parametric shapes so that we can come in afterwards and 'fill in the blanks'. Type this code into a new file and save the file as 'N-Gon.a8s'
HeaderGo ahead and change the header with information pertaining to you and the script. Change "Parametric Shape Plugin" to the name of the shape ("N-Gon Shape Plugin", perhaps). The author is you, and the rest is optional, dependent on what type of script it is and how much you want to put into it.
#Plugin DirectiveLike with any script, the first command must be the directive telling Anim8or what the script is. For plugins, this is the #plugin("<editor>", "<type>", "<name>") directive. Go ahead and change the name to something like "N-Gon".
ASL Snippet
#plugin("object", "mesh", "N-Gon");
Parameter DirectivesFollowing that, we have parameters! These are the input fields that show up when you double-click the shape. The #parameter directives allow you to define the parameters of the parameters (hah).
For example,
ASL Snippet
#parameter("<name>", <type>, <starting value>, <floor value>, <ceiling value>, <optional params>);
#parameter("Scale", float, 1, 0.01, 10000, scale);
The label to the left of the input field will be titled "Scale", it'll be a float value (decimal number), starting with the number 1, and can range from 0.001 to 10000. Additionally, the Uniform Scale tool increases/decreases this value when it's used on the shape.
#Return DirectiveThe #return directive contains the shape that we're working on. This must be globally declared (see below the #button directive). If you want to make it more context-specific, you can change the variable from $S to something like $nGonShape or something. Just remember that you have to use this variable everywhere instead, and to change the global variable declaration. I tend to use very short variable names for those variables that will be referenced *a lot* in the script, possibly hundreds of times.
#button DirectiveThis maps out what the button looks like.
ASL Snippet
#button(<width>, <height>, <number of colors>, <data>);
If you're using Kubajzz's
ASL Editor, you can flesh this out using its built-in button editor. It's a little complicated otherwise, and outside the scope of this tutorial.
Global VariablesAlright, variables! In a lot of programming languages, there's something called variable scope. When you declare variables outside of any functions (we cover functions after this) at the beginning, it becomes accessible to all the functions. Such variables are called global variables, since they can be accessed and/or changed everywhere in the script. However, if you declare the variable inside a function, it can only be accessed and/or changed from within that function. Such variables are called local variables.
Because the #return directive required it, we declared the $S shape data type here. And in case we needed to access $output and $obj from within any custom functions, they were declared globally as well.
FunctionsIn the "Hello World!" tutorial, the script didn't have fancy functions. ASL is (currently) a procedural programming language. This means all the commands are interpreted from top to bottom, and that's how you program it. Functions add a little modularity to it, allowing you to program a block of code that can be called at any point in the script, for as many times as needed, according to parameters that you define.
In Anim8or, a function is structured like so:
ASL Snippet
<type> $<name> (<parameters>){}
int $RoundDown (float $a)
{
return $a;
}
<type> is the type of data that'll be returned when you use the function in code. The type can be nearly anything, such as int, float, point2, point3, string, float4x4. For $RoundDown, int is the type. <name> needs to be structured the same as variables, with a dollar sign preceding it. It's conventional to make function names as uppercase CamelCase. That is, the beginning of every word, including the first one, is uppercase, whereas everything else is undercase. With variables, it's lowercase CamelCase, where the first word's first letter is lowercase and every subsequent word's first letter is uppercase.
The parameters are basically local variables that are created on the spot, as copies of whatever data that is input. For example , if $RoundDown(1.333); was used, $a is a variable that holds the value 1.333 within that function. So in the following example,
ASL Snippet
float $plusOnePointOne (float $a)
{
$a = $a + 1.1;
return $a;
}
$a is a local variable that can be accessed anywhere within that function. So if $plusOnePointOne(1.0) was used, $a holds the value of 1.0 (a float), and in the function $a gets re-assigned as the old value of $a (that is, 1.0), plus 1.1. 1.1 + 1.0 = 2.1, which would be the result.
Notice the "return $a;" line. All functions (except those with a "void" type) require a return command that returns a value of the same type as that function. That's the point of declaring the type of the function in the first place! It can be a variable of the same type as the function, or it can be a value by itself that is of the same type. For example, "return 1.5;" would work instead of "return $a"...except that every time $plusOnePointOne() is called, it'll give the value of 1.5 regardless of the parameters used.
The $main() FunctionNow, look further down in the template and find the $main() function. This is where the meat of the script needs to reside. Note the "void" data type. This means that this function doesn't return anything--it only performs actions on data that is already there (or locally created). You can create your own void-type functions to manipulate global data if need be, but the $main() function must be of the type void.
Scripts can run without a $main() function. As mentioned before, it'll just interpret the script procedurally from top to bottom. However, if you decide that you need functions, you must have a $main() function. Additionally, you can only have directives, custom functions, and globally declared variables outside of the $main() function. You can't have a command, (such as $output.print("Print Me!")
outside and roaming alone.
It should go without saying, but the $main function is what gets run when the script is executed. All other functions get compiled, but unless it's called from the $main function, it will never get used. The same goes for global variables.
Assigning Parameters to VariablesASL has a specific function for pulling info from the parameters you specified in the #parameter directive. It's simply:
The name must be the exact same as the name in the #parameter directive. Also, the type of the variable must be exactly the same as the the type indicated in the directive, or else you'll come across errors. Note, however, that you can access the value without assigning it, just by plugging in parameter("<name>"), anywhere a normal variable or value would go. Generally it's easier to just assign it to a variable at the start and use that variable where needed.
Another note: You cannot directly change parameters via code, they can only be read from what the user input.
Opening and Closing ShapesIf you wish to edit and add onto or subtract from shapes, you have to open them first. And then when you're done, close them! So make sure you're using .Open() before you try editing a shape, and .Close() before the script ends. Simple right?
Creating the N-Gon ShapeBefore you can code a parametric shape, you need to have some understanding of how to go about programming the logic of it.
What is an N-Gon?An N-Gon is really just a single polygon with multiple points spread evenly in a circular fashion around its origin. Basically, a shape with N amount of sides. Got 4 sides? That's a square. Got 8 sides? An Octagon. Got 100 sides? Well, might as well call it a circle.
A bit of math is usually required to figure out where each point goes on the circle. If you have any experience with trigonometry, you'll know that sine and cosine are the keys to this riddle. Don't know trig? Well, here's a quick explanation (not really an explanation, just a way for you to remember what sin and cosine achieve).
What cosine and sine do is give the X and Y coordinates of a circle when you specify the angle (assume the circle starts on the positive X-axis, and the radius is 1.0). You just have to remember that cosine = X-Coordinates, sine = Y-Coordinates. You can practice with a calculator and the circle image above. Use sine and cosine on any degrees you want, and plot it on the grid above. You'll find it'll always rest on the circle!
Anim8or's sin() and cos() functions deal in radians. There are PI (3.14) radians in 180 degrees. So 2*PI radians in 360 degrees. In our code, we're going to be manipulating points based on fractions of a circle (for example, if we want 5 sides, a point will be placed every 1/5 of the circle). So when we figure out what fraction of the circle we need, we just multiply that fraction by 2*PI.
How are shapes made?When you think about creating a parametric shape via code, you should be thinking about these things:
- What math/formulas/algorithms/patterns do I need to plot out the points in 3D space?
- How do I connect these points together to make faces?
- How do I manage the UV coordinates?
- How do I tie in the parameters?
- Materials, textures, etc?
We'll be doing the first four of those items in this tutorial.
Time To Code!Plotting points with mathTime to start coding! Generally, when creating a parametric shape, you plot out the points using whatever formulas or patterns you figured out, into 3D space. You do this using the point3 data type. A "point3" variable holds an x, y, and z float value (it's also known as a vector).
So in the $main() function, we need to declare a local point3 variable. In a new line under the float variable declarations, add this:
Woah! It's not just a variable, it's an array! If a variable is declared with a [number] after it, such as in the line above, that means it's an array. An array is basically a variable that holds many variables of the same type inside it, accessed by the number specified. Kind of like if you went to a library and needed a book off the shelf, you'd find it by using the catalog number it's sorted by. We call this number the index, and indexes in Anim8or start on 0 instead of 1.
When we declare an array, we must also set the array size. In some programming languages, it's required to allocate the entire size of an array first. Fortunately for us, Anim8or allows us to add or subtract from an array whenever we want. In the line above, we set the size to zero (empty), since we'll be adding to it later. If we wanted to start it with 25 null elements, we'd write "point3 $p[25]".
Let's go ahead and create the number-of-sides variable, since we'll be playing with that as well. I'm calling it "$numSides". It's an int(eger) type, since we can only deal with whole numbers when describing the number of sides of a shape. We'll also need an index variable for code later, so add $i to the int declarations. Go ahead and add $math to the float variables as well. Your local variables should look something like:
ASL Snippet
/* Local Variables */
float $s, $xs, $ys, $zs, $math;
int $numSides, $i;
point3 $p[0];
Since we want the user to be able to input the number of sides, let's start being creative. The N-Gon will exist on the X and Y plane, meaning that the Z-axis is going to be totally ignored. This means we can swap the "Z Scale" parameter for the number of sides instead.
Go back to the top of the script, and change this line:
ASL Snippet
#parameter("Z Scale", float, 1, 0.01, 10000, scale_z);
to this:
ASL Snippet
#parameter("Number of Sides", float, 4, 3, 250, scale_z);
What we just did replace the "Z Scale" parameter with one for the number of sides. It's a float value (scale_z only works with float types), starting with four sides, with a minimum of three sides and a max of 250 (250 is an internal limit in Anim8or for the number of sides a face can have -- See
this post for a method to bypass this limit). It's going to seem a little weird since this means you can set a decimal number of sides, such as 4
.37, but the decimal part will be ignored in the code. You don't have to make this into a scalable parameter and instead make it an int-type parameter, but it's fun to be able to right-click and drag to increase/decrease the number of sides
Now go back in the $main() function and replace this line:
ASL Snippet
$zs = parameter("Z Scale");
with:
ASL Snippet
$numSides = parameter("Number of Sides");
What this does is read the float value of the "Number of Sides" parameter, and save it as an integer to the $numSides integer variable (it rounds it down, lopping off the decimal portion).
You can delete the ", $zs" out of the local variable declarations since we no longer need it.
Alright, we're ready to start plotting those points. Go down to the area where the main shape creation code will go, after "$S.Open();". Add the following lines:
ASL Snippet
/* Main shape creation code goes here */
for $i = 0 to $numSides - 1 do
{
}
This is called a "for" loop. It's a way to repeat the same code over and over until whenever it's told to stop. It increments the initial variable (in our case, $i) every time the loop happens. So the above statement is saying "$i starts at 0. Execute the following loop until $i equals $numSides minus 1".
In any shape, the number of sides is equal to its number of points. Hence we're plotting the same number of points as the number of sides. We're starting at 0 instead of 1 to make it simpler for later code, which is why we have to do the number of sides
minus 1.
Now, add this line of code inside the for loop:
"$p" is the array that holds the points. ".push()" is a function that adds a new element to the array, containing whatever data is within the parenthesis. In our case, it's (0, 0, 0), which is the x, y, and z coordinates, in that order.
Well, (0, 0, 0) is the origin...we want each point at their rightful place on the circle! So it's time for that cosine and sine math. As a bit of an optimization, instead of doing all the math inside the for loop, let's do as much of it before the for loop as possible. Replace that line with:
ASL Snippet
$p.push((cos($i*$math), sin($i*$math), 0));
Before the for loop, after the $S.Open(); line, add this line:
ASL Snippet
$math = 2.0*PI/$numSides;
What we're doing is pre-computing as much of it as possible so that it doesn't have to compute it over and over every time the for loop iterates. It's not necessarily important considering how fast computers these days compute things, but it's good practice especially if you have code that needs to iterate thousands upon thousands of times.
Remember the whole fractions of circles bit that I mentioned in the trig part of this tutorial? This is it. It's not very obvious, right? Well, for the purpose of explanation, if you substituted "$math" in the for loop with "2.0*PI/$numSides", and moved the operands around, you'd have something like this (don't change to this in the code):
ASL Snippet
$p.push((cos(($i/$numSides)*2*PI), sin(($i/$numSides)*2*PI), 0));
For X, we have cos(($i/$numSides)*2*PI). ($i/$numSides) is the fraction, and 2*PI is the circle. Multiplied together, you get the angle in radians. Cosine finds the X coordinates based on this angle. Sine finds the Y coordinates. It's all coming together, right?
Let's think about it just to make sure. Let's say we have four sides that we want to generate, so $numSides would have the value of 4. In the first loop, $i equals 0. So $i/$numSides = 0/4 = 0. In the second loop, $i equals 1, so $i/$numSides = 1/4 of the circle. The third loop $i equals 2 => 2/4 = 1/2, and the fourth loop $i equals 3 => 3/4. The for loop ends at $numsides = 3, since $numSides - 1 = 4 - 1 = 3. We just mapped out four points at each quarter of the circle.