L Script User Guide
L Script User Guide
Without this prior knowledge, you may find yourself pounding your
head on the table for no good reason—unless you like that sort of
thing.
This manual is designed for people who are just setting out to
learn programming with LScript. Mainly, we plan to train the
beginner or hobbyist. Professionals are welcome too, however, we
will be moving at a much slower pace than your time may allow. We
also won’t tackle some of the more difficult techniques that, as a
professional, you’ll need to have. For the experienced programmer,
we suggest reading the included LScript docs and release notes.
So if you have a good handle on how LightWave works, and want
to delve into some uncharted programming territory, this manual is
for you.
CHAPTER 2:VARIABLES
When you run your scripts, you often need to store information
for later use. Whether you need to remember the state of a button
pressed, or the position of an object, you may want to have access
to that data later on. Variables can temporarily store this data while
the script is running. It is almost impossible to create a
multipurpose script without using variables. For that reason,
understanding how variables work is an integral part of
programming.
Here are a few examples of variables you might find in a script:
myVariable
control01
frameNumber
frame_step
These variables do not hold anything, because we have not
assigned any values to them yet. They merely indicate storage
spaces set aside by LScript. When we want to store values, we can
put information into these storage spaces, using these names as
references.
As the word suggests, the values in variables are dynamic and can
vary while your script runs. You have full control over what values
are stored within a variable and when those values change.
However, the blame shifts back to you if errors appear, because if
the values change, you specifically wrote code that changed it.
Values in variables cannot just change by themselves.
Tracking and Debugging
Over the course of your scripting career, you will find that the
values in a variable may be completely different from what you
expected. This is not the fault of the variable, but the code.
Somewhere along the way, some piece of your code mistakenly
changed its value. Don’t sweat it; it happens all the time. Knowing
how to find and fix these errors is the key to keeping sane when you
deal with variables. Tracking down where values go bad is a
necessary evil, and as a beginning programmer, expect to do it
often.
Sometimes finding and fixing these bugs can take you a long time.
This process is called debugging; it can take a while at first and be
very frustrating. As time goes on, you will get more experienced
and undoubtedly make fewer mistakes. Luckily, the LScript system
6 VOLUME 1: LSCRIPT INTRODUCTION
has a tool to help us hunt down and kill these nasty bugs—the
LScript Debugger. You can find more information about this tool in
the chapter covering LSIDE.
Variable Names
A few rules govern the names you give variables:
1 The name must be unique; it can’t have the same name as a com-
mand, statement, function, or another variable. This rule keeps
LScript from getting names confused when you run your script.
2 The name cannot contain any spaces or symbols, with the excep-
tion of the underscore(_).
3 The name cannot start with any numbers, but it can have numbers
within the name.
Examples:
• The variable cricket is legal.
• The variable cricket3 is legal.
• The variable 3cricket is not legal.
Case sensitivity
LScript elements are all case sensitive, and that includes
variables. This means that a difference in upper and lowercase text
can mean two different variable names. Always keep this in mind
when you write code, because a simple mistake in a character’s
case can be a tough bug to track down. None of the following
variable names are the same:
cricket
Cricket
CriCKet
Cricket
Variable Declarations
A declaration is simply setting a name and value for a variable.
You usually make a declaration by putting the equal (=) symbol
after the variable name, and following the equal sign with a specific
value. The following paragraphs will list the different types of
variable and give examples of their use. Let’s start off with a very
simple value, the integer.
CHAPTER 2:VARIABLES 7
Note: Some of these variable types have advanced features and uses,
but we will not list them here. We do not want to confuse you in the
beginning. Rest assured that if we used anything advanced in the
example code, we have explained it fully.
Integers
The integer is one of the most common data types you will use in
your scripts. This data type represents both positive and negative
whole number values no greater than 4,294,967,294. If you can
remember some of your junior high school math, a whole number
is a number without any fractional values.
Valid Integer values: 0, 1, 1481, -253, 823523
Invalid Integer values: 1.0, 3532.99, -25.38, 21.001,
4294967295
Example declarations:
myVariable = 0;
myVariable = 1481;
Note: The semicolon represents the end of the statement, and this is
how code will appear in your programs. Although we do not need to
show our examples this way, we want you to get accustomed to seeing
how the code really looks.
Integers are used in almost all non-3D math. This is not to say that
integers are not still used widely in programming, but the whole
non-fractional value thing pretty much negates its use in 3D
mathematical equations. That’s because very rarely do animators
move, rotate, or scale items in whole numbers! For these non-
fractional values, we turn to the Numeric variable type.
Number
Numeric data is one of the most widely used data types in LScript.
This is because Numeric data covers the fractional values that the
integer data type does not. This fractional portion of the value is
specified using a decimal point or scientific notation.
Valid Numeric values: 2.45, -1.5, 22.1, 9e-10
Invalid Numeric values: (none)
8 VOLUME 1: LSCRIPT INTRODUCTION
Example declarations:
myVariable = 2.45;
myVariable = -1.5;
String
Strings are words or phrases stored in a variable. The trick with
Strings is that the values for the variable must be within quotation
marks (“ ”) and, they must all fit on one statement line. LScript
thinks that a value that is outside the quotation marks is either
another variable or a command/function.
Valid String values: “apples”, “oranges”, “Hello!”,
“1”, “true”
Invalid String values: apples, oranges, hello!, 1, true
Example declarations:
myVariable = “apples”;
myVariable = “oranges”;
Vectors
Vectors represent a series of three Numeric values contained
within the less than (<) and greater than (>) symbols and separated
by a comma. These values are handy when you want to store
coordinate values and RGB color values.
Booleans
The final data type is the Boolean; the Boolean represents both a
binary and an integer variable value. The easy thing to remember
about Booleans is that only four possible values can be stored in it:
0,1, true, and false.
CHAPTER 2:VARIABLES 9
Arrays
The array is probably the most complex variable type that we will
discuss. Arrays are collections of many pieces of data, all stored
under one variable name. An array can hold any type of variable. In
fact, arrays in LScript can even be a mixture of several different
types of variables.
For example, let’s say we wanted to store the colors of a traffic
signal in one variable.
The first color is “red”
The second color is “yellow”
The third color is “green”
Another way to write it would be:
1 “red”
2 “yellow”
3 “green”
We added a number in front of each color to show where it sits in
the list. If I asked, “What is color 2?” you’d naturally answer,
“yellow.” This is how arrays work.
Note that I wrote the colors using their string names, but because
arrays can be of any type, the list could easily look like this:
<255, 0, 0>
<255, 255, 0>
<0, 100, 0 >
Using a standard variable name, a set of open and closed brackets
([]), and a sequential integer called an index, we can store and
retrieve data from an array of values. The index must be a positive
integer value, or a variable that contains a positive integer value;
you cannot use zero, negative integers, or fractional values for the
index. In our traffic light example, the variable names would look
something like this:
10 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
trafficColor[1]
trafficColor[2]
trafficColor[3]
Valid Array indices:
trafficColor[1]
studentNumber[114]
trafficColor[lightNumber]
Invalid Array indices:
trafficColor[0]
accountNumber[-4]
trafficColor[1.5]
pointNumber[<9,1.0, 12>]
Example declarations:
trafficColor[1] = “red”;
trafficColor[2] = “yellow”;
trafficColor[3] = “green”;
We need to look a little more closely at one part of arrays: using a
variable as the index. Let’s set up a slightly more complex example
using what we already know:
trafficColor[1] = “red”;
trafficColor[2] = “yellow”;
trafficColor[3] = “green”;
lightNumber = 1;
trafficColor[lightNumber] has a string value of “red.”
Let’s explain this a little. Because we declared that the variable
lightNumber had an integer value of 1, LScript will read that value
when it accesses the array. That means that the statement now, in
theory, reads:
trafficColor[1];
If we look up the value of trafficColor[1] in the declarations
list, we can see that it had a string value of “red.”
This may seem a bit confusing right now, however this method is
common practice in programming and we’ll have many more code
examples to explain this subject further. For now, just try to absorb
the basic concept.
CHAPTER 2:VARIABLES 11
String Math
While we’re playing with variables, let’s do a little string math.
What do you think this group of declarations will do?
text1 = “Hello”;
text2 = “ World!”;
text3 = text1 + text2;
The result for text3 is “Hello World!” We get to this result
because we declared that text1 has the String value “Hello” and
we declared text2 has the value “World!” Therefore, the text3
receives the values “Hello” + “ World!” together to get “Hello
World!” This is string math.
Variable Arithmetic
Much like string math, variable arithmetic uses math with other
variables to create new or alter existing values. Here’s another code
snippet:
val1 = 1000;
val2 = 10;
val3 = 5;
val4 = val1/val2 * val3;
The result for val4 is 500. We divided 1000 by 10, which gives us
100 and then we multiplied that by 5.
val[1] = 1;
val[2] = 10;
val[3] = val[1] + val[2];
The result for val[3] is 11. Variable arithmetic is used quite
often in coding. Luckily we’ll have a lot of practice.
Constants
There is one last type of variable that we neglected to tell you
about. The variable type Constant acts so differently from any
other that we decided to throw it at the end of the chapter. As
different as Constants may be, though, they are easy.
Constants act exactly as their name implies—they are constant.
You cannot change their values. The Constants define variables
that LScript uses internally, so you would not want them to change.
For example, let’s look at light types:
12 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
Light types:
A Distant light has a light type of 0.
A Point light has a light type of 1.
A Spot light has a light type of 2.
LScript pre-defines these values to make them easier for you to
remember and use.
LWLIGHT_DISTANT = 0
LWLIGHT_POINT = 1
LWLIGHT_SPOT = 2
Now you can just write the variable LWLIGHT_DISTANT in your
code. That’s a lot easier to remember than a distant light has a type
value of 0! Because Constants are so easy to remember and read,
LightWave protects them. Like I said before, you cannot change
their values… ever.
CHAPTER 3: FUNCTIONS
Functions are self-contained sections of code that handle much
smaller, but repetitive, programming tasks. A function is set aside
from the main program, and it contains its own variables and lines
of code independent of the main script. Your program can use
these functions by sending them specific data to use. This process
is known as “calling a function.”
Once a function is called, its data and lines of code are processed
just like a regular script. The function is completely cut off from the
code that called it. Only the data that was specifically sent to the
function is usable from the main script. The function now has
“focus.”
When the code in the function finishes running, it can send the
processed value(s) back to the script that called it. These values
wouldn’t do us much good if they just stayed in the function! Focus
then returns to the place in the main script where the function was
originally called, and the script continues to run right where it left
off.
You are not limited to just the functions included within LScript;
you can easily create your own User-Defined Functions (UDFs),
customized to fit your needs. In fact, you can even group some of
your more popular functions together to make a library of tools
shared by numerous scripts. If you script for a while, you’ll quickly
find that you can amass a small arsenal of very powerful functions.
An Example from the Real World
The following example from the real world should help you
understand how functions work.
Think of your script as a large car-manufacturing plant. The plant
must make thousands of cars a month, but it doesn’t make all the
parts of the car in the same plant. For example, the tires are made
by one company, the glass by another, spark plugs by another, and
so on. When all these parts come together, they can build a car.
Let’s look more closely at this idea. The car-manufacturing plant
starts to build a car and realizes that it needs tires. They call the
tire company and order 1000 tires for a cost of $10,000. The tire
company takes the money and builds the tires. Then they send the
tires back to the car plant. Building the tires is a function of the
larger process of constructing the car. Now let’s make this idea
resemble a program and a function.
14 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
Parts of a Function
The example above, although correct, looks a lot different from
our previous example. Let’s look at some of the differences.
The first difference appears with the word ‘main.’ Because your
first scripts will be LScripts for Modeler, we will focus on rules
unique to Modeler scripting. As the word suggests, the main()
function is where LScript begins to execute code. Every Modeler
LScript must have a main() function.
Body Markers
Second, what are those curly braces? Those are called body
markers, and they tell LScript where the code begins ({) and where
it ends (}). Notice that if you remove all the other code in the
script, the main() function looks like this:
main
{
}
Every line of code within these body markers belongs, or is
bound, to the main() function. This is because an open marker ({)
after the name of a function indicates that it owns all code until it
reaches the closed marker (}) for the function. If the closed marker
for a function is accidentally left out, LScript will not know where
the main() function ends, and will bring up a nasty error.
16 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
Semicolons
In our example, a bunch of extra code belongs to the main()
function. Notice that a semicolon (;) follows each line of code. The
semicolon tells where each statement ends. It works much like a
period at the end of a sentence.
Remember that you cannot just hit the ENTER key and go to a new
line in your text editor; LScript will not consider a line of code
ended until it reaches a semicolon(;). Improper punctuation can
result in an error. For example, the following code:
main
{
buildFrame();
buildEngine();
tires = getTires(1000,10000);
}
is the same as:
main
{
buildFrame();
buildEngine();
tires
=
getTires
(1000,10000);
}
The code looks awkward, and really shouldn’t be written this way,
but it is legal. Scripters can arrange and format their code however
they want just as long as a semicolon(;) is used to finish a line.
And finally, the following code is also the same, and legal:
main
{
buildFrame(); buildEngine(); tires = getTires(1000,10000);
}
Note: Your most common mistake will be to forget the semicolon. This
step is very easy to miss.
CHAPTER 3: FUNCTIONS 17
addAB: a, b
{
tmpVal = a + b;
return(tmpVal);
}
You have two functions here: main() and addAB(). First, the
main() function declares the variables a and b with the integer
values of 10 and 15. By passing the variables a and b, the focus
now shifts from the main() function to the addAB() function.
In addAB(), we declare the variable tmpVal to have the value of
a + b, which translates to 10 + 15. So, now the variable tmpVal
has the integer value of 25.
In the next line,
return(tmpVal);
We tell the function to send the value in the tmpVal variable (25)
back to the main() function, where we have the variable c waiting
to receive its value. To prove it, we threw in an info() command
to print the value of c to the screen. This of course will be 25.
So we sent values to a new function, where they were added, and
sent back to the main() function. This is the basic procedure of all
functions.
CHAPTER 4: CONDITIONAL STATEMENTS 19
{
mergePoints();
}
…next statement…
Let’s walk through this example. The if-then statement
evaluates the pointsSelected variable to see if it has a value of
2. If it does, then the mergePoints() command is run. If
pointsSelected does not have a value of 2, then the
mergePoints() command is skipped and the next statement is
run.
In the example, the block of code contained only one statement to
run (the mergePoints statement). When this happens, the if-
then statement can actually be written without the body markers,
as shown in the code that follows:
if(pointsSelected == 2)
mergePoints();
…next statement…
You can write the code this way only when a single line of code
will be executed if the expression is found true.
Operators
You may have noticed that the expression in the if-then statement
was pointsSelected == 2. This is not a typo, two equal signs
are the operators in this expression. Let’s look at the different
operators we can use in LScript.
The following list contains the basic operators we will learn:
If-then-else Statement
The if-then-else statement is nearly the same as the if-then
statement, except for one major difference. Rather than completely
skipping over any code if the expression is false, the code jumps
to the else block and executes the code there.
Syntax:
if(…expression…)
{
…code to be executed if the expression is true…
}
else
{
…code to be executed if the expression is false…
}
…next statement…
Notice that the else statement directly follows the closed marker
(}) of the true statements and there is no semicolon following it.
This is because the else statement is still part of the if-then
statement.
22 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
Example:
if(pointsSelected == 2)
{
mergePoints();
}
else
{
error(“2 or more points must be selected.”);
}
We can also write the previous conditional statement like this:
if(pointsSelected == 2)
mergePoints();
else
error(“2 or more points must be selected.”);
Writing the statement this way is actually more correct than the
previous format. The conditional’s body markers are unnecessary
when the statement uses only a single line of code. As you can see,
this format can really save space in long scripts.
The error() statement simply brings up an error message box
to the screen, and stops the execution of the script.
Example #2
done = false;
if(!done)
info(“The procedure IS NOT done.”);
else
info(“The procedure IS done.”);
…next statements…
case /value2/:
…statements…
break;
default:
…statements…
break;
}
…next statements…
Example #1:
valueA = 3;
valueB = 1;
switch(valueA - valueB)
{
case 1:
info(“A - B = 1”);
break;
24 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
case 2:
info(“A - B = 2”);
break;
default:
info(“No Matches found!”);
break;
}
…next statements…
In the example above, the variables valueA and valueB are
given integer values. The result of the expression valueA -
valueB (which has the value of 2) is then passed to the switch()
command. The switch command will then compare this value to a
list of case values. When a match is made, the code bound to the
case command is executed.
The first case statement value is 1. The expression value does
not match this case value, so LScript will skip over the code and
compare the next case statement. The next case value happens to
match the expression value, so LScript executes the info()
command. This command displays the text “A - B = 2” in an
info() box. The next statement is the break statement. This
instructs LScript to direct execution to the switch() statement’s
closed marker (}).
The switch() statement help you avoid using several if-then or
if-then-else statements to compare an expression against several
different values.
CHAPTER 5: LOOPING STATEMENTS 25
The for-loop
The for-loop statement, although complex, is the most
common of the looping statements. This statement is an iterating
loop. This means that when the block of code in the loop is done, it
changes a variable, compares it to a value in an expression, and
determines if it should start over again. The process repeats until
the result of the expression comes back false.
Syntax:
for(variable declaration; …expression…; iteration)
{
…Statements to loop…
}
Example:
for(i = 0; i < 10; i++)
{
info(i);
}
When LScript executes the for statement, four things happen.
First, the variable i is declared with a value of 0:
26 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
Note: This coding savior has a very nasty side. Programming with the
while loop requires a fair amount of care and concentration when you
write or things can go horribly wrong.
Syntax:
while(…expression…)
{
…statements…
}
…next statements…
Notice that the while loop looks a lot like an if-then statement.
That is basically what a while statement is, except that instead of
moving on to the next line of code, a running while loop will repeat
until the expression is no longer true.
Note: In this way, the while loop is also like the for statement
discussed previously.
Example:
done = false;
while(!done)
{
if(x ==5)
done = true;
else
x++;
}
28 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
done = false;
while(!done)
{
if(x ==5)
done = true;
}
This is what we call an endless loop. The while() command
checks if the value for the done boolean is false, but the block of
code for the while() statement has no iterator. The x never
changes, so it will NEVER have a value of 5 and done will never
become true. Thus, the script never ends. You must manually kill
Layout or Modeler to run LightWave again when you fall into an
endless loop.
Note: You can also create endless loops with the for statement.
You must be very careful when you work with the for and
while() loops. Make sure that your variables have some kind of
iterator, or will change so that the script can end. Users really hate
it when a script hangs and they must kill Lightwave and lose all
their work.
30 V O L U M E 1 : L S C R I P T I N T R O D U C T I O N
CHAPTER 6: MODELER LSCRIPT INTRODUCTION 31
Note: All our initial scripts will have the extension (.ls), signifying
they are LScripts.
34 V O L U M E 1: M O D E L E R LS C R I P T
5 In Modeler, select Construct > LScript and browse until you find
your “hello.ls” script. When you select your script, Modeler exe-
cutes it. If you typed everything correctly, you should see an info
box with the “Hello World” text displayed. If you get an error
message, go back into the editor and check your text.
Note: Where the info and error messages are displayed depends on
the Alert Level setting. At the beginner level, the message appears as
a panel in the middle of the screen. At the expert level, the message is
printed in the tool tips section of Modeler.
Okay, you’re done! You are now a programmer! Ahem… let’s look
more closely at the script we made.
Comments
Comments are a great way to put little descriptive notes in your
scripts. These notes remind readers what your script is trying to
do. So, the more information you put in comments, the more
readers will appreciate your effort. Comments are totally ignored
by LScript. In fact, all comments are internally removed from the
script before Modeler even tries to run it. There is nothing you
could put in a comment that would be wrong, in terms of syntax.
Let’s look at a comment we will put in our new Hello.ls script.
main
{
// This is a comment.
text = “Hello World!”;
info(text);
}
You create comments by typing the double-slash characters (//).
Any text following these characters is considered commented out
and is ignored by LScript.
So this setup is wrong:
main
{
comment //
}
CHAPTER 7: MODELER SCRIPTING BASICS 37
What if you want to comment out a bunch of lines? There are two
ways to handle this. The first way uses the method you have
already learned:
main
{
// This script displays an info box to the screen.
// The variable text has the value of: “Hello World!”
…
}
The second way uses new characters:
main
{
/* This script displays an info box to the screen.
The variable text has the value of: “Hello World!” */
…
}
The text between the /* and */ characters is commented out.
Use this method when you want to give a long description, or to
comment out large sections of faulty code. It is more efficient and a
little nicer to look at.
main
{
}
Now, let’s add the editbegin() and editend() commands.
main
{
// Command Sequence mode.
editbegin(); // Mesh Data Edit mode.
editend(); // Command Sequence mode.
}
If you ran this script, nothing would actually happen. Modeler
does not give you any indication that the mode changed; it’s totally
transparent.
The addpoint() command will create a single point according to
the three coordinate arguments you send it. Let’s put it between
the editbegin() and editend() commands.
main
{
// Command Sequence mode.
editbegin(); // Mesh Data Edit mode.
addpoint(0,0,0); // Make point 1.
addpoint(0,1,0); // Make point 2.
addpoint(1,0,0); // Make point 3.
editend(); // Command Sequence mode.
}
Run the script and you will see Modeler create three points in the
Front projection window. With these points created, it’s only
natural that we want to create a polygon using the addpolygon()
command. First, we need to change our original script around a bit
to handle this new functionality.
Originally, we created the point using the following line of code:
addpoint(0,0,0);
That code sent the values 0,0,0 to the function addpoint();
addpoint() then created the point and LScript automatically
moved to the next line of code. However, we missed something else
that happened right after the point was created. Remember what
we said about functions? We said that some functions would
actually return a value to the calling script once their job was done.
CHAPTER 8: MODELER: POINTS AND POLYGONS 41
In this case, the return value was sent; we just were not listening.
The value returned from the addpoint() command is the point
identifier (or point id) of the point created. This value is an internal
value created whenever a new point or polygon is created. We use
point ids to identify which points the user selected or which points
we want to edit. So let’s fix our last script:
main
{
//Command Sequence Mode.
editbegin(); // Mesh Data Edit mode.
point[1] = addpoint(0,0,0); // Make point 1.
point[2] = addpoint(0,1,0); // Make point 2.
point[3] = addpoint(1,0,0); // Make point 3.
editend(); // Command Sequence Mode.
}
If you ran this script, you would see that it runs exactly the same
as it did before. The difference is that now we notice the value the
addpoint() function sends back. We did this by stating that the
variable array point[1] equals the returning value of the
addpoint() command. To prove it, we will use the variable array
with another new command, addpolygon().
Let’s make a triangle. This script is exactly the same as the script
above, except the comments were removed and another line was
added in the MeshDataEdit block.
…
main
{
editbegin();
point[1] = addpoint(0,0,0);
point[2] = addpoint(0,1,0);
point[3] = addpoint(1,0,0);
polygon = addpolygon(point);
editend();
}
…
As you can see, the addpolygon() command creates a polygon
from the points created. We pass the point identifiers stored in the
variable array point[1], point[2], and point[3] and create a
polygon. Just like when you make a polygon in Modeler, point order
is very important to the addpolygon() command. By passing the
values in the order that we did, we determined the side that the
42 V O L U M E 1: M O D E L E R LS C R I P T
Note: We do not advise that you practice this in any of your scripts.
You should have the point count information prior to calling the first
editbegin() command.
CHAPTER 8: MODELER: POINTS AND POLYGONS 43
…
This code creates and populates the variable array called
points[]. If we had three points selected the variable would look
like this example:
points[1] = point identifier #1
points[2] = point identifier #2
points[3] = point identifier #3
Conveniently, the array indexes (the numbers in the brackets)
each represent a point number. So points[1] actually means
point 1. This works out well for how we will handle their values.
In the next step, we want to get the coordinate value of the last
point selected. This is where we will move all the other selected
points. To get to the point’s coordinate data we need to use the
pointinfo() MeshDataEdit command.
…
// Determine coordinates of last point selected.
lastPntPos = pointinfo(points[pntCnt]);
…
Given an argument of a point identifier, pointinfo() returns a
vector value that represents the x, y, z coordinate. In this case, we
sent it the point identifier from the point[] array. We chose the
index number as the value stored in pntCnt because three points
are selected, and the pntCnt variable is set to be the number of
points selected. We can safely assume that the last index in the
array is the last point selected. Therefore, pntCnt also equals the
last point selected.
To view the vector coordinate value of the last point, insert an
info() box after the pointinfo() line. The following is an
example of what you will see:
<0.12, 4.53, 2.3>
Next, we want to be able to go through all selected points, get
their values and move them to this new location. The “go through
every point” part of that statement indicates that we need to use
some kind of loop. Remember a loop is a control structure that
repeats lines of code until it is told to stop. We need to put this
command in the MeshDataEdit block.
So how do we go about creating this loop? We need four pieces of
data for the loop structure to work right:
CHAPTER 8: MODELER: POINTS AND POLYGONS 47
integer value. This tactic is used all the time in for-loops. Let’s
demonstrate:
currPnt = 1;
point[currPnt]; // The first point identifier.
currPnt = 2;
point[currPnt]; // the second point identifier.
currPnt = 3;
point[currPnt]; // the third point identifier.
and so on..
As the for-loop continues to run, and its currPnt variable
increases, the one line of code can handle ALL the points in the
array.
The second parameter we pass to pointmove() is the new
location for the point. In this case, it is the coordinate value we
stored from the last point selected.
Run the script and watch it in all its glory. Only one thing is
missing. Right now the points are merely moved to their new
locations; we still have to merge them. The last line actually does
the damage:
// Merge the points together.
mergepoints();
With no arguments, the mergepoints() command will merge all
the points defined by the selection mode, which are exactly on top
of each other, as we have set up here.
There, it’s all done! Let’s look at the final script:
weldfast.ls
main
{
// Edit only selected components.
selmode(DIRECT);
// Get the number of currently selected points.
pntCnt = pointcount();
if(pntCnt == 0)
{
error(“No points selected”);
}
50 V O L U M E 1: M O D E L E R LS C R I P T
main
{
//declare the variable out here for scope.
totPntVal = < 0, 0, 0>;
selmode(USER);
pntCnt = pointcount();
if(pntCnt == 0)
{
error(“No points selected.”);
}
editbegin();
/* Find the average point location by adding all the
coordinates then dividing by the number of points selected.
*/
for(currPnt = 1; currPnt <= pntCnt; currPnt++)
{
currPntVal = pointinfo(points[currPnt]);
totPntVal = totPntVal + currPntVal;
}
pntAvgPos = totPntVal / pntCnt;
//Move all the points to that selection.
for(currPnt = 1; currPnt <= pntCnt; currPnt++)
{
pointmove(points[currPnt],pntAvgPos);
CHAPTER 8: MODELER: POINTS AND POLYGONS 51
}
editend();
mergepoints();
}
And this one welds groups of points together:
main
{
selmode(USER);
pntCnt = pointcount();
if (pntCnt == 0)
{
error(“No Points selected.”);
}
moveId = floor(pntCnt/2);
editbegin();
for(currPnt = 1; currPnt < (moveId + moveId);
currPnt++)
{
info(currPnt);
pntPos = pointinfo(points[currPnt+1]);
pointmove(points[currPnt],pntPos);
/* since its dealing with groups of points we have to
increment the currPnt variable twice. */
currPnt++;
}
editend();
mergepoints();
}
52 V O L U M E 1: M O D E L E R LS C R I P T
CHAPTER 9: VMAPS 53
CHAPTER 9: VMAPS
This chapter covers some basic functions for dealing with VMaps.
Whether you want a weightmap to control a SubPatch surface, or
you want to use a texture map in UVmapping, Vertex Maps give you
enormous power. Likewise, LScript provides an equally powerful
toolset to modify these Vertex Maps. Before we delve into this new
code, though, we need to discuss a few new areas of LScript.
The example we will develop simply changes the value of
WeightMaps by 50 percent, but we will cover a wide range of new
commands and functions along the way. Let’s get started.
As usual, we will start with the main() function, but for an
added twist let’s include the following piece of code:
//Preprocessor Compiler Directives.
@script modeler
main
{
//Create a VMap Object Agent.
//Get the number of points selected.
//Scale the Selected VMap values by 50%
}
What is a Preprocessor?
If you tear the word "preprocessor" apart, it means “happens
before processing,” and that is exactly what the preprocessor
commands do. Before the contents of the main() function are
executed, the compiler searches for any preprocessor directives. In
this case, @script modeler instructs the LScript compiler that
this script runs in Modeler. This preprocessor helps LScript
determine the type of script when the script is added as a plug-in.
In fact, let’s add a few more preprocessor directives:
@name “tmp”
@version 2.3
As you probably guessed, the @name pragma lets you name the
script. This name appears in Modeler’s plug-in drop-lists and
configuration screens. The @version pragma indicates the
minimum version of LScript your script is designed to work with.
The version is important because, as the LightWave system
expands, so does LScript. Scripts that are run today may not run on
older versions of LScript. The @version pragma lets us check
54 V O L U M E 1: M O D E L E R LS C R I P T
which version a user has, and if that version will work with our
script. If the user tries running the script in an older version of
LScript that does not support the commands we use, LScript issues
an error.
For a full list of Preprocessor directives, please check the
Reference section of this manual.
VMTEXTURE “texture”
VMMORPH “morph”
VMSPOT “spot”
VMRGB “rgb”
VMRGBA “rgba”
We passed the constant VMWEIGHT, indicating that we want to
reference the weight maps in the geometry.
We can use the following example equations to access the data
members in this Object Agent:
nameOfVMap = vmapObj.name;
numOfValues = vmapObj.dimensions;
typeOfVMap = vmapObj.type;
The period (.) separates the Object Agent name and its data
member or method. So, right now we have the vmapObj Object
Agent created, and it currently stores the data for the first
WeightMap created in the object.
We then tested for at least one WeightMap in the object by
including the following lines of code:
if(vmapObj == nil)
error(“No weight maps in the mesh!”);
This code states that if vmapObj equals nothing (nil), then no
VMaps were found in the object, and an error message should be
sent to the user notifying them of this situation. However, if
vmapObj finds at least one WeightMap, it continues on to the next
line of code. If you want to see this reference to the first VMap,
throw the following line after the if-block:
info(vmapObj);
Before we can test out our script, though, we need to create a test
object to experiment on.
Next, replace the info() function with this next piece of code:
pntCnt = pointcount();
//Check to see if the current point is mapped in the VMap.
if(vmapObj.isMapped(points[pnt]))
{
//If so, get the VMap value.
values = vmapObj.getValue(points[pnt]);
main
{
//Create a VMap Object Agent.
vmapObj = VMap(VMWEIGHT);
{
//If so, get the VMap value.
values = vmapObj.getValue(points[pnt]);
}
//Loop through all the dimensions of the value
for(x = 1;x <= vmapObj.dimensions; x++)
values[x] *= scaleAmt;
main
{
//Create a VMap Object Agent.
vmapObj = VMap(VMWEIGHT);
/* Recreate the Object Agent with the name of the VMap the
user chose from the drop-list. */
vmapObj = VMap(vmapnames[vndx]);
editend();
}
62 V O L U M E 1: M O D E L E R LS C R I P T
CHAPTER 10: SURFACES AND LAYERS 63
main
{
//Get a surface name.
//Select the polygons with the surface.
//Cut the selected Geometry.
//Find a free layer.
//Paste geometry into this new layer.
}
First, let’s get some preliminary data. In the main() function,
enter the following code:
//Switch to USER mode.
selmode(USER);
workLayer = lyrfg();
…
pntCnt = pointcount();
if(pntCnt)
{
//Test the conditional statement.
info(“Got Some!”);
}
…
The edit block should look familiar to you—we did a lot with it in
previous chapters. Using pointcount(), we got the point count
of the selected geometry. We then made an if-statement to test
whether a value was actually stored in the pntCnt() variable. You
may be thinking that this conditional looks a little too simple to do
anything, but we have to remember some rules about variables and
conditional statements.
First, a conditional statement relies on a value of true or false
to execute or not execute code in the if-block. The internal
Boolean variables, true and false, have values of 1 and 0
respectively. In actuality, the true value can be any non-zero
number and still be true. Therefore, as long as pntCnt does not
equal 0, it is considered true, and will execute the code in the if-
block. This is exactly how we want it to function. If you test it, you
will see that the info() box should come up with the text “Got
Some!” in it. This means the script got this far, and the conditional
worked as expected.
Next replace the info() lines with this code:
//Put the selected geometry into the clipboard.
cut();
main
{
//Switch to USER mode.
selmode(USER);
workLayer = lyrfg();
68 V O L U M E 1: M O D E L E R LS C R I P T
pntCnt = pointcount();
if(pntCnt)
{
//Put the selected geometry into the clipboard.
cut();
lyrsetfg(workLayer);
}
}
CHAPTER 11: DISPLACEMENT MAPS 69
newtime()
The newtime() function is called every time the scene’s current
frame is altered. This function receives three arguments from
Layout: a Mesh Object Agent (id), the current frame (frame), and
the current time (time). The Mesh Object Agent (id) is created
from the object from which the script is applied.
Typically, these arguments are made available to other functions
through several global variables.
flags()
The flags() function instructs Layout how the script will
process the point data. Typically, a single line of code, containing a
return() statement, is all this script requires to function
correctly. One of two constants, passed by the return()
statement indicate whether the script will process points in WORLD
or LOCAL coordinates. If the constant is omitted, points will be
processed in WORLD coordinates.
70 V O L U M E 1: L A Y O U T LS C R I P T
process()
The process() function is where most of your important point-
altering code belongs. This function receives a Displacement Map
Object Agent (da) as an argument when it is called by Layout. The
process() function gets called by Layout every time a frame is
evaluated.
The Displacement Map Object Agent contains just two data
members, oPos[] and source[]. Both of these data members
have access to a point’s x, y, and z coordinates. However, there are
two major differences between these data members. First, oPos[]
is read-only, where source[] is not. Second, oPos[] returns the
coordinates from the object’s origin (local), while source[]
returns coordinates in world space.
options()
The options()function is called every time the user double-
clicks or edits the properties of the script in the plug-in list. It
contains all the necessary code to setup and run the script’s
interface.
Example: Splat!
First, let’s get the header information out of the way. This script
does not require anything special from the later versions, so we are
going to stick with version 2.3. We will specify a Displacement Map
script, and we will give it the name “Splat.” That way we can add it
as a plug-in and it will show up in our plug-in list.
CHAPTER 11: DISPLACEMENT MAPS 71
@warnings
@version 2.3
@script displace
@name Splat
We will use four functions for this script, so let’s set them up here:
create
{
}
process: da
{
}
and…
options
{
}
We will deal with the interface later, but we need to set an initial
value, so that we can do some testing. For now, let’s set up a global
variable to share some of these values across multiple functions.
@script displace
@name Splat
// Global variables.
splatValue = 0;
create
{
We have set the variable splatValue = 0. Later, we will set up
an interface to request the splat rate from the user, but for now, this
will do. Next, we want to simply set the description of the script to
display its name in the plug-ins list.
create
{
setdesc(“Splat!”);
}
72 V O L U M E 1: L A Y O U T LS C R I P T
process: da
{
// da is the Displacement Map Object Agent passed
// from Layout.
info(“process: “, da.oPos[1]);
}
Now is a good time to save your script. Save it as splat.ls.
However, before we can run our script, we need to make a test
object to play around with. Because Displacement Maps perform
translations on numerous (or all) points of an object, our test
object should have as little geometry as possible. That way, if
something goes wrong, it’s not wrong hundreds of times!
In Modeler, make a simple box with the center of the box at the
origin of world space (0,0,0). The point data that we have access to
can vary drastically depending on the box’s location in 3D space.
Save the object as Box.lwo. It’s a good idea to have numerous test
objects available that can cover a wide range of modeling
situations. The more situations you test with your script, the less
likelihood that you will overlook a possible design flaw or bug.
Load the Box.lwo into Layout.
With the Box.lwo object selected, open the Displacement Map tab
in the Object Panel and apply the splat.ls script onto the box
object.
An info() box requester immediately pops up and starts to give
you data. No, this is not an error message, it is actually the result of
the info() function we set up in the newtime() function. If you
accidentally closed the requester, you can generate the same result
by simply moving the time slider back and forth. This first info()
panel displays the name of the object that the script is applied to,
the current frame number, and the current time.
CHAPTER 11: DISPLACEMENT MAPS 73
This process will continue, and pntCounter will never equal one
again.
To fix this we can simply inform our script that we should reset
the counter when the frame changes. Simply add this line of code
into the newtime() function:
newtime: id, frame, time
{
pntCounter = 0;
}
Save your work, remove the old script from the object, and try out
the new script. After a few tests, you should notice that the counter
resets back to zero once the time slider is changed. The result is a
single info() panel displayed every time the frame is updated.
This is how we expected it to run the first time.
Now we want to see how an animation affects a point’s x position
value. We will start by setting up a simple test animation that we
will use throughout this example. To make animating a little easier,
you may want to disable the script in the plug-in list. This will cause
the script to temporarily stop sending messages to the screen. Next
create two animation keyframes: one at 0,0,0 on frame 0, and the
other at 2,0,0 on frame 10. Save the scene, so you can easily reset a
possibly corrupted scene.
When you have animated the scene, turn your script back on, and
walk through the animation frame by frame. You should notice that
the displayed x position does not change, even though its position
in the scene does. What we have here is an example of using an
object’s local coordinates. By using the Object Agent’s oPos[1]
data member, a point’s coordinates are determined locally no
matter where the object is positioned in the scene. This even holds
true if you change the position of the pivot point in Layout!
What if you wanted to have access to the point’s World
coordinates? Instead of accessing the Displacement Map Object
Agent’s oPos data member, try using the source data member. The
info() code line would then look like this:
info("process: point: ", pntCounter, " ", da.source[1]);
Now update and rerun the script. You should see the difference
that the source data member makes when you change the time
slider. As the object’s point travels across the x axis, its world
position changes. For our needs, we will use the object’s world
CHAPTER 11: DISPLACEMENT MAPS 75
process: da
{
pntCounter++;
if(pntCounter == 1)
{
// Get the point's WORLD position.
xPos = da.source[1];
yPos = da.source[2];
zPos = da.source[3];
info(xPos, " ", yPos, " ", zPos);
}
}
When you run the script with the new process() function
added, you should still get an info() box, but it will display the
point’s x, y, and z coordinate values. These are the values we will
play with.
There is one other major difference between the oPos[] and the
source[] data members. While we can retrieve a point’s
coordinate values through oPos[], we cannot set these values. For
example the following statements would generate errors:
da.oPos[1] = 1;
da.oPos[2] = 1;
da.oPos[3] = 1;
However, this code is correct:
da.source[1] = 1;
da.source[2] = 1;
da.source[3] = 1;
Because the source[] data member allows a scripter to both
read and write data, we can manually set a point’s World
coordinates. LScript does not allow us to alter the original point
values of an object.
Our Splat script will simply impose a minimum y coordinate value
for an object’s geometry. To do this, we add one more set of lines to
the process() function.
info(xPos, " ", yPos, " ", zPos);
76 V O L U M E 1: L A Y O U T LS C R I P T
// Global variables.
splatValue = 0;
create
{
setdesc("Splat!");
}
process: da
{
// Get the point's WORLD position.
xPos = da.source[1];
yPos = da.source[2];
zPos = da.source[3];
{
reqbegin("Splat!");
c1 = ctlnumber("minimum y value: ", splatValue);
return if !reqpost();
splatValue = c1.value;
reqend();
}
In order for us to better explain the various values and where they
came from, we have left some code in the process() function that
is redundant and unnecessary. Take a moment and see if you can
make our process() function even more efficient. This is the
highly efficient process() function:
process: da
{
// Determine if the point has become less
// than the minimum value allowed.
if(da.source[2] <= splatValue)
da.source[2] = splatValue;
}
Example: dynSplat!
Rather than rely on an interface to retrieve our data, we will use
another object’s world coordinates to define the impact planes we
used in the previous example. This will make our script more
dynamic, offering the option of animating the effect. We will use the
previous splat.ls script as a starting point for this new script.
First, we will need to change the name of the script, so change the
@name pragma to read:
@script displace
@name dynSplat
//Global variables.
Next, we need to change some of the global variables we used.
Remove the splatValue variable, and add the following:
@name dynSplat
obj;
objName = “Null”;
currTime;
78 V O L U M E 1: L A Y O U T LS C R I P T
create
{
These values will hold the Object Agent created from the object
we will reference, the name of the object, and the scene’s current
time. Now we will change the script’s create() function to take
into account the script’s new name:
create
{
setdesc("dynSplat!");
}
In the previous script, we were not using the newtime() function
at all, so it was removed. However, we will now use it to set our new
variable, currTime, to the scene’s current time. Since currTime
is global, all the functions in the script will have access to its value.
newtime: id, frame, time
{
currTime = time;
}
We will also assign an Object Agent, created from the name of the
reference object, to the global obj variable. Right now we have the
name of the reference object stored in the variable objName. This
currently has the value of “Null.” However, you need an object
named “Null” in the scene for this script to work correctly. We will
eventually add an interface to cover this, but for now we will keep it
set manually to “Null”.
newtime: id, frame, time
{
currTime = time;
if(objName)
obj = Mesh(objName);
}
As you may have noticed, we also made a small error when we
checked that the objName variable actually has a value before we
create the Object Agent. We decided to make this assignment here,
rather than in the process() function, for speed concerns. If we
created the Object Agent in the process() function, it would be
called constantly. So much, in fact, that it may be noticeable to the
user. Since the newtime() function gets called only when the
current frame is changed, the operation should go unnoticed.
CHAPTER 11: DISPLACEMENT MAPS 79
// Global variables.
obj;
objName;
currTime;
create
{
setdesc("dynSplat!");
}
process: da
{
if(obj)
{
pos = obj.getPosition(currTime);
if(da.source[2] < pos.y)
da.source[2] = pos.y;
}
}
options
{
reqbegin("dynSplat!");
c1 = ctlmeshitems("objects: ", "Null");
if(!reqpost())
return;
obj = c1.value;
if(obj)
objName = obj.name;
reqend();
}
CHAPTER 12: OBJECT REPLACEMENT 81
Example:
In a scene set to 24 frames-per-second, you get the following
numbers for a series of frames:
FRAME TIME INDEX (24FPS)
1/24 or .042
2/24 or .083
3/24 or .125
In a scene set to 30 frames-per-second you get the following
numbers for the time index:
FRAME TIME INDEX (30FPS)
1/30 or .033
2/30 or .066
3/30 or .1
curType is a constant value that indicates the current type of
rendering. The script can examine this value and provide different
geometry for interactive previewing and actual rendering.
curType can be one of the following types:
NONE indicates that no geometry will be loaded for the current
time index.
PREVIEW indicates that a Layout preview will be generated.
WRITE ENABLED
newFilename
newFilename is the filename of the object that will replace the
current object. This must be the full path of the object because
LScript uses this variable to load the new geometry into Layout.
changeobj1 = "";
changeobj2 = "";
prog = "Changer v1.01";
swapframe1 = 1;
swapframe2 = 30;
Let’s take a closer look at our global variables and explain what
each does for us during the script’s execution:
changeobj1 holds the name of the first object we exchange.
Because the point of the script is to change our existing object into
a different object, we need a place to store the name of the
replacement objects.
changeobj2 is another storage area for the second object to be
replaced.
prog is just a useful short cut for labeling a script. It lets you
change the name of the script for requestors in a single place, so
you avoid searching the whole script for each instance where you
post the name.
swapframe1 is the frame number that corresponds to the
changeobj1 object. We get two frame numbers from the user;
these are the frames where they want to swap objects.
swapframe2 is the second swap frame and corresponds to
changeobj2.
Now, let’s start with the create() function:
create
{
setdesc(prog);
}
The create() function is called the first time the script is
loaded. This could either be the first time the script is added or
when a scene file is loaded. In the create() function, you may
prime your variables or other actions you want to happen at the
creation of the script.
In this case, we use the setdesc() function to identify the active
script. The function setdesc() takes a single string variable and
that string is displayed in the plug-in line. It passes the contents of
the prog variable, so the line looks like the following example:
CHAPTER 12: OBJECT REPLACEMENT 85
process: ra
{
thisframe = ra.newFrame;
}
As we stated earlier, the process() function gives us access to
the Replacement Object Agent and all data members of that object.
The Replacement Object is passed to our script through the ra
variable. We could call the ra variable anything we like, but
because ra is part of the templates provided by the LScript Editor,
we will use it here.
We discussed the difference between local and global variables
earlier. Notice that in the second line of the process() function we
encountered the variable thisframe, which we have not
previously declared. We do not need this variable anywhere else
but inside the process() function, so we just create it dynamically
inside the function. No other section of the script can see the value
of thisframe, and that is alright because we use it only to keep
track of the frame where the script is running.
The variable thisframe gets the current frame through the
Replacement Object data member newFrame. To get to a data
86 V O L U M E 1: L A Y O U T LS C R I P T
member of an Object Agent, use the correct syntax: follow the name
of the variable containing the Object Agent with a period, and then
add the name of the data member. See the sample below:
ObjectAgent.datamember
The correct syntax for our script is as follows:
ra.newFrame
Because newFrame is a read-only data member, it is used to fill a
variable only. We can see below how we handle an assignable data
member.
switch(thisframe)
{
case swapframe1:
ra.newFilename = changeobj1;
break;
case swapframe2:
ra.newFilename = changeobj2;
break;
}
We use the information from ra.newFrame and check to see if we
should change the object or not.
The switch() statement lets us organize a series of choices with
code that is readable and easy to expand. switch() takes a
variable, in this case thisframe, and compares it against a series
of ‘cases’ we define. For our purposes, we gave it two cases to
review on each frame. It will test thisframe against both
swapframe1 and swapframe2 and perform the commands under
each of these cases if it gets a positive match. switch() functions
like a series of if() statements for each swap variable, but it
groups the series in a more logical manner.
As the script runs, if switch() comes across a frame that
matches one of the swapframe variables, it will assign
ra.newFilename to one of the changeobj variables. This will
cause Layout to load the object file in one of the changeobj
variables.
In the options() function below, we assign the values to both
groups of the swapframe and changeobj variables.
CHAPTER 12: OBJECT REPLACEMENT 87
load: what,io
{
if(what == SCENEMODE)
{
changeobj1 = io.read();
swapframe1 = integer(io.read());
changeobj2 = io.read();
swapframe2 = integer(io.read());
}
setdesc(prog);
}
save: what,io
{
if(what == SCENEMODE)
{
io.writeln(changeobj1);
io.writeln(swapframe1);
io.writeln(changeobj2);
io.writeln(swapframe2);
}
}
The load() and save() functions let us store and retrieve
important information for our script. There are two modes that
both of these functions can run under, OBJECTMODE and
SCENEMODE. Since replacement information is saved only in the
scene file and not a specific object, we will run both functions in
SCENEMODE.
Under SCENEMODE, any time we save the scene file, the
information in the save() function is written into the .lws file and
looks like this:
Plugin ObjReplacementHandler 1 Changer
Script I:\PAPERWORK\LSCRIPTDOCS\LS-OR\SCRIPTS\CHANGER.LS
X:\Obj02.lwo
1
X:\Obj03.lwo
30
EndPlugin
Layout gets all the information it needs to reload the plug-in and
fill in the saved values. Notice how the information is stored
sequentially in the order we saved it.
88 V O L U M E 1: L A Y O U T LS C R I P T
Also note that all information written into the scene file is
considered text. Therefore, when we write into the scene file the
integer variable ‘swapframe1’ with the command writeln(), it is
converted into text. Remember this, because we will need to
convert the text back into an integer when the scene file is loaded
again. The integer() command performs the conversion as seen
in the load() function.
options
{
reqbegin(prog);
c1 = ctlfilename("Swap Object",changeobj1,20,true);
c2 = ctlinteger("Swap Frame",swapframe1);
c3 = ctlfilename("Swap Object",changeobj2,20,true);
c4 = ctlinteger("Swap Frame",swapframe2);
return if !reqpost();
changeobj1 = getvalue(c1);
swapframe1 = getvalue(c2);
changeobj2 = getvalue(c3);
swapframe2 = getvalue(c4);
reqend();
}
We create an interface for the users in the options()function;
when the user selects the options button in Layout, the interface
appears. Interface creation is covered elsewhere in this manual, but
we’ll run our script in Layout and see how it works.
RUNNING OUR SCRIPT
Before we open Layout, you should choose three objects to use.
One is our base object and the others are the replacement objects.
Make sure your objects are small and simple so the test goes
quickly. If you choose your favorite million-polygon masterpieces
for the test, you’ll have to wait for them to load when they swap
out.
With that said, let’s open Layout, go to the Edit Plug-ins panel and
load our script into LightWave. When the script is loaded, close the
Edit Plug-ins panel and load the first object.
When your object is loaded, open the properties panel for that
object and select Changer in the Object Replacement pull-down
menu.
CHAPTER 12: OBJECT REPLACEMENT 89
The small triangles on the right of the panel let you open a file
requestor to choose your replacement objects.
90 V O L U M E 1: L A Y O U T LS C R I P T
The number fields are the frames where your chosen objects are
inserted into the scene.
Figure 12-5. Fields show frames where are objects are inserted
Select your objects, change the frame numbers and then click
‘OK’. Now we can make a preview of the animation and watch the
objects get replaced.
Note: Object replacement will not occur if you slide through the
keyframes. You must either make a preview or render the scene to see
the object replacement in action.
CHAPTER 12: OBJECT REPLACEMENT 91
For those of you who have been good and coded along with the
examples, here is the complete listing to check your code.
FULL LISTING (REPLACER.LS)
@version 2.3
@warnings
@script replace
@name Changer
changeobj1 = "";
changeobj2 = "";
prog = "Changer v1.01";
swapframe1 = 1;
swapframe2 = 30;
create
{
setdesc(prog);
}
process: ra
{
thisframe = ra.newFrame;
switch(thisframe)
{
case swapframe1:
ra.newFilename = changeobj1;
break;
case swapframe2:
ra.newFilename = changeobj2;
break;
}
}
load: what,io
{
if(what == SCENEMODE)
{
changeobj1 = io.read();
swapframe1 = integer(io.read());
changeobj2 = io.read();
swapframe2 = integer(io.read());
92 V O L U M E 1: L A Y O U T LS C R I P T
}
setdesc(prog);
}
save: what,io
{
if(what == SCENEMODE)
{
io.writeln(changeobj1);
io.writeln(swapframe1);
io.writeln(changeobj2);
io.writeln(swapframe2);
}
}
options
{
reqbegin(prog);
c1 = ctlfilename("Swap Object",changeobj1,20,true);
c2 = ctlinteger("Swap Frame",swapframe1);
c3 = ctlfilename("Swap Object",changeobj2,20,true);
c4 = ctlinteger("Swap Frame",swapframe2);
return if !reqpost();
changeobj1 = getvalue(c1);
swapframe1 = getvalue(c2);
changeobj2 = getvalue(c3);
swapframe2 = getvalue(c4);
reqend();
}
CHAPTER 13: CUSTOM OBJECTS 93
process()
The process() function contains most of your important
drawing code. This function is called by Layout every time Layout
determines that the object needs to be redrawn and receives a
single argument: the ca Custom Object Agent. This Object Agent
contains numerous data members and methods for the scripter to
create and draw Custom Objects.
@name custText
// Global variables.
obj;
currTime = 0;
The first piece of information we will need is the name of the
script’s object. We will need this information to create the label we
will use to display the object’s name. We can get to this piece of
data from the Mesh Object Agent argument sent to the create()
function.
create: ma
{
obj = ma;
setdesc(“custText”);
}
By assigning the Mesh Object Agent, ma, to the global obj
variable we are making the Object Agent’s data members and
methods available to all the functions in this script. While we were
at it, we also defined the script’s description.
Like any of the animating objects in Layout, a Custom Object uses
the XYZ coordinate system to determine where in 3D space it
should be drawn. For our little script, we will use the current
location of the mesh object as the place we will start drawing our
text. This will make our name indicator easy to find, and easy to
determine the mesh object to which it belongs.
We can get to the mesh object’s position information through a
method for the global Mesh Object Agent we used earlier. However,
the getPosition() method requires an argument indicating the
time the function should sample the Mesh object’s position
information. Since the scene’s current time is passed to the script’s
newtime() function, we will simply add this function, and assign
the time argument to the global variable: currTime:
newtime: frame, time
{
currTime = time;
}
With the global variables set, we can start working on our
process() function. First, we want to get the position of the mesh
object.
96 V O L U M E 1: L A Y O U T LS C R I P T
process: ca
{
// Get some information from the Mesh object.
objPos = obj.getPostion(currTime);
}
Now you can see why we needed those variables to be global. We
not only use the obj Mesh Object Agent from the create()
function, but we also use the currTime value from process().
Next we will actually draw the text:
process: ca
{
// Get some information from the Mesh object.
objPos = obj.getPostion(currTime);
@warnings
@version 2.4
@script custom
@name custText
// Global variables.
obj;
currTime = 0;
CHAPTER 13: CUSTOM OBJECTS 97
create: ma
{
obj = ma;
setdesc(“custText”);
}
process: ca
{
// Get some information from the Mesh object.
objPos = obj.getPostion(currTime);
edge = @ 1, 2, 2, 3, 3, 4, 4, 5, 5, 1, 6, 7,
7, 8, 8, 9, 9, 10, 10, 6, 1, 6, 2, 7, 3,
8, 4, 9, 5, 10, 2, 5, 1, 3 @;
98 V O L U M E 1: L A Y O U T LS C R I P T
The vert variable lists the position of each point in our object
starting with the first point, vert[1], and ending with vert[10].
The edge list defines each edge of our wireframe by listing the
index of the starting and ending points. For example (using the
vert and edge lists from above):
vert[1] = <0.0, 0.0, 0.0>
vert[2] = <1.0, 0.0, 0.0>
edge 1 = 1, 2
meaning:
edge 1 = vert[1], vert[2]
or:
edge 1 = <0.0, 0.0, 0.0>, <1.0, 0.0, 0.0>
Now that our object is defined, we have to set up the process()
function to draw it correctly. Seventeen edges will be made in total.
With a carefully designed for-loop, we can easily draw them in a
single statement.
process:
{
// Draw the lines.
for(x = 1;x < 35;x += 2)
ca.drawLine(vert[edge[x]],vert[edge[x+1]]);
}
The drawline() method accepts two arguments, a starting point
and an ending point. Given these two values, LScript will instruct
Layout to draw a line connecting the two points. Our for-loop
simply travels down our list of verts[] and their associated
edge[]’s and formats them to be drawn correctly. Notice how we
are incrementing the x variable each time the statement loops. This
allows us to access the first vertex by using the counter x, then the
second vertex by simply using x + 1. That way for each step of the
loop we go through, the drawline() method draws one edge.
To mix things up a bit, change the process() function:
process:
{
// Draw the lines.
for(x = 1;x < 31;x += 2)
ca.drawLine(vert[edge[x]],vert[edge[x+1]]);
CHAPTER 13: CUSTOM OBJECTS 99
ca.setPattern("dot");
@warnings
@version 2.3
main
{
vectors = nil;
indices = nil;
pointcount() || error("I need some mesh to work with!");
reqbegin("Mesh2Custom by Bob Hood");
c1 = ctlfilename("Script file","mycustobj.ls");
return if !reqpost();
filename = getvalue(c1);
reqend();
// construct the arrays
editbegin();
foreach(poly,polygons)
100 V O L U M E 1 : L A Y O U T L S C R I P T
{
pnts = polyinfo(poly);
m = pnts.count();
for(x = 2;x < m;x++)
{
vec1 = pointinfo(pnts[x]);
vec2 = pointinfo(pnts[x + 1]);
ndx1 = 0;
ndx2 = 0;
if(!vectors)
{
// initialize the array
vectors += vec1;
vectors += vec2;
ndx1 = 1;
ndx2 = 2;
}
else
{
// find the index for each vector
// in the array
for(y = 1;y <= vectors.size();y++)
{
if(vectors[y] == vec1)
ndx1 = y;
if(vectors[y] == vec2)
ndx2 = y;
}
if(!ndx1)
{
vectors += vec1;
ndx1 = vectors.size();
}
if(!ndx2)
{
vectors += vec2;
ndx2 = vectors.size();
}
}
indices += ndx1;
indices += ndx2;
}
}
CHAPTER 13: CUSTOM OBJECTS 101
editend();
}
file.write("<",vectors[x].x,",",
vectors[x].y,",",vectors[x].z,">");
}
file.writeln(" @;"); file.nl();
file.write("edge = @ ");
for(x = 1;x <= indices.size();x++)
{
if(x > 1)
{
file.write(",");
if(x & 1)
{
file.nl();
file.write(" ");
}
}
file.write(indices[x]);
}
file.writeln(" @;"); file.nl();
file.writeln("process: ca");
file.writeln("{");
file.writeln(" for(x = 1;x < ",indices.size() + 1,";x +=
2)");
file.writeln("ca.drawLine(vert[edge[x]],
vert[edge[x+1]]);");
file.writeln("}");
file.close();
}
CHAPTER 14: PROCEDURAL TEXTURING 103
Shader Functions
Like other Layout scripts, a few functions are specifically
designed for LS-PT. These functions work in a few ways: they help
initialize the shader, determine the frame that the script is
currently acting on, or, as in the case of the flags() UDF, they
request only specific buffers needed for the script.
init()
init() is called once at the beginning of each render sequence;
here you should initialize any script values that need to be set up
before the render process begins.
cleanup()
cleanup() is called at the end of the rendered sequence; here
you perform any variable house-cleaning.
newtime(frame, time)
newtime() is invoked at the start of each new time within the
current render sequence. An integer value 'frame' indicates the
current frame number, while the number 'time' specifies the current
time index. Unlike the frame number, the time index is a floating-
point number generated for each frame. The time index depends on
the frames-per-second setting for the scene.
Example:
In a scene set to 24 frames per second, you get the following
numbers for a series of frames:
FRAME TIME INDEX (24FPS)
1/24 or .042
2/24 or .083
3/24 or .125
In a scene set to 30 frames-per-second, you get the following time
index:
104 V O L U M E 1 : L A Y O U T L S C R I P T
flags()
Like LS-IF, a LS/PT script must tell Layout which attributes of a
surface's texture it will modify. The script may return one or more
of the following values from the flags() function:
NORMAL
COLOR
LUMINOUS
DIFFUSE
SPECULAR
MIRROR
TRANSPARENT
ETA
ROUGHNESS
RAYTRACE
Note: For a full explanation of how Scene() works and the available
types, see page 151 in Volume 2, Chapter 11: Scene Object Agents in
the reference section.
i = integer(thisFrame / pulserange);
if(i != 0)
{
j = thisFrame - (i * pulserange);
}
k = pulserange / 2;
if(j <= k)
{
// pulsing up
sa.luminous = j / k;
Run through a few frames this way on your own and you will see
how it all comes together in the end. Make sure that you process all
of the conditional statements when you do, because the values
change dramatically if you miss an if-statement along the way.
save: what, io
{
if (what == OBJECTMODE)
{
io.writeNumber(pulserange);
io.writeNumber(keepdiffuse);
}
}
The save() function lets you store the shader information in the
object file. In this case we want to save the ‘pulserangee’ and the
‘keepdiffuse’ variables.
There are actually two separate modes for the save() function:
SCENEMODE saves plug-in information in the scene file, and
OBJECTMODE saves plug-in information in the object itself. We want
OBJECTMODE for our shader script because shader information is
stored directly into the LightWave object file.
We use the ‘io’ agent to add the information we want into the
object file. For a list of commands for writing to files, see page 5 in
Volume 2, Chapter 1: Common in the Command Reference section.
load: what,io
{
if (what == OBJECTMODE)
{
CHAPTER 14: PROCEDURAL TEXTURING 113
pulserange = io.readNumber();
keepdiffuse = io.readNumber();
}
setdesc(prog + " is Pulsing on "+string(pulserange)
+ " Frame Intervals");
}
The load() function is similar to the save() function. When an
object with PULSE2.LS attached is loaded, the load function is
called and we retrieve the information we stored with save(). In
this case, we read back the stored pulserange and keepdiffuse
values for this particular surface.
We also do one more thing to give feedback to the user. Because
setdesc() is called in the create() function, it will display the
default values to the user. If the values for this particular surface
differ from the default, they will not be displayed at load time. We
solve this problem by making a call to setdesc() after we load the
surface values from the object. Now the user will see the proper
values in the shader plug-in list without having to open the
interface.
The final part of the code is the options() function, which is
where you build your user interface. The options() procedure is
called when the user selects the options pop-up for your shader.
Although we will not discuss how to create an interface here, a
few details should be pointed out:
options
{
reqbegin(prog);
reqsize(314,69);
The command reqbegin() marks the beginning of the interface
and sets the stage for the rest of the control. The value you pass to
reqbegin() is the name you want to appear in the title area of the
window. This is another reason why we made the name of our
script a variable—by changing the name of that variable at the top
of the code we can change the name of the window as well. Not a
particularly exciting part of the coding process, but it cuts down on
update time and that’s a good thing.
c1 = ctlminislider("Pulse Range",pulserange,1,100);
ctlposition(c1,13,12);
c2 = ctlcheckbox("Keep Diffuse",keepdiffuse);
ctlposition(c2,201,12);
114 V O L U M E 1 : L A Y O U T L S C R I P T
return if !reqpost();
pulserange = getvalue(c1);
keepdiffuse = getvalue(c2);
reqend();
setdesc(prog + " is Pulsing on "+string(pulserange)
+ " Frame Intervals");
}
The last elements that we want to point out in the interface
section of the script are the last two lines. The reqpost() function
returns a Boolean value when the user presses the "OK" or
"Cancel" buttons. The reqend() function is called to actually close
the panel.
Finally, we make one more call to setdesc() to set the new
values for display. You can find the Pulse2.ls script on the
release CD. For those bold coders who entered the code as we went
along, here is the complete code so you can check for mistakes:
PULSE2.LS
// Pulse2
// Cycle the luminosity and diffusion (inversely) of a
// surface so that the surface appears to "pulse"
//
// Updated 04.21.98 Bob Hood
// Updated 07.20.01 Scott Wheeler
@version 2.3
thisFrame;
pulserange = 2;
prog = "Pulse v1.01";
keepdiffuse = true;
create
{
scene = Scene();
pulserange = scene.fps;
setdesc(prog + " is Pulsing on " + string(pulserange)
+ " Frame Intervals");
}
{
thisFrame = frame;
}
flags
{
return(LUMINOUS,DIFFUSE);
}
process: sa
{
if(thisFrame == 0)
return;
j = thisFrame;
if(thisFrame > pulserange)
{
i = integer(thisFrame / pulserange);
if(i != 0)
j = thisFrame - (i * pulserange);
}
k = pulserange / 2;
if(j <= k)
{
// pulsing up
sa.luminous = j / k;
if (!keepdiffuse) sa.diffuse = 1.0 - (j / k);
}
else
{
// pulsing down
j -= k;
sa.luminous = 1.0 - (j / k);
if (!keepdiffuse) sa.diffuse = j / k;
}
}
save: what, io
{
if (what == OBJECTMODE)
{
io.writeNumber(pulserange);
io.writeNumber(keepdiffuse);
116 V O L U M E 1 : L A Y O U T L S C R I P T
}
}
load: what, io
{
if (what == OBJECTMODE)
{
pulserange = io.readNumber();
keepdiffuse = io.readNumber();
}
setdesc(prog + " is Pulsing on " + string(pulserange)
+ " Frame Intervals");
}
options
{
reqbegin(prog);
reqsize(314,69);
c1 = ctlminislider("Pulse Range",pulserange,1,100);
ctlposition(c1,13,12);
c2 = ctlcheckbox("Keep Diffuse",keepdiffuse);
ctlposition(c2,201,12);
return if !reqpost();
pulserange = getvalue(c1);
keepdiffuse = getvalue(c2);
reqend();
setdesc(prog + " is Pulsing on " + string(pulserange)
+ " Frame Intervals");
}
CHAPTER 15: IMAGE FILTER SCRIPTS 117
take and so on. You would save the information with the following
example:
save: what, io
{
io.writeln(shot);
io.writeln(show);
io.writeln(take);
io.writeln(episode);
io.writeln(date);
io.writeln(slateframe);
}
The save() function gets two arguments passed to it from
Layout. The first argument, what, indicates which of two save
modes Layout is currently calling this function from. If what has
the value of SCENEMODE, this indicates that the user has just
chosen to save the scene. However, if what has the value of
OBJECTMODE, this indicates that the user has tried to save the
object with which the script is associated.
Knowing what mode you are working in is very important. In
SCENEMODE, the values that we will save must be in the form of a
string (ASCII), while OBJECTMODE requires the values to be in
binary. Naturally, the what argument will almost always have the
value of SCENEMODE because the only time you will save data in
OBJECTMODE is when you write a Shader script; no other LScript
class has the ability to attach itself to an object.
The io argument passed to the save() function is the Object
Agent we will use to access the script’s lines of data that are stored
in a scene file. We will use the writeln() method to embed string
data into a saved scene file. Once the information is saved, we will
use the load() function to retrieve the values:
load: what,io
{
shot = io.read();
show = io.read();
take = io.read();
episode = io.read();
date = io.read();
slateframe = integer(io.read());
}
120 V O L U M E 1 : L A Y O U T L S C R I P T
options()
The options() function creates the interface for your plug-in,
which is what the user sees when the user selects the Properties
for your plug-in. To learn more about creating an interface for your
plug-ins, see Chapter 5: LSIDE.
Example: Black and White
The first script we will create is very basic: we will take the RGB
values from a picture and average them to create a black and white
image.
First, open the LScript Editor. The editor gives us some great tools
that make creating scripts easier. One of these tools is the
templates function under the tools menu. Select Templates and
highlight Image Filter.
A ready-to-use template loads, and we will use it to create Image
Filter scripts. We no longer need to remember the format for any
script; it is provided for us. We will, however, need to change and
delete some code from this template before we are finished. But,
the groundwork is done for us.
We will not go over any of the scripting commands that do not
directly affect the Image Filter process. Any commands that we do
not review are covered in the reference section at the end of this
manual.
//————————————————————-
// LScript Image Filter template
//
The first few lines of the default script give you an area to put in
your comments and the name of your script. Any line that is
preceded with // is a comment line. All text after this line will not
be evaluated as a command.
Let’s replace the line with the name of the script we will create:
CHAPTER 15: IMAGE FILTER SCRIPTS 121
//————————————————————-
// Black and White
//
A series of compiler directives follow the main comments. At this
point, the only directive that concerns us is the @script directive.
This command helps LightWave classify the type of the script when
it is added to LightWave as a plug-in.
@version 2.5
@warnings
@script image
As you know, the first line of the next group is a comment; it tells
us that all variables defined outside of user-defined functions
(UDFs) are considered global and available to the entire script.
Because the width and height of the image will define our values,
we must define our variables and arrays later in the process()
code. This is because we cannot find out the size of the image
before the process() function is called.
// global values go here
After we initialize global variables, we need the built-in function
calls that tell LScript what to do when the script begins and ends.
create
{
// one-time initialization takes place here
}
The create() function is called when the script is first activated,
which can either be when it is first activated by the user, or when it
is loaded from a scene. It is a great place to set the name of the
script, so, let’s do that.
create
{
setdesc(“Black and White”);
}
The setdesc() command accepts a text value that sits in the
title area in the Image Filter plug-in list. Your scripts look more
professional when you replace the default LScript title with your
title. We named the script, “Black and White” for this example, but
you can name it anything you like.
122 V O L U M E 1 : L A Y O U T L S C R I P T
Through this double loop, we can read and write data for every
pixel in the image. The mathematical formula we use for creating a
black and white image is rather simple. For each pixel, you first
average all three values of Red, Green, and Blue and assign the sum
of the pixel values to the variable avg.
for(j = 1; j <= imgWidth; ++j)
{
// Compute the average value.
avg = (ifo.red[j,i] + ifo.green[j,i] + ifo.blue[j,i]) /
3;
}
Now that we have the average value determined, place the value
in avg into each of the color channels:
avg = (ifo.red[j,i] + ifo.green[j,i] + ifo.blue[j,i]) / 3;
@version 2.5
@warnings
@script image
create
{
setdesc(“Black and White”);
}
process: ifo
{
// Gather the image’s width and height.
imgWidth = ifo.width;
imgHeight = ifo.height;
124 V O L U M E 1 : L A Y O U T L S C R I P T
"get") any motion attribute at a given time and to write (or "set")
any attribute at the current time.
get(attribute,time)
The get() function lets us retrieve data on the orientation of the
object with which the script is associated. The type of information
we get back from the get() function depends on what type of
‘attribute’ we request. Three types of attributes are available:
POSITION, ROTATION, or SCALING.
Example:
If we wanted to get the rotation information on our object we
would make the following call to get() ;
TheObjectRotation = get(ROTATION,time);
A call to the get() function returns a vector type. That is, the
returned value is a grouping of three numbers <x,y,z> containing
the selected attribute’s values. In the example above, the variable
‘TheObjectRotation’ would get a vector value containing the
three values for object rotation as <h,p,b>.
It is then possible to access any one value of the vector by using
its individual name. If we wanted to get the Heading information for
our object, we would make the following access:
ObjectHeading = TheObjectRotation.h;
We can also apply the same technique to the get() function
itself, allowing for a quicker and cleaner way to retrieve specific
channel information :
ObjectHeading = get(ROTATION,time).h;
time is the time index at which you are requesting the ‘attribute’
information. In the above examples, we have been retrieving all
values at the current time. You can, however, ask for values ahead
or behind the current time by adding or subtracting to the time
index. The following code would give you the heading information
one second in the future.
ObjectHeading = get(ROTATION,time+1.0).h
CHAPTER 16: ITEM ANIMATION 129
set(attribute,value)
The set() function lets you set the specified 'attribute'
(POSITION, ROTATION, SCALING) to the provided vector 'value.'
This affects the object's properties at the current time index.
objID (READ-ONLY)
a pointer to the Object Agent to which this particular plug-in has
been assigned.
Scripting Example:
ROTATER.LS
Axis Rotation Control
The example we will examine allows the user to assign a
rotational value for the distance an object has traveled along a
certain axis of motion. One use for this type of controller would be
to assign a nuts rotation along a bolt. As the nut moves up and
down along the bolts length, it would rotate along the threads.
This is a very basic type of object control, but it serves as a good
example of how easy it is to constrain object motion with LScript
Item Animation.
//-----------------------------------------
// Axis Rotation Control
//
// Scott Wheeler
// 09/12/01
@version 2.3
@warnings
@script motion
@name AxisRotationControl
As with all scripts, the top portion is reserved for gratuitous
credit claiming and compiler directives. One directive of note is
@name. With @name, we can assign the script any name we want it
to have in Layout. Without @name, the save name of the script is
used instead.
In this case, with no @name directive, the script would simply be
called Rotater. It is a small detail to consider, but it does help to
make a script appear more professional in LightWave’s plug-in
listings.
130 V O L U M E 1 : L A Y O U T L S C R I P T
rotspermeter = 1.0;
controlaxis = 1;
controlpos = 1;
A good place to declare any global variables used by the script is
immediately after the compiler directives. Globals are variables
that can be seen from anywhere in a script. Their limited cousins
are the local variables, which are defined to do some local task
within a particular UDF.
Note: Local variables are most often used as counter variables for
looping or as temporary storage locations.
ma.set(ROTATION,<roth,rotp,rotb>);
With all of the computing and assigning finished, we need to
reapply the motion to the object with the changes we made. We do
this with the set() method. Because we want to affect only the
ROTATION of the object, we will set() only the ROTATION.
Note here that the ROTATION information is written back as a
vector. This means Heading, Pitch and Bank are combined. That is
the reason there are < > around the variables. The < > allow you to
combine three variables into one vector. Be very careful about the
order in which you create a vector; it does not perform any error
checking for you. If we made a mistake and changed the position of
the variables, we would pass one channel’s values to another. The
following is an example:
Example:
ma.set(ROTATION,<rotp,roth,rotb>);
Here the Heading and Pitch information are flipped, leading to
some rather strange results.
options
{
reqbegin("Axis Rotation Control");
c1 = ctlchoice("Rotation
Axis",controlaxis,@"H","P","B"@);
c3 = ctlchoice("Control Axis",controlpos,@"X","Y","Z"@);
c2 = ctlnumber("Rotations Per Meter",rotspermeter);
return if !reqpost();
controlaxis = getvalue(c1);
controlpos = getvalue(c3);
rotspermeter = getvalue(c2);
reqend();
}
The options() UDF is where we set all of our buttons and
controls for the user when he or she asks for the script's
properties.
Interface design is covered in another section of this manual, but,
for the inquisitive, the above code generates the following
interface.
CHAPTER 16: ITEM ANIMATION 135
load: what,io
{
if(what == SCENEMODE) // processing an ASCII scene file
{
controlaxis = integer(io.read());
controlpos = integer(io.read());
rotspermeter = number(io.read());
}
}
save: what,io
{
if(what == SCENEMODE)
{
io.writeln(controlaxis);
io.writeln(controlpos);
io.writeln(rotspermeter);
}
}
The load() and save() UDFs allow us to save any variables we
want to keep when the scene is saved. This way any variables we
want to restore when the scene is reloaded are restored
automatically for us. Without this we would have to reassign the
values each time we loaded a scene, thus severely limiting the
script’s usefulness.
136 V O L U M E 1 : L A Y O U T L S C R I P T
///-----------------------------------------
// Axis Rotation Control
//
// Scott Wheeler
// 09/12/01
@version 2.3
@warnings
@script motion
@name AxisRotationControl
rotspermeter = 1.0;
controlaxis = 1;
controlpos = 1;
create
{
setdesc("Axis Rotation Control");
}
if (controlpos == 1) rotamount =
pos.x*(rotspermeter*360);
else
if (controlpos == 2) rotamount =
pos.y*(rotspermeter*360);
else
rotamount = pos.z*(rotspermeter*360);
else
rotb = rotamount;
ma.set(ROTATION,<roth,rotp,rotb>);
}
options
{
reqbegin("Axis Rotation Control");
c1 = ctlchoice("Rotation
Axis",controlaxis,@"H","P","B"@);
c3 = ctlchoice("Control Axis",controlpos,@"X","Y","Z"@);
c2 = ctlnumber("Rotations Per Meter",rotspermeter);
return if !reqpost();
controlaxis = getvalue(c1);
controlpos = getvalue(c3);
rotspermeter = getvalue(c2);
reqend();
}
load: what,io
{
if(what == SCENEMODE) // processing an ASCII scene file
{
controlaxis = integer(io.read());
controlpos = integer(io.read());
rotspermeter = number(io.read());
}
}
save: what,io
{
if(what == SCENEMODE)
{
io.writeln(controlaxis);
io.writeln(controlpos);
io.writeln(rotspermeter);
}
}
138 V O L U M E 1 : L A Y O U T L S C R I P T
CHAPTER 17: CHANNEL FILTERS 139
Example1: maxValue
We will start by creating a script that will simply set and enforce a
channel’s maximum value. When the script is enabled on an item’s
channel, that channel will not be able to exceed the predefined
value. First, let’s get some of the script’s header information out of
the way.
CHAPTER 17: CHANNEL FILTERS 141
@warnings
@version 2.5
@script channel
@name maxValue
Nothing really new is going on here. We are just setting some of
the preprocessor directives to instruct LScript on what options we
want to use with this script. Notice that we used the channel
setting in the @script directive to define this script as a Channel
Filter.
Like most of the script architectures found in Layout, Channel
Filters have a number of familiar functions available to them. The
first is the create() function:
create: channel
{
}
The create() function is called whenever the script is initially
loaded. This can happen either when the user applies a Channel
Filter script to a channel, through the plug-in list, or when an object
in a scene file is loaded with a Channel Filter applied. We usually
will set up some of the script’s key variables in the create()
function, check for user errors, and set the script’s description.
Right now we will just set up the script’s description:
create: channel
{
// Set the plug-in list’s description.
setdesc(“maxValue”);
}
As you can see, the create() function gets the channel
argument passed to it every time it is called. This argument is
actually a Channel Object Agent created from the channel on which
the script was applied. We can test this by adding a simple info()
function to create():
create: channel
{
info(“We have attached this to the: “, channel.name,
“channel.”);
// Set the plug-in list’s description.
setdesc(“maxValue”);
}
142 V O L U M E 1 : L A Y O U T L S C R I P T
not the data being displayed that determines when or how often
the functions get called; it’s the design of the Channel Filter script
itself.
Clearly, we cannot use info() requesters to display data within
the process() function. You definitely don’t want to terminate
and reload Layout every time you make changes to your script.
We’ll have to come up with another way to display the data we want
to visualize while we’re learning this type of script. Rather than
getting the channel data all the time, we would really like to be
notified when changes are made to the channel we’re working on.
What we’re looking for is an equivalent to a master’s event handler.
Luckily, the Channel Object Agent has such a method:
create: channel
{
channel.event(“newEvent”);
For this script we will eventually need an interface, but right now
we first need to worry about getting the script to run the way we
want it to. However, for this script to work correctly, we still need
the data that the interface would supply. Rather than spend a
lengthy amount of time making an interface, we’ll define a global
variable and give it an initial value:
@name maxValue
// Global variables:
maxValue = 1.0;
create: channel
{
When the script is tested and proven to be working correctly, we
will make our interface. Next, let’s set up the process() function
to monitor the channel’s value.
process: ca, frame, time
{
// Get the value from the Channel Access Object Agent.
currValue = ca.get(time);
}
We pass the time index to the get() method to retrieve the
channel’s value at that time. The get() method will return a single
floating-point value. Remember that we are simply asking for the
channel’s value, not the entire keyframe’s value. In our example
above, the value returned would be the value of the item’s x
position at the specified time.
Now that we have the value, let’s compare it to our maxValue
variable.
process: ca, frame, time
{
// Get the value from the Channel Access Object Agent.
currValue = ca.get(time);
is, then we want to use the Channel Access set() method to assign
the channels value equal to the maxValue. That way, the channel’s
value can reach, but never be larger than, the value stored in
maxValue. Consider this setting a ceiling of sorts.
That’s it. Experiment with what we have at this point:
1 Load the Cow object.
2 Add the script to the Cow’s x channel.
3 Try to move the Cow along the x axis, exceeding the maxValue
(1.0).
You should notice that although Layout will let you move and
keyframe the item—so the channel’s value becomes greater than
maxValue—as soon as you change or update the current frame,
the item snaps to the maxValue.
So this is the script thus far with an added interface to set the
maxValue variable:
@warnings
@script channel
@version 2.5
@name maxValue
//Global variables.
maxValue = 1.0;
create: channel
{
// Set the plug-in list’s description.
setdesc("maxValue");
}
create: channel
The objName variable will contain the name of the reference
object we’ll be using. We’ll first create a Mesh Object Agent from
this variable in the process() function:
process: ca, frame, time
{
// Create an Object agent
obj = Mesh(objName);
Position.X and matches the name of the channel that was sent
to the process() function as the ca argument. However, we
cannot assume that this will always happen.
We need to search through all the names of the channels in the
item, looking to match the name of the channel that was passed to
the process() function. Therefore, we will look at the same
channel on two different objects. This calls for a while()
statement:
// Get the object's first channel.
chan = obj.firstChannel();
if(chan)
{
// Get the value from the reference
// object’s Channel Object Agent.
maxValue = chan.value(time);
//Global variables.
ObjName = “Null”;
CHAPTER 17: CHANNEL FILTERS 149
create: channel
{
// Set the plug-in list’s description.
setdesc(“maxValue”);
}
LS-GN Functions
The Generic scripts include a few functions; these functions help
you manage Layout scenes.
loadscene(filename[,title])
The loadscene() function loads the indicated filename as a
Layout scene file. The optional title parameter is used to name
the scene file internally once it is loaded. This optional name will
become the scene's new filename. If this parameter is not provided,
the filename parameter is used in its place.
152 V O L U M E 1 : L A Y O U T L S C R I P T
savescene(filename)
The savescene() function saves the current scene in Layout as
the provided filename.
Example: INCREMENTSCENE.LS
Because script functions are basically unrestricted in Generic
LScripts, any example here is incapable of covering more than a
small portion of their scope. With that in mind, let’s look at a short
script that shows the kind of functionality you can add to
LightWave through the Generic script type.
The following script takes a scene loaded into LightWave and
saves out scene files with incrementally increasing version
numbers each time you run the script.
//-----------------------------------------
// Incremental Scene Saver v1.0
//
// Scott Wheeler
// 08-03-01
@version 2.3
@warnings
@script generic
@name LW_IncrementalSceneSaver
The first section of the script is the main comments area, which
was discussed in more detail in the LScript Procedural Textures
(See page 107 in Chapter 14 ).
After our script label is the pragma directives section. To make
your scripts easy to read, all directives should be placed at the top
of the script. Pragma directives are identified by the @ character at
the beginning of the line, followed by the directive name and ending
with a possible argument. You can find a complete list of the
pragma directives in the appendix of this manual.
Before we can change the name of the loaded scene, we need to
find the name of the current scene. We do this by accessing scene
specific information from a Scene Object Agent. We can create a
Scene Object from the current scene by calling the Scene()
constructor.
scene = Scene();
CHAPTER 18: GENERIC SCRIPTS 153
cropsize = (sceneName.size()) - 4;
sceneName = strsub(sceneName,1,cropsize); // crop .lws
Because sceneName holds the full filename of our loaded scene,
it also includes the “.lws” extension. We want to append a new
version number to the end of our scene name and we do not want it
to follow the “.lws” extension. So, we need to trim the “.lws” off the
back of the filename before we do anything else.
First, we find out how big the filename is without the “.lws”
extension. A built-in variable method in LScript makes this process
very easy. The size() method, available to all variables, returns
the variable’s size. In the case of a character string, such as a
filename, it returns the number of characters in that name. By
subtracting four from the total length, we get the length of the
filename without the “.lws” extension.
Now that we know the filename length, we can crop off the
extension. LScript provides a series of commands for processing
strings and extracting sections of those strings. One of these is the
strsub() command. strsub() takes a string and returns a subset
of that string based on the cropping variables you pass it.
Example:
strsub() takes the following parameters:
strsub(<sceneName>,<first character position>,<length>)
let’s say our filename is
sceneName = “X:/myscenes/scene.lws”
The total length of the filename is 21. We want to remove the last
four characters, 21 – 4 = 17. So, a call to strsub() to remove
the last four characters would be:
strsub(sceneName,1,17);
This should be read as, “take the string sceneName and starting
at the first character in that string, give me 17 characters.” Because
the full name is 21 characters long, the last four characters are not
returned.
With the “.lws” extension removed we can now evaluate our
filename for a version number.
if (strsub(sceneName,cropsize - 4,1) == "_")
156 V O L U M E 1 : L A Y O U T L S C R I P T
Our script will append a “_vXXX” where the XXX stands for a
number value (e.g., v001, v002 …). Because this is a fixed value
with a length of four characters, we know that if a version number
exists in our filename the fourth character from the end of the
name will be an underscore (_).
With this is mind, the if() statement checks to see if the fourth
character from the end is indeed an “_”.
ver = integer(strsub(sceneName,cropsize - 2,3)) + 1;
If the fourth character from the end is an “_”, then a version
number exists on the end of the current scene. We now want to
know what that value is so we can increase it by one before saving
the scene.
We have already seen how we can use the strsub() command to
pull a series of characters out of a string. Now we add the
integer() command to the mix. A string value can be converted
into an integer by passing it through the integer() command.
Thus, the integer version number of our current scene is converted
from text to an integer. We can then simply add one to increase the
version number.
sceneName = strsub(sceneName,1,(sceneName.size()) - 5));
This line removes the existing “_vXXX” string from the end of the
filename to prepare for applying the new version number.
else
ver = 1;
The ‘else’ section of our if() statement is performed if the
fourth character from the end is not “_”. This occurs if no version
name was assigned to the scene yet. In this case, we need to make
sure the version number is set to ‘1’ before we save the scene.
pad = "_v" + ver.asStr(3,true);
We use the variable method asStr() to convert the data from an
integer type to a string type. By passing the first argument (3), we
are specifying the length of the string. This way, the string is always
3 characters long.
return((sceneName + pad + ".lws"));
To finish the setupscene() function, we need to return() back
the adjusted scene so that it can be saved.
CHAPTER 18: GENERIC SCRIPTS 157
@version 2.3
@warnings
@script generic
@name LW_IncrementalSceneSaver
generic
{
scene = Scene();
if(scene.filename == "(unnamed)")
SaveSceneAs();
else
SaveSceneAs(setupscene(Scene().filename));
}
setupscene : sceneName
{
cropsize = (sceneName.size()) - 4;
sceneName = strsub(sceneName,1,cropsize); // crop .lws
@version 2.3
@warnings
@script master
@name masterTest
Nothing that we have done so far should look new. We have a
short description, a version number, a warning level, the type of
script we are making, and the name of the script. This is just a
typical beginning for a script.
As we mentioned earlier, Master Class scripts can monitor
practically everything the user does in Layout as a command. This
functionality is one of the major advantages of using a Master Class
script over a Generic script. However, before we can listen in on
the user, we first need to define the relationship between our script
and Layout. We do this in a function called flags().
The flags() function is used by LScript only internally. You will
never call this function manually from anywhere in your code. Its
sole function is to set up how Layout should handle the script after
the scene it is attached to is cleared.
When a Master Class script is started, LScript automatically
searches through your script’s code and calls the defined flag()
function to determine the relationships our script has with Layout.
This function contains a return() statement that we will use to
set these options in Layout. As we mentioned earlier, two options
are available to this function, SCENE and LAYOUT.
So the following example shows how the flags() function for
this script will look:
162 V O L U M E 1 : L A Y O U T L S C R I P T
flags
{
//set flag so that we monitor all Layout events.
return(SCENE);
}
Like most of the Layout scripts we have seen so far, most of the
script’s work is performed in the process() function. As part of a
Master Class script, the job of the process() function is to be
called by Layout every time a command is issued by the user. Two
arguments are accepted by the script’s process() function that
are automatically passed to it from Layout: event and command.
The following example demonstrates the function:
process: event, command
{
// Display the values of the two passed variables.
}
To accomplish our goal, we will simply throw in an info()
function. This will display the two values every time the
process() function is called by Layout.
…
// Display the values of the two passed variables
info(event, " ", command);
…
If you run this script, you will notice that every time you click on
something, alter a setting, or change frames, the info() requester
will appear and display the event number, command used, and the
parameters used by the command (if there are any). The integers
returned by the event argument are easily represented by the
NOTHING, COMMAND, TIME, SELECT, and RENDER_DONE constants.
This displayed data should give you a good idea of what is and is
not a Layout command. More importantly, you should notice how
often the process() function gets called when animating. In fact, if
you use this script while animating, the amount of info()
requesters that get displayed can be incredibly frustrating.
For this reason, you do not want to make the process() code
consume excessive amounts of processing time. Right now our
script slows our productivity down because we have to manually
close the info() requester every time the user does anything.
However, if there were many processor intensive commands put in
place of the info() statement, it could slow down Layout’s
CHAPTER 19: MASTER CLASS 163
Note: The specific length of this generated pause will actually depend
on the speed of your computer’s processor and how long it will take to
perform the for-loop.
Now run the script and try making a scene with a null moving
around the screen. You should notice how much the loop has
slowed the interactivity of Layout. That is because the timer loop is
run every time Layout calls the process() function. Now replace
the for-loop with the info() statement we had in there before.
Here’s the final code:
// masterTest.ls: A script to demonstrate commands sent
// by Layout to Master Class scripts.
@version 2.3
@warnings
@script master
@name masterTest
flags
{
//set flags so that we monitor all Layout events
return(SCENE);
}
}
Save and keep this script. We will use it later in example 2.
Example 2: selected.ls
The example we will tackle next will simply display the selected
items in an interface. This script will greatly expand on what we
have already learned about Master Class scripts by monitoring all
the commands from Layout, and also reacting to a select few.
Specifically, we will look for the commands that handle selecting
items.
For this example, we will do most of the script’s work in the
process() function. However, the final version of this script
requires a user interface. We have dedicated an entire section of
this manual to a discussion of making interfaces in LScript. So
rather than describing what is happening in detail, we will simply
gloss over what we are doing and cover it in the later section,
“Interfaces and LSIDE.”
Setting up the script
First, let’s get the header information out of the way.
// selected.ls – This Master Class script
// lists the currently selected items.
@version 2.2
@warnings
@script master
@name selected
Now let’s put in the functions we want this script to use.
Eventually there will be more code for them, but for now we are just
setting up the script.
create
{
// We need to do some initialization stuff here.
}
flags
{
// This function contains the code necessary to tell
// Layout to clear script if scene is cleared.
return(SCENE);
}
CHAPTER 19: MASTER CLASS 165
create
…
Now that this variable is declared globally, all the functions in the
script can share the Object Agent’s methods and data members.
@version 2.2
@warnings
@script master
@name selected
create
{
// Create a scene object
sceneObj = Scene();
}
flags
{
// This function contains the code necessary to tell
// Layout to clear the script if the scene is cleared.
return(SCENE);
}
if(event == COMMAND)
{
for(i = 1; i <= selItems.size(); i++)
info(selItems[i].name);
}
}
Testing the script.
Clear the current scene and add the Master Class script
“selected.ls” to the plug-ins list on the Master Plug-ins Panel.
168 V O L U M E 1 : L A Y O U T L S C R I P T
1 Select the first object. A single requester, with the name of the
object you selected should appear.
2 Press the OK button to close the panel.
3 SHIFT-select a second object. Once again a requester should appear
displaying the selected object.
4 Press the OK button to close the panel. However, notice that a sec-
ond requester now appears, displaying the first object you
selected.
If you repeat these steps until you have several objects selected,
you will notice a pattern of several requesters opening in
succession, each displaying the previously selected object. How
many requesters eventually appear will depend on the number of
items that are selected. As you can see, by using this method of
displaying data, your script could potentially generate dozens of
requesters. Although this is far from efficient, this test does prove
CHAPTER 19: MASTER CLASS 169
command. Add this line in front of the for-loop (don’t forget the
braces):
…
if(event == COMMAND)
{
if(command == "SelectItem")
{
// Display the contents of the items[] array
for(i = 1; i <= selItems.size(); i++)
info(selItems[i].name);
}
}
Now run the script, and perform some tests to see how we did.
Strangely, you should notice that nothing happens when you run
this code. No matter how many items you select, your script no
longer generates info() requesters. Why? What did we do wrong?
The problem is that our if-then statement is comparing two
strings, the value in the command variable and the string
“SelectItem”, looking for a match. However, using a little
deductive reasoning, if the for-loop and info() statements are not
being executed, then we must assume these values never appear to
be equal. So our code is never seeing the statements bound to the
if-then statement. Something must be wrong with the if-then
statement.
The two sides of the if-then statement never equal because the
variable command actually has a value of “SelectItem
30000000”, not “SelectItem”. Thus, the two sides never equal,
and the for-loop is never executed. To prove this theory, you could
insert another info() command before the if-then statement to
display the current value of the variable command. If you run the
code, you will notice that there was never a chance for the for-loop
statement to run.
We need to isolate the actual command from the entire command
string, ignoring the argument. We can do this by using the parse()
function. The word parse means to break something down into its
component parts. That is essentially what the function will do.
Given a separator string, and the string to parse, the parse()
function will return an array containing all the parts that made up
the initial string. The separator string is simply a character that the
parse() function will look for in order to break up the given string.
For example:
CHAPTER 19: MASTER CLASS 171
if(event == COMMAND)
172 V O L U M E 1 : L A Y O U T L S C R I P T
{
if(command[1] == "SelectItem")
…
Run the script again and select an item. You should notice that
the script reacts only to the selection process. Changing frames or
opening windows no longer triggers the statements bound to the
for-loop statement. We have improved this script’s usability by
simply reducing the actions that trigger the display code.
Unfortunatley, our little fix is faulty. Notice that the info()
requester comes up when you select an object, but try to add an
object to the selection using the SHIFT-click method. Nothing
happens. With further tests you would notice that the desired
effect happens only when the first item is selected, but never
happens when multiple objects are selected. The problem is not an
error in our code, but an error in our preliminary research.
When we initially discovered that the “SelectItem” command
was the command that Layout issued when items were selected, we
quickly implemented a fix that would watch for instances of that
command. We should have spent a little more time to completely
understand how the selection system in Layout works. This would
have led to some additional information we needed before we
started to code our fix. We failed to realize that Layout actually uses
three commands to handle item selection, not just one.
If you run the MasterTest.ls script again and do some further
tests, you wll notice the three different commands Layout uses to
add and remove items from a selection. These commands now
become important to us:
“SelectItem”
“AddToSelection”
“RemoveFromSelection”
For our code to work properly, we need to test for all three of
these commands, not just the one. Luckily it is fairly simple to
change our script to meet these new demands. Simply change the
if-then statement to compare three sets of values instead of one.
if( command[1] == "SelectItem" or
command[1] == "AddToSelection" or
command[1] == "RemoveFromSelection")
CHAPTER 19: MASTER CLASS 173
@version 2.2
@warnings
@script master
@name selected
create
{
// Create a scene object
sceneObj = Scene();
}
flags
{
// This function contains the code necessary to
// tell Layout to clear the script if the scene is cleared
return(SCENE);
}
command[1] == "AddToSelection" or
command[1] == "RemoveFromSelection")
{
// Get the currently selected objects
selItems = sceneObj.getSelect();
Less is More
Our script works correctly, and that is the first step. Now we
should take a look at the code and see if we can make it smaller, run
faster, or make it more efficient. Our script is small, but we already
have one way we can decrease the amount of memory it uses.
Under certain circumstances we may be able to make it run faster.
First, we should look at how we are handling the selItems[]
array. When we run the Scene Object Agent’s getselected()
method, we generate an array of Object Agents that get stored in
the aforementioned selItems[] array. Each of these Object
Agents contains a massive amount of data in the form of Data
Members and Methods, which describe the various properties
found in each individual item.
If we take a closer look at our script, we see that we need to know
only the name of each item in the Object Agent. If we could discard
the unnecessary data from the array, the stored data would be
drastically smaller, and take less memory. Although it is impossible
to remove data from an Object Agent, it is possible to make an
entirely new array of only the items’ names. This would get us the
results we wanted, and also set up some future functionality we will
add to the script later.
To make this new array, we will simply copy each of the items’
names from the selItems[] array and store them in a new array
called items[].We can do this by placing another for-loop after
the selItems[] array is built:
…
selItems = sceneObj.getSelect();
for(i = 1; i <= selItems.size(); i++)
CHAPTER 19: MASTER CLASS 175
items += selItems[i].name;
…
// Display the contents of the items[] array
for(i = 1; i <= items.size(); i++)
info(items[i]);
For these latest round of changes we have altered the contents of
the process() function only. So here is how it stands now:
process: event, command
{
//Parse the command variable
command = parse(" ", command);
if(event == COMMAND)
{
if( command[1] == "SelectItem" or
command[1] == "AddToSelection" or
command[1] == "RemoveFromSelection")
{
// Get the currently selected objects
selItems = sceneObj.getSelect();
Make it pretty
The most obvious problem we have left to solve is how to better
display the contents of the items[] array. The info() requester
we are using works, but it is far too cumbersome and annoying to
use in our final script. We want to display the entire list of items’
CHAPTER 19: MASTER CLASS 177
items, sceneObj;
create
…
Next, we want to set up the options() function. This function is
automatically called when the user either double-clicks the plug-
in’s instance on the Master Plug-in Panel, or when the Properties
option is selected from the plug-in’s Edit drop-list. It contains all of
the interface code needed to draw and update the requester
properly.
options
{
// Check to see if the requester is already opened
if(reqisopen())
// If it is open, close it
reqend();
else
{
// Create a requester named: “Selected:”
178 V O L U M E 1 : L A Y O U T L S C R I P T
reqbegin("Selected:");
@version 2.2
@warnings
@script master
@name selected
items, sceneObj;
CHAPTER 19: MASTER CLASS 179
create
{
// Create the Scene Object Agent
sceneObj = Scene();
flags
{
// This function contains the code necessary to tell
// Layout to clear the script if the scene is cleared
return(SCENE);
}
options
{
// Check to see if the requester is already opened
if(reqisopen())
// If it is open, close it
reqend();
180 V O L U M E 1 : L A Y O U T L S C R I P T
else
{
// Create a requester named: “Selected:”
reqbegin("Selected:");
c0_count
{
// This function serves the interface’s list box
// control and returns the size of the items[] array
return(items.size());
}
c0_name: index
{
// This function serves the interface’s list box
// control and returns the value of the indexed array
return(items[index]);
}
getSelected
{
// This UDF was added to serve two of the functions in
// this script. It simply populates the global item[]
// array variable
Static Scripts
The examples we created in previous chapters have been static.
That is, every time the script was run, the values stored in the
script’s variables remained the same. This naturally caused the
script to perform the same. No matter what scene was loaded, or
whatever surface was created, the script would function in the
same manner, time after time. That is because the script’s variables
were hardwired with static values. The following code example
demonstrates static values:
main
{
objFile = “c:/newtek/objects/animals/cow.lwo”;
load(objFile);
}
No matter how many times we run this script, it will always
perform the same function: load the cow object file. It can never
change because the values stored in the variable objFile cannot
change; its value is static. However, if we were to alter the path
CHAPTER 20: INTERFACE INTRODUCTION 185
Dynamic Scripts
Rather than hardwiring data into the script, you can make a
variable’s value dynamic by creating an interface for it. By giving
the user the ability to change a variable’s value while the script is
running, we can offer different functions or options. For example
let’s look at the previous example again, with one alteration:
main
{
// Interface code
Note: We left out the actual interface code in order to make this
snippet easier to read. We’ll get to the code soon enough, don’t worry.
even more useful now because the user determines which object
gets loaded, not the programmer. This is one of the biggest
advantages to writing an interface to your script.
Requesters
All interfaces must start with a requester. These are the panels
that are displayed when an interface is first created. These
requesters can contain elements, called controls, that either
request or display data from your script. Each requester can have
dozens or even hundreds of these controls. Your only limitation is
the amount of space you have to display everything you need on
the screen.
Each requester can be assigned a name for the title bar display of
the panel. A panel’s size and contained space is measured in two-
dimensional pixels (X and Y), much like the coordinates of an
image. You can either specify a size for the panel, or you can allow
LScript to automatically determine its proper size for you. LScript
takes into account which controls are used, and how much space
each control needs to be displayed correctly. This can result in a
quick-to-finish interface when time is essential.
CHAPTER 20: INTERFACE INTRODUCTION 187
Note: For a complete list of interface controls and functions see page
29 in Volume 2, Chapter 2: Interface in the Command Reference at the
end of this manual.
Types of Interfaces
There are two types of LScripts found in LightWave: those that
function independently from frame specific item data, such as tools
and utilities, and those that evaluate item data on every frame,
such as Item Animation scripts. Both of these interface types are
coded in similar ways and even use the same interface commands
and functions. However these similarities end when it comes to
comparing actual script structure.
In the previous chapters we demonstrated how Modeler and
Generic class scripts use a singular function, main() or generic(),
to contain a script’s commands. This one function contains all of
the code necessary for the script to open, process and close the
script. It is logical to say that we will place all of our script’s
interface code in this function.
When Layout scripts are run, they calculate their code and scene
data on every frame of an animation. This happens when the user is
animating or previewing an animation as well as rendering a scene.
So, these scripts are called whenever a frame changes, and that can
be hundreds or even thousands of times during the course of
making your animation. This poses a major problem when
discussing the structure of interface code.
For example, let’s take a blind approach to creating an interface
for an Item Animation script. Knowing that the important code
belongs in the process() function, you may be inclined to place the
code that creates your interface there. This would ensure that your
interface would be called, and that your variables would be set by
the user. Your script should be happy.
However, one major problem with this approach is that the
process() function gets called every time the information in a
frame changes. So, if the user edits an object, which has the Item
Animation script attached to it, the interface will appear. This
would get old very quickly. In comes the options() function to the
rescue.
With the interface code placed in a function called options(),
separate from the process() function, the panel is called only
when the user requests it. The user can do this by either double-
clicking the script’s instance, located in the list window of the plug-
in, or by selecting Edit > Properties from the plug-in’s command
list.
When you have a firm grasp of how interfaces work, and the code
that goes into making them, we have no doubt that you will find the
Interface Designer incredibly powerful and use it for all your
interface coding needs. So for now, fight the urge to use the
Designer and do it the harder way first. You will appreciate the
knowledge you will gain in the following chapters when you
attempt to write your first big project.
Getting Started
For this example we will focus on some introductory interface
code. Let’s start by making a Modeler Interface for an incredibly
simple script. This script will add two numbers together and
output the result to an info() panel. Let’s set up script as a
Modeler Class script called “ModInterfaceTest.”
194 V O L U M E 1 : I N T E R F A C E S
@script modeler
@name ModInterfaceTest
@version 2.3
main()
{
}
Now to make things a little easier, let’s outline what we plan to do:
1 Draw the interface.
2 Get the values from the interface.
3 Add the two values together.
4 Display the sum.
Now put it in the script:
…
main()
{
// Draw the interface.
// Get the values from the interface.
// Add the two values together.
// Display the sum.
}
The first thing on our to-do list is to draw the interface, but first
let’s make sure the code works. We will start by creating some test
values and variables. When the main part of the script is working,
we will remove the temporary assignments and write the interface
code. This will ensure that the interface will be written on top of
known-to-be-working code. Although it may appear to be a little
backwards, this method will save us time in the long run. We will
not be doing anything major here, but to make sure we are all on
the same page, the functioning code should look like the following
example:
…
main()
{
// Draw the interface.
Note: We have the two temporary variables val1 and val2 in there.
They will be removed when the interface is put in.
If you run this code you should get an info() panel that displays
15. If you ever need a script that displays the sum of 10 + 5, you are
all done! Although the script is simple, we can make it far more
useful if we make an interface that requests these two values from
the user and displays their sum.
The first and primary part of any LScript interface is the
requester. Without one of these panels, we have no place to put our
controls. In order to create one, our interface code must switch
LScript into Requester Mode with the reqbegin() function. This
function does two things: it creates a requester with a given title
and it tells LScript which panel is active. Any controls added after
this call will be assigned to the active panel.
The title that appears at the top of the interface’s panel can be set
by passing a string to reqbegin(). As you can imagine, a
reqbegin() function has a counterpart called reqend(). This
function takes us out of Requester Mode and closes the interface.
Let’s work these two commands into the current script example:
…
// Draw the interface
reqbegin(“Sum of two numbers:”);
reqend();
val1 = 10;
val2 = 5;
…
Now save the file and test the code in Modeler. Do not worry if
you do not see an interface, we have left out a very important step
in the process. All we told LScript is to create a Requester, and to
close it. We need to let LScript know that we are finished making
196 V O L U M E 1 : I N T E R F A C E S
reqpost();
// Get the values from the interface.
reqend();
…
The requester is now displayed, and LScript will not run any more
code until one of the two buttons is pressed. When you run your
code you should get an interface that looks a lot like the one that
follows.
reqpost();
// Get the values from the interface.
…
It is important to create controls with valid and useful default
values. These values are clues to the user for what should be
entered in the requester. Also, if the user does not know how to use
the tool properly, they may just hit the “OK” button to see what the
defaults do. If your default values are illegal or poor choices, the
CHAPTER 21: MODELER AND GENERIC CLASS INTERFACES 197
user will not get a good test drive of your script. In fact, it may even
result in an error.
The two lines we added above demonstrate the way you place a
control on a requester. The ctlinteger() function is called
along with the two passed values. When the control is successfully
created the function returns what is known as a Handle. This is
basically an internal value that LScript uses to uniquely identify
each control. This Handle is then stored in the c0 variable.
If you run your script now, you will see two controls placed nicely
on the screen.
reqpost();
// Get the values from the interface.
…
The ctlposition() function requires three arguments (three
additional arguments are optional). The first is the Handle of the
control you want to affect. In this case we’ll be using the Handle
stored in the c0, and c1 variables. The second and third arguments
are the column number and row. Arranging controls on the screen
can be a very time consuming process due to the fact that you have
198 V O L U M E 1 : I N T E R F A C E S
to run the script to see the results. For this reason, using the LSIDE
interface editor is a real time saver.
Run the script and see the results:
reqend();
Now the values of the controls are stored in the variables val1
and val2. With these declarations complete, the script should now
perform as planned when we run it. Test it out.
This seems to be working fine, but test it again. This time, after
entering the two values, press the “Cancel” button. Notice that it
CHAPTER 21: MODELER AND GENERIC CLASS INTERFACES 199
still displays the result. This goes against a common practice used
in UIs: if the “Cancel” button is pressed, the action should be
ignored. We want to change the script to correct this problem.
The one mistake that we are making is that we are currently
ignoring the return value of the reqpost() function. This value
indicates if the user pressed the “OK” or “Cancel” button to close
the requester. We can get to this value by replacing the current
reqpost() function with the following code:
…
return if !reqpost();
…
This code simply states that if the return value of the reqpost()
function is Boolean 'false' (meaning that "Cancel" was pressed),
then return to the function that called the script in the first place. In
this case, that would be Modeler. This, in essence, ends the script.
Now the script should work.
The completed code:
@script modeler
@name ModInterfaceTest
@version 2.3
main
{
// Draw the interface.
reqbegin("Sum of two numbers:");
c0 = ctlinteger("Value2: ",5);
c1 = ctlinteger("Value1: ",10);
ctlposition(c0, 35, 30);
ctlposition(c1, 35, 5);
return if !reqpost();
create: obj
{
}
destroy
{
}
load: what,io
{
if(what == SCENEMODE) // processing an ASCII scene file
{
}
}
202 V O L U M E 1 : I N T E R F A C E S
save: what,io
{
if(what == SCENEMODE)
{
}
}
Now run this Item Animation script and double-click its instance
in the plug-in list. You should see a panel like the image that
follows:
Inside the options() function you can now create your interface
to function any way you want by adding controls in the same
manner as you did in the previous chapter. They should all look
and function the same as they did when you used them in Modeler
interfaces. However, there is one other area that we need to
address before your Layout interface will work exactly like the ones
found in Modeler.
Variable Scope
When you declare a variable inside a function, the data for that
variable is made available only to the code inside that same
function. For example, if we had two functions, A and B, and we
declared the variable TEMP in function A, function B would not
even know that TEMP existed, never mind give the current value
correctly. This is the problem we currently face with our Layout
interfaces.
The code that gathers the data from the interface is located in the
options() function. The code that performs the actual tasks of the
script is located in the process() function. Therefore, the values
collected from the interface will not be accessible to the script’s
performing code. Because the variables were declared locally to
the options() function, functions like create() or process()
cannot read them. That means that any variable that is shared
between the various functions must be declared as a global
variable.
We can do this by simply placing the name of the variable after
the directives section and before the first function listed in your
code. For example, using the previous code snippet:
@version 2.2
@warnings
@script motion
204 V O L U M E 1 : I N T E R F A C E S
create: obj
{
}
Although we do not want to declare every variable as global, the
variables option1, option2, and option3 are now available to
every function, rather than just the options() function. More
importantly, the code in the process() function can now use the
data collected from the interface.
Description Line
One last thing you should do for the user is to summarize the
important options they chose in the interface and display them on
the script’s description list. This is done using the setdesc()
command and works much like the info() function where the
information you want to be listed is stored as a string argument.
For example, place this line at the end of the options() function:
…
reqend();
setdesc(“Script: ” + option1);
}
…
When the requester is closed, this function will inform the user of
exactly what was selected for option1. Obviously not all of the
selected values can be displayed. The displayed values should be
of some importance—values that the user will care what they had
selected.
Example:
We will make a simple animation clamping script that will invoke a
strict maximum coordinate setting on an item’s keyframe values.
This will be an Item Animation script, we will work in all the
trimmings as we go along. Let’s start with the normal header setup:
CHAPTER 22: LAYOUT INTERFACES 205
@version 2.3
@warnings
@script motion
Now let’s declare a global variable to store the maximum x
coordinate value.
maxXPos = 1.0;
The function we will start with will be process(). Initially, we will
make all our processing code work correctly. Then we will develop
the interface to handle all the values we want to use as user
input(s). Rather than go through the inner makings of an Item
Animation script, we will jump straight into the interface code.
However, so the script runs similarly, here is the working function:
process: ma, frame, time
{
// Using the ma Motion Object Agent, get current
// position.
currPosition = ma.get(POSITION,time);
Execute this script in any item’s motion plug-in list and move it
around. You will notice that the item will not be able to move past
the maxXPos value. In this case, we declared the maximum X
position (maxXPos) globally with a value of 1.0. By manually
changing the maxXPos variable, we change the maximum X
position.
206 V O L U M E 1 : I N T E R F A C E S
xPos = getvalue(c0);
More Functionality
Now that this simple interface is working properly, we can easily
expand the script to cover the two other coordinates. With some
simple copying and pasting, some changes to variable names, and
adding a few more global variables, you could easily have a script
that functioned with the interface shown below:
will be based off of fully functional and tested code that should not
change much after everything is implemented. Working this way is
a much more efficient use of your time.
your life much easier. If you try to implement too much at once, a
simple mistake can leave you wasting a lot of time hunting down
the bug.
Note: Be sure to give credit where credit is due. If you use an idea or
snippet of code that is not yours, be sure to give the original author
credit in the form of a comment (within your code, not on the
interface!).
Note: The flip side of that argument does not necessarily work. If you
like the way your interface works, it is not guaranteed that the
Community will!
CHAPTER 24: LSIDE 213
The File menu lets you handle files from a text editor—you can
create a new file, save a file, save a file as another name, and close
the file you are currently working on.
The only new type of command found in this menu is the Toggle
Write command. This command lets you toggle the write privileges
for your scripts so that you cannot inadvertently write over them.
This comes in very handy when you have numerous scripts open. It
eliminates the chance of accidental errors.
The Edit menu contains commands to Undo, Delete, Cut, Copy,
Paste, and Search and Replace. The functions Cut, Copy, and
Delete require some amount of text to be highlighted before they
are usable. Select the item that you want to perform the action on
and then select the option from the Edit menu. For example, if you
want to delete a line of code, you select the line and then choose
Edit > Delete.These commands work with the Windows Clipboard.
CHAPTER 24: LSIDE 215
The View menu lets you change some of the viewing options
inside the editor. The first option, Font, lets you change the font
size for the display area. You increase or decrease the size.
Figure 24-4. Use the Highlight option to make code stand out
The Tools menu holds one of the most important tools in the
editor, the Templates option.
Figure 24-6. Change the colors of your code on the Colors panel
On the Colors panel we can change the the way text is viewed in
the interface to suit any color preferences. You can alter your
interface so that comments are displayed in a color that you prefer,
operators stand out more, and so on. You can also add a
Background Image to the editor and set its orientation within the
background.
But wait, that’s not all! You can check the syntax of your script
from the Tools menu. If you check your syntax while you script, you
speed up your scripting time. Your productivity increases because
you do not need to execute the script in LightWave to find out
whether or not you forgot to place a semicolon at the end of line 20.
Because the LScript Editor can hold more than one script in
memory at any one time, you can use the Next button to flip
between all loaded scripts.
218 V O L U M E 1 : I N T E R F A C E S
Figure 24-7. The Next button lets you move between loaded scripts
Figure 24-8. The Quit button lets you Quit the LScript editor
Text Area
The main window of the LScript Editor is the text area. Inside this
window, you edit all of your scripts. If you have edited text in any
word processor, then you will have no problem grasping the
concept here. In fact, the LScript Editor can load and edit any text
file.
CHAPTER 24: LSIDE 219
The unique features of the editor are in the two drop menus
above the main text area.
The drop menu on the left is the script drop menu. Like the Next
button in the Menu Area, you use this control to switch between
scripts that are loaded into memory. Unlike the Next button, you
can select any script you like from a list rather than stepping
through them in order.
The script drop menu also places a character at the beginning of
the script name to tell you the status of that script.
* : An asterisk means that the script has been modified and not
yet saved
^ : A carat means that the script is read-only
The right drop menu is for the function list. Like the script drop
menu, you can jump to the names in the list. The difference here is
that you jump inside the active script to any defined function that
may exist. Because a template for a Generic script is loaded in the
LSED, and there are no user-defined scripts yet, the only function in
the list is generic().
Command Area
The field below the Text area is the Command area, which is
where you can enter specific commands.
Figure 24-11. The Command area for typing commands instead of using available
buttons
In the Command Area, you can tell LSED exactly what you want it
to do. This area is for those of you who like to type out commands
rather than using those pesky little buttons.
The following commands are recognized by the Command entry
window:
goto <linenumber>
open
s~<searchtext>~
s~<searchtext>~<replacetext>~
home
end
CHAPTER 24: LSIDE 221
Figure 24-12. The Message area where LSED will display messages about your script
Figure 24-13. The left field is the Position area, and the right field is the Mode area
The Mode area tells you whether or not you are in Insert or
Overwrite mode. In Insert mode, you insert text at the cursor when
you edit; in Overwrite mode, you write over any text as you edit.
LScript Debugger
The LScript Debugger is a source-level script debugger that works
with the LScript plug-ins in real-time. As the script executes, the
LScript Debugger can control that execution through standard
debugging tools such as step, step over, and run. You can place
break points in the script code, and you can monitor, or even alter,
operational values from the debugger interface as the script
processes.
Note: You can no longer run the debugger from outside of LightWave;
the stand-alone application is now incorported directly into the LCore
subsystem alongside of LScript.
224 V O L U M E 1 : I N T E R F A C E S
var red[width];
var green[width];
var blue[width];
var alpha[width];
var i,j;
}
…
The View menu lets you change the font size to one of three
predefined sizes. You can also toggle on syntax highlighting the
same way you can in the LScript Editor.
Source Area
The Source area displays the complete script that is currently
running.
226 V O L U M E 1 : I N T E R F A C E S
Figure 24-16. The Source area where your current script is displayed as it runs
Notice the column to the left of the script listing. Inside that
column is a yellow triangle; the triangle marks the next line that will
execute when you either continue to run the script or step through
the script. The next statement pointer is a fixed position that
moves only when the script advances. You can manually adjust
another pointer, though.
The green triangle is a user-defined positioning marker that you
can place anywhere in the script. You set the break point for the
current script with this pointer and/or scroll through the entire
script.
CHAPTER 24: LSIDE 227
Figure 24-17. Top: The first arrow is the next statement pointer, which cannot be
adjusted. Bottom: The arrow here is the user-defined positioning marker.
You set a break point so you can avoid hitting the next statement
command frequently while the script runs through a loop. You
simply place a break point on the opposite side of a loop and tell
the debugger to keep running (F5). It will then process the loop and
return control of the debugger back to you when it reaches the
break point.
If at any point you want to just let the script continue, you can hit
(F6) or quit out of the LScript Debugger. In those two cases the
break point is ignored and the script keeps running.
Status Area
The Status Area tells you several important details about the
current script; most importantly, it tells you the name of the
current script. The status area also tells you in which user-defined
function (UDF) the script is executing and what line number it is on.
228 V O L U M E 1 : I N T E R F A C E S
Figure 24-18. The Status area gives details about the current script
Watch Area
The Watch Area lets you monitor the values of any variable as the
script runs. You assign a variable to watch with the Add Watch (F3)
command from the Debug menu.
CHAPTER 24: LSIDE 229
When you add a variable, the UDF information from the Status
area comes in handy because you must supply the UDF information
for each variable you wish to track. In the image below, we look at
the i variable from the process UDF.
Figure 24-20. Specifying that the Watch area should watch the variable i
You are not limited to just watching the variable—you can also
double-click on any listed variable, change the value, and see how
this change affects the way the script functions.
230 V O L U M E 1 : I N T E R F A C E S
Message Area
The Message Area is a trap area for any message generated by the
script. Any message generated by the info(), warn() or error()
commands is trapped by the debugger and re-routed to the
Message Area for display.
The Debugger Message area
The Debugger and Editor are integrated. If the LScript Editor is
open, and the script being debugged is loaded in the editor, the
Debugger will tell the Editor to keep the current debug line
synchronized with the current line of the script in the Editor.
Figure 24-23. The Menu area for the LScript Interface Designer
232 V O L U M E 1 : I N T E R F A C E S
Figure 24-24. Left: Options in the File menu. Right: The Export menu and its available
options
If both the Interface Designer and the LScript Editor are open, you
can use the Export function to export your interface as LScript
code directly to the editor. You just tell the Export function the type
of code you would like it to write: Modeler LScript, Layout LScript,
or Panels. If you select Panels, LSID will export C code for you.
The component menu is where you can select from a variety of
interface components. Simply select the one you want to add to the
interface and it is created on your template.
CHAPTER 24: LSIDE 233
Figure 24-25. Available components that can be added from the Component menu
The Tools menu lets you add a new layer to your interface. This
has no effect on how your script looks but you can put similar
buttons on separate layers so that editing a multi-buttoned
interface becomes easier. This behavior is akin to layers in Modeler.
Each layer may have a separate piece of the object, but they are all
considered part of the same object.
234 V O L U M E 1 : I N T E R F A C E S
Figure 24-27. Left: Select the items to align. Right: Items aligned Left
Figure 24-28. Top: Select the items to align. Botton: Items aligned with Bottom
Note: Along with the menu align tools you can use the arrow keys to
‘nudge’ a control in any direction one pixel at a time.
Component Tree
The Component Tree gives you a textural view of how your
interface looks. The left column holds two toggles: the Quill shows
236 V O L U M E 1 : I N T E R F A C E S
you which Layer you are currently modifying, and the Eyeball
toggles the visibility of each layer. The visibility of a layer affects
only the view in the Interface Designer. If you export an interface
with a layer set to invisible, its controls are still exported as part of
the interface. So, delete any controls you do not want before you
export.
Figure 24-31. Click on downward-facing arrow to expand a control and see its values.
Another nifty function of the component tree is that you can use it
to parent controls to one another. This works the same way as
parenting items in the Scene Editor in LightWave. In the component
tree window, drag the child component beneath the component
you want it parented to. A yellow line appears under the parent
item; it will indent slightly to show that the item you selected is
now parented.
238 V O L U M E 1 : I N T E R F A C E S
Figure 24-33. The short line beneath the first Number component indicates a child
item
Figure 24-34. The line between the Numbers indicates a parenting relationship
Now the child items stay attached when you move the parent
item. This is the interactive version of the ctlgroup() command.
Because all parenting occurs when you create the interface in LSID,
the ctlgroup() function is not included in the export code.
Parenting is also important when you create an interface with
tabs. By using parenting, you can place the controls you want
under each tab.
Let’s look at an example of tabs and parenting.
2 The default has three tabs, so let’s add three different controls
under each tab: Boolean, Number, and Choice.
Figure 24-37. Drag each of the controls under the Item lines
4 To see the control under each tab, you click on the tab item you
want. However, you must select individual tabs in the Tree view.
You cannot select individual tabs in the working dialog.
5 Select Export > Layout LScript so we can send our interface to the
LScript Editor. Or, if the Editor isn’t currently running, LSID will sim-
ply prompt you to save the generated code to a disc file.
LSID creates the code in the image that follows, which you can
then cut and paste into your script.
CHAPTER 24: LSIDE 243
At this point you should probably take a moment and save the
dialog as an Interface Designer ( .id) file. This will allow you to edit
the project in the designer at a later date. The code generated from
the export we performed earlier is meant to be used only within a
script. This is not same as saving the project you have worked on
thus far.
Message Area
The Message Area at the bottom of the window is where the LSIDE
system can post any relevant information to the user.
244 V O L U M E 1 : I N T E R F A C E S
LScript Index i
Commands
LSCRIPT INDEX Toggle Write 214
E
Comments 36 Edit
comparison statement 23 Points and Polygons 43
A Conditional Statements 19 Edit menu 214
add version number 155 boolean and if-then-else 22 editbegin 39
if-then 19 editend 39
append version number 155
If-then-else 21 Editor 213
Arithmetic 11 operators 20 Command Area 220
Array switch 23 Message area 222
index 9 Constant Light Types 12 position and mode area 222
Arrays 9 Constants 11 Text Area 218
asterisk 219 light types 12 Editor Menu Area 213
available variables 203 Controls 187 event handler 143
Copy 214 Expression symbols 20
B create function 69, 93, 117
Background Image 217 creating a requester 195 F
Barn.ls script 97 crop filename 155
File menu 214
Basics of Scripting curly braces 15
Filter scripts 117
Modeler 33 Custom Functions 13
FissureLite script 67
Black and White script 120 Custom Objects 93
Fixing Errors 5
Body Markers 15 custText script 94, 96
flags function 69, 93, 159
Boolean and if-then-else 22 Cut 214
Focus 13
Booleans 8
Brackets 9 D Font 215
For-loop 25
break statement 23 Debugger 223 Function list 220
buffers 117 Debugging 5 Functions 13
debugging scripts 213 body markers 15
C Delete 214 cleanup 93, 103
carat 219 Description Line 204 create 69, 93, 117
Case sensitivity 6 design rules 209 destroy 69, 93, 117
Changer script 83 Designing interfaces 190 ending 16
destroy function 69, 93, 117 flags 69, 93, 159
changing point coordinates 69
focus 13
Channel Filters 139 dialog boxes 186
generic 151
Channel Object Agent 139, Directive 53
Image Filters 117
140 Displacement Maps 69 init 93, 103
cleanup function 93, 103 display items 164 load 70, 94, 118
Code Errors 5 Distant light 12 main 15
Code symbols 20 double-slash 36 newtime 69, 93, 103
Colors panel 217 Dynamic Scripts 185 options 70, 94, 120
Command Area 220 dynLimiter script 145, 148 parts of 15
Command History 161 dynSplat script 77 passing arguments 18
Command Sequence 39 process 70, 94, 118, 160
save 70, 94, 118
ii V O L U M E 1 : L S C R I P T I N D E X
U
UDF 143, 154
UDFs 13
Undo 214
upper case names 6
User Defined Function 143,
154
User-defined Functions 13
V
Variable Declarations 6
Variable Names 6
Variable names
case sensitive 6
Variable Scope 203
Variables 5
arithmetic 11
arrays 9
booleans 8
constants 11
global 83
integers 7
light types 12
local 83
number 7
string 8
string math 11
vectors 8
Vectors 8
version number 155
View menu 215
VMaps 53
W
WeldAvg script 50
weldfast script 43
While loop 27
WYSIWYG 190