Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
88 views

L Script User Guide

Isug

Uploaded by

Dona Delavegas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
88 views

L Script User Guide

Isug

Uploaded by

Dona Delavegas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 254

LSCRIPT USER MANUAL

Written by Brian Marshall and Scott Wheeler


This publication, or parts thereof, may not be reproduced in any
form, by any method, for any purpose without prior written
consent of NewTek.

© 2002 NewTek. All rights reserved.


NewTek
5131 Beckwith
San Antonio, TX 78249
LScript Table of Contents i

LSCRIPT TABLE OF CONTENTS

Chapter 1: Scripting Introduction . . . . . . 1


Who Should Read This Book? . . . . . . . . . . . . . . . . . . . . . . 1
What Will You Learn? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
What Happens When You’re Done? . . . . . . . . . . . . . . . . . 3
Chapter 2: Variables . . . . . . . . . . . . . . . . . . 5
Tracking and Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Variable Names. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Case sensitivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Variable Declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
String. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Vectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Booleans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
String Math . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Variable Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Constants. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Chapter 3: Functions . . . . . . . . . . . . . . . . . 13
An Example from the Real World . . . . . . . . . . . . . . . . . . 13
Making a Program and a Function . . . . . . . . . . . . . . . . . . 14
Parts of a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Passing arguments to the function . . . . . . . . . . . . . . . . . . 18
Chapter 4: Conditional Statements . . . . . 19
If-then Conditional Statement. . . . . . . . . . . . . . . . . . . . . . 19
Operators. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
If-then-else Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Boolean and if-then-else . . . . . . . . . . . . . . . . . . . . . . . . . . 22
ii 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

Switch Conditional Statement . . . . . . . . . . . . . . . . . . . . . 23


Chapter 5: Looping Statements . . . . . . . . 25
The for-loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
The while loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Chapter 6: Modeler LScript Introduction 31
Chapter 7: Modeler Scripting Basics . . . . 33
Hello World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
The main() function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Chapter 8: Modeler: Points and Polygons 39
Creating Points and Polygons . . . . . . . . . . . . . . . . . . . . . . 39
Editing Points and Polygons . . . . . . . . . . . . . . . . . . . . . . . 43
Chapter 9: VMaps . . . . . . . . . . . . . . . . . . . 53
What is a Preprocessor? . . . . . . . . . . . . . . . . . . . . . . . . . . 53
What are Object Agents? . . . . . . . . . . . . . . . . . . . . . . . . . 54
Setting up the Test Object . . . . . . . . . . . . . . . . . . . . . . . . 55
Chapter 10: Surfaces and Layers . . . . . . . 63
Chapter 11: Displacement Maps. . . . . . . . 69
create() and destroy() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
newtime() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
flags() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
process() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
load() and save() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
options() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
Example: Splat! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
Example: dynSplat! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Chapter 12: OBJECT REPLACEMENT . . 81
LS-OR Methods and Members . . . . . . . . . . . . . . . . . . . . 81
Chapter 13: Custom Objects . . . . . . . . . . 93
LScript Table of Contents iii

Chapter 14: Procedural Texturing . . . . 103


Shader Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
LS-PT Functions and Data Members . . . . . . . . . . . . . . . 104
Chapter 15: Image Filter Scripts . . . . . . 117
Image Buffers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Image Filter Functions . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Adding a Progress Monitor. . . . . . . . . . . . . . . . . . . . . . . 124
Chapter 16: Item Animation. . . . . . . . . . 127
LS-IA Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Chapter 17: Channel Filters . . . . . . . . . . 139
The Channel Object Agent . . . . . . . . . . . . . . . . . . . . . . . 140
Chapter 18: Generic Scripts . . . . . . . . . . 151
Generic Script Construction . . . . . . . . . . . . . . . . . . . . . . 151
LS-GN Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Chapter 19: Master Class . . . . . . . . . . . . 159
Supported Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
Determining the CS command . . . . . . . . . . . . . . . . . . . . 169
Less is More . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Make it pretty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Chapter 20: Interface Introduction . . . . 183
What is an Interface? . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Static Scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Dynamic Scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Types of Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
LSIDE: Interface Designer . . . . . . . . . . . . . . . . . . . . . . . 190
Chapter 21: Modeler and Generic Class
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 193
Modeler Class Interfaces . . . . . . . . . . . . . . . . . . . . . . . . 193
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
iv 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

Generic Class Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 200


Chapter 22: Layout Interfaces . . . . . . . . 201
Variable Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Description Line . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
More Functionality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Chapter 23: Rules for Designing Interfaces
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Rule #1: Functionality First . . . . . . . . . . . . . . . . . . . . . . 209
Rule #2: Keep It Simple . . . . . . . . . . . . . . . . . . . . . . . . . 210
Rule #3: Speed Over Beauty. . . . . . . . . . . . . . . . . . . . . . 210
Rule #4: One Item at a Time. . . . . . . . . . . . . . . . . . . . . . 210
Rule #5: Industry Standards . . . . . . . . . . . . . . . . . . . . . . 211
Rule #6: Study Examples . . . . . . . . . . . . . . . . . . . . . . . . 211
Rule #7 Listen to User Criticism . . . . . . . . . . . . . . . . . . 211
Rule #8: You Are Your Best Critic . . . . . . . . . . . . . . . . 212
Chapter 24: . . . . . . . . . . . . . . . . . LSIDE 213
LScript Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
LScript Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
LScript Debugger Interface . . . . . . . . . . . . . . . . . . . . . . 224
LScript Interface Designer . . . . . . . . . . . . . . . . . . . . . . . 230
LScript Index
CHAPTER 1: SCRIPTING INTRODUCTION 1

CHAPTER 1: SCRIPTING INTRODUCTION


So you want to learn to make your own LScripts? Great! The
ability to create your own scripts is a powerful asset for an artist.
You can make custom tools that are tailored to meet your exact
needs, and if you can do it well, you’ll be an invaluable asset to
both your organization and the LightWave community. You’ll also
be a very rare breed, a hybrid of sorts between artist and
programmer—someone who can design really cool tools, and make
these tools specifically for their target audience: artists.
Software developers, like NewTek, can’t possibly design
LightWave’s tools to do exactly what you need, every time you need
it. With the wide range of LightWave users, in film, video, print, and
games, the demand for custom tools would be too high. However,
NewTek has designed their toolset so that it satisfies the LightWave
community as a whole. This functionality grows from the core of
LightWave and gives us the power to create our own tools. This is
LScript.
Before we can start learning anything about LightWave’s scripting
language, we should make one thing very clear: Scripting is a form
of programming. Now that every bone in your artistic body is
trembling, let us assure you that NewTek has taken much of the
pain and suffering out of normal programming. They removed many
of the headaches associated with languages like C and C++, and
retained much of their functionality.
We are not saying that it will be easy. We will cover topics and
procedures that, until now, were totally alien to an artist. As long as
you take it one step at a time, read the sections, do the examples in
order, and most of all stay patient, it should be fun. We hope that
you enjoy the wonderful world of programming with LScript.
Who Should Read This Book?
To learn how to program with LightWave’s scripting language, you
need to understand how LightWave works. We are talking about
basic modeling and animating practices, so when we discuss
surfaces and polygons, you can relate. Even if you are a master of
another application and you are just learning LightWave, you
should have these basic skills before attempting to learn LScript.
The tools found in LScript are subsets of those found in
LightWave, so many of the concepts and practices we will discuss
involve how LightWave and LScript interact with each other.
2 VOLUME 1: LSCRIPT INTRODUCTION

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.

What Will You Learn?


The hardest part about learning to program is that you must learn
a little about every aspect of programming before you can create
your first script. As you get better, and your knowledge base
increases, you can expand on what you already know. We use the
teach-by-laying-a-proper-foundation technique that you may
remember from your teachers in high school. Although the
foundation we have to learn is very large, the rewards are plentiful!
To help ease you into scripting, we will use several real-world
examples to illustrate some difficult concepts. We hope that a
different perspective on this subject will help you associate these
concepts with everyday ideas.
We’ve broken down the manual into several sections:
• Part I. Introduction to Programming. You’ll be introduced to
such programming concepts as commands, variables, control
structures, program flow, and functions.
• Part II. Examples. Through the use of examples, you learn how to
create scripts in Modeler and how Modeler and LScript work
together.
• Part III. LScript and Layout. We’ll continue with our teaching-by-
example method and explore the relationship between LScript
and Layout.
• Part IV. LScript Programming Environment. We will cover the
LScript programming environment including the LScript Editor,
Interface Designer, and Debugger.
CHAPTER 1: SCRIPTING INTRODUCTION 3

• Part V. Library of Terms. Here you will find a Library of Terms.

What Happens When You’re Done?


When you’re done, you’ll script your heart out! Your programming
education will not be done when you finish reading this manual,
though. This manual serves only as the primer for your real
education: practice and experience. By sitting down and writing
numerous scripts, you’ll discover your own programming voice and
create some truly useful scripts. You’ll also find a thousand ways to
do things wrong, but that just comes with the territory.
Luckily, there are thousands of books and online articles covering
the various ways to program. Although not all of them will be
applicable to how LScript works, they all should be able to teach
you some techniques that you can apply to programming in
general, and in some cases, programming specific to 3D. Also seek
out discussion groups and LScript enthusiasts; these people are
resources and they love to lend a helping hand to beginners.
4 VOLUME 1: LSCRIPT INTRODUCTION
CHAPTER 2:VARIABLES 5

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.

Note: Vectors also support object messages, discussed later in this


section. A complete list of messages can also be found in the See page
159 in Volume 2, Chapter 12, Variables.

Valid Vector values: <1.9, -2.4, 0.23>, <124.1, -2.35,


1>
Invalid Vector values: <“one”, 5.0, 102.4>
Example declarations:
myVariable = <1.8, -2.5, 0.23>;
myVariable = <124.1, -2.35, 1>;

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

Valid Boolean values: 0, 1, true, and false.


Invalid Boolean values: 4, -2
Example declarations:
myVariable = true;
myVariable = false;

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.

Note: Constants are capitalized because they act so differently from


the other variables, and have such tighter restrictions. Capitalizing
constant values helps to instantly identify them within your code.

Another advantage that constants give you is that they protect


your code against internal changes within LScript. If the
programmers at NewTek find it necessary to change the code and
alter some of the internal values, LWLIGHT_DISTANT will always
mean the same thing to your script.
CHAPTER 3: FUNCTIONS 13

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

The Program: “make the car”


build the frame
build the engine
buy 1000 tires from tire company for $10,000
…other actions…
car is made

The Function: “buy tires”


Buy 1000 tires for $10,000
Buy the materials to build the tires
Assemble the tires
Send 1000 tires to the plant
Granted, the function will look different in LScript (the procedure
outlined above is known as "pseudo-code"), but we have the
necessary components. Let’s look at it a little closer.
First the main program, “make the car” starts to build the frame,
then the engine, and it then needs to buy 1000 tires for $10,000. At
this point, the program realizes that there is a function called “buy
tires” and calls it. In order for the tire company to send the tires, it
must know how many tires to send and receive payment.
These pieces of the function are called the arguments. Arguments
are data that the function needs to complete its task—in this case,
to build tires. When the tire company receives the proper
information, they can produce the tires and send them to the car
manufacturer. The main program, “make the car,” calls the “buy
tires” function, and sends the information: 1000 tires, $10,000. This
process of sending data is known as passing the arguments to the
function.
The last part of the function is called the return value. Simply put,
the return value is whatever the function has agreed to return to
the main program once it is done. In this case, the return value is
the tires.

Making a Program and a Function


Our example is a little far-fetched for a real LScript function, but it
gives you a clear idea of how functions work. Excluding real LScript
terminology, let’s format our example as LScript would want to see
it.
CHAPTER 3: FUNCTIONS 15

The main program: make car.


main
{
buildFrame();
buildEngine();
tires = getTires(1000,10000);
…finish the car…
}
and the function:
getTires: numberOfTires, payment
{
buyMaterials();
assembleTires();
…build tires code…
return(tires);
}

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

Note: The code between two markers is also referred to as a block of


code.

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

But let’s get back to our example:


tires = getTires(1000,10000);
For now, let’s look only at the data after the equal sign:
getTires(1000,10000);
Internally, LScript searches through its database of commands
and statements looking for the word getTires(). LScript has no
commands or statements named getTires(), so it assumes that it
is a User-Defined Function (UDF) and will look throughout the
script to find it. We have already declared the getTires()
function, so LScript does not need to look far. However, if LScript
could not find a function, you would get an error.
Two pieces of data are passed to the getTires() function: 1000
and 10000 (separated by a comma). This is indicated by the two
values contained within the parentheses. In LScript you must pass
the same number of arguments as you have declared in the
function. We declared two arguments in the getTires() function:
numberOfTires and payment.
Remember:
getTires: numberOfTires, payment
{

}
Anything that follows the colon (:) in this declaration indicates
an argument that the function needs. Somewhere in the body of the
getTires() function, it uses the arguments numberOfTires and
payment to perform its task.
The final part of the getTires() function is the return
statement.

return(tires);

The return statement tells the getTires() function to send data
back to the main() function. In this case, we returned the value of
the tires. Notice that the return value is in parentheses. Later on
you will learn how to capture that returned data (hint: remember
the variable on the left side of the equal sign.) so we can use the
data back in the main() function.
18 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

Passing arguments to the function


Okay, let’s look at an example that will actually work in Modeler.
Although this sample piece of code will not do much, it helps us
further explain some of the concepts involving functions.
main
{
a = 10;
b = 15;
c = addAB(a, b);
info(c);
}

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

CHAPTER 4: CONDITIONAL STATEMENTS


In a perfect world, your script would always know exactly what is
going on in your scene or with your model. It would be aware of any
changes or adjustments the user could make. And it would already
know how to deal with any task that it might come across. If you
were making a one-time-only script that ran on only one scene or
object, then you could program around these conditions. Of course
you cannot expect a sophisticated script to work on numerous
scenes or objects in such a way.
There will be times when you need to test if some condition exists
in your object or scene and have your script act accordingly. These
tests are called Conditional Statements.
For example, you want to use a Modeler script to merge two
selected points. Before you can merge anything, you may want to
check that the user has two points selected. If the points are
selected, then the script can merge the two points. But, if two
points are not selected, you must politely tell the user to first select
two points to merge.
A situation like this happens often in programming, and a variety
of statements can perform these types of conditional tasks. The
role of conditional statements is to verify other statements. If a
statement is true, then LScript will execute some piece of code. If a
statement is false, that piece of code is ignored. The most common
of these conditional statements is the if-then statement.
If-then Conditional Statement
The if-then statement checks that an expression is true. If the
expression is true, then the script will execute the block of code
between body markers that immediately follows the statement. If
the expression is false, then that block of code is ignored and
LScript will execute the next line of code following the closed body
marker (}).
Syntax:
if(…expression…)
{
…code to be executed if the expression is true…
}
Example:
if(pointsSelected == 2)
20 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

{
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:

Operator Translates in English to


=i is now the value of
! NOT
== equals
> is greater than
< is less than
>= is greater than or equal to
CHAPTER 4: CONDITIONAL STATEMENTS 21

Operator Translates in English to


<= is less than or equal to
!= is NOT
Now we will use the aforementioned if-then statement to
demonstrate the uses of each operator where applicable:
a = b a is now the value of b.
if(a == b) if a equals b…
if(a != b) if a does NOT equal b…
if(a > b) if a is greater than b…
if(a >= b) if a is greater than or equal to b…
if(a < b) if a is less than b…
if(a <= b) if a is less than or equal to b…

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.

Boolean and if-then-else


When you combine the if…then/else statement with a
Boolean variable in the expression, you get an interesting situation.
If the variable’s Boolean value is true, then the expression is
considered true. Likewise, the expression is considered false if
the variable has a false value.
Example #1:
objSaved = false;
if(objSaved)
info(“Object was saved.”);
else
saveObject();
…next statements…
CHAPTER 4: CONDITIONAL STATEMENTS 23

Example #2
done = false;
if(!done)
info(“The procedure IS NOT done.”);
else
info(“The procedure IS done.”);
…next statements…

Switch Conditional Statement


The switch() conditional statement is extremely powerful. Like
the if-then statement, an expression is solved, but its resulting
value is compared to a list of values. If the solution matches a list
item, then the code directly following the case statement is
executed. The break command leaves the switch() statement all
together.
Syntax:
switch(…expression…)
{
case /value1/:
…statements…
break;

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

CHAPTER 5: LOOPING STATEMENTS


The simplest of LScripts contains a series of Layout or Modeler
commands. These commands are executed one statement at a time,
one after another until the end of the script.
What if you wanted to repeat a series of statements x amount of
times? You could type the exact statements over and over again.
But that would be insane, and very inefficient. Not only that, but
what if you do not know how many times you want the statements
to repeat? The value may be based on a user input, or a variable
that was set by another command. It would be impossible to create
an LScript to handle these situations without using a looping
statement.
A looping statement executes a block of code while a condition is
true. When it executes the last statement within that block of code
and comes across the closed marker (}), it returns to the
statement’s open marker ({) and repeats the code. It repeats until
the expression becomes false, then it moves on to the next
statement. The most common of these statements is the for-loop.

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

for(i = 0; i < 10; i++)


Next, LScript checks to see if the expression defined in the second
part of the statement is true or not. In this case, LScript checks to
see if the variable i is less than 10. (Currently, i is 0, so because 0
is less than 10, the expression is true.)
for(i = 0; i < 10; i++)
If the expression is true, LScript will execute the statement(s) in
the block of code directly following the statement. The only
statement which we have setup to run is a simple info() box:
for(i = 0; i < 10; i++)
{
info(i);
}
However, this info() is not printing straight text. This info()
statement prints the current value of the variable i, which is our
iterator variable. This will allow us to better understand what
changes are actually happening to the variable i.
The for statement is not done yet. When all the statement(s) in
the block are run, the iterator variable is changed. How the variable
gets changed is defined in the third portion of the for statement.
for(i = 0; i < 10; i++)
In this case, we used i++ , which is a quick way of saying i = i +
1. This statement defines how the variable i is changed every time
the code gets looped. So where i started off having a value of 0,
after all the statements in the code block are run, i will have the
value of 1, then 2, then 3. This means that the info() box will be
appearing and printing the value of i, until i is no longer less than
10.
As you can see, using the for statement not only gives your code
the ability loop, but it can also count. This can come in very handy
when dealing with values in arrays. Say you have the following
array:
objects[1] = “Null”;
objects[2] = “cow.lwo”;
objects[3] = “ball.lwo”;
Imagine what this code will do:
CHAPTER 5: LOOPING STATEMENTS 27

for(i = 1; i <= 3; i++)


{
info(objects[i]);
}
When the iterator in a for statement tests false, LScript moves on
to the next bunch of code.

The while loop


The for statement is nice when you want to repeat a bunch of
statements a known amount of times. However, knowing when you
need to stop looping is not always possible, especially in a script
designed to handle a wide range of situations. What if you want the
loop to run until an action from the script tells the loop to stop?
Enter the while loop to save the day!

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

Before we enter the while loop, we must declare the Boolean


variable to be false. This variable is what the while loop will check
to see if it should continue or not.
done = false;
This variable will simply act like a light switch to the block of
code bound to the while statement. When the variable done is
false, the loop runs, when it is true , the loop stops. Therefore, in
order for any of the statements in the while block to run even
once, the done variable must initially be set to false.
First, the while() statement checks to see if the Boolean
variable is not true (false). If this is indeed the case, the bound
code begins to run.
while(!done)
{
if(x ==5)

The if-then statement checks to see if the integer variable x is
equal to 5. In this case, the variable x has a value of 0. So the else
block of the if-then statement is executed. The else block is the
iterator for this while() loop. The variable x now has a value of 1
and the loop repeats.
Eventually, x gets a value of 5 and the if-then statement becomes
true. The Boolean variable done is assigned a value of true.
done = true;
When the while() expression checks to see if done is false,
the loop ends. The focus of the program then turns to the next set
of statements.
So what is so dangerous about this? Take a good look and see if
you can find what is wrong with this example:
CHAPTER 5: LOOPING STATEMENTS 29

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

CHAPTER 6: MODELER LSCRIPT INTRODUCTION


Up until now you have learned the basics of computer
programming without any real reference to LightWave, 3D
animation, or even LScript. Now it’s time to apply the knowledge
you gathered to familiar concepts and theories: 3D modeling and
animation.
This section should be a progression of sorts from the previous
chapters. We will use the theories and practices covered in the
previous section, which means that you should already have a
basic understanding of the following concepts:
• Variables
• Conditionals
• Functions
• Control Structures
If anything seems unfamiliar to you, please take a moment to
reread these chapters, because you need a good frame of reference
about these concepts. We don’t expect you to be a master
programmer at this point, but we won’t revisit material as we
continue on in our noble quest. Instead, we’ll build on and reinforce
these programming concepts to expand on what you already know.
Remember, LScript is a programming language, and like any
language, you must start with the basics. You’ve learned about
nouns and verbs, and it’s time to learn how to build sentences and
paragraphs. Eventually, you’ll be able to make a short story, and
finally your bestseller. But before you can accept your Pulitzer, you
have to survive second grade!
We will use a different approach to teaching in this section of the
manual. Instead of learning by discussing concepts and theories as
we did in the previous section, we’ll use a method called Learning
by Dissection. In Learning by Dissection, you learn how to program
by analyzing individual lines of code and how they impact the
script’s performance. You’ll use this method for the rest of your
programming career, so get used to it!
The objective for this section is to teach you the basics of
LScripts for Modeler. The language for programming for Modeler
and Layout is similar, but the structures and program flow are
radically different. A Modeler script lacks much of the
programming overhead that Layout needs to run. A Modeler script
isn’t any less powerful—it just has fewer technical requirements.
32 V O L U M E 1: M O D E L E R LS C R I P T

Think of Modeler as a large spinning gear in the complex machine


known as LightWave. A script is actually a much smaller machine,
with a single gear trying to mesh with the larger Modeler gear. As
you can imagine, trying to get your little gear running with a gear
that’s already spinning is no easy task. But if everything is done
correctly, the two gears spin together in unison, with the Modeler
gear providing the power for the smaller LScript machine to run.
A script that runs in Layout is a little more complex. Rather than
meshing with the one Modeler gear, the script gear must mesh with
a half-dozen Layout gears! It’s tricky, but as you’ll see in the Layout
section of this manual, it’s definitely possible. Starting with LScripts
in Modeler is a good first step before we wrestle with the Layout
machine.
The LScript programming language offers you complete access to
the commands and functions found in Modeler. You can glimpse the
possibilities of this powerful addition to LightWave in the hundreds
of scripts already available. Remember, if you can do it in Modeler,
you can do it in LScript.
That said, interfacing with some sections of Modeler is much
easier than with others. We’ll cover a small subset of Modeler’s
functions to accomplish our goals for this section, which should
teach you enough to get you rolling. The back of this manual
contains a complete list of commands and functions for Modeler.
Our goal is to show you how to use some of these, and give you the
knowledge to learn the rest.
So let’s get scripting!
CHAPTER 7: MODELER SCRIPTING BASICS 33

CHAPTER 7: MODELER SCRIPTING BASICS


Let’s start off by making a quick little LScript for Modeler, just to
get our hands dirty. This piece of code will introduce you to the
process of making your first script and outlines how you get that
script into Modeler—nothing more. (We need to start somewhere.)
In this process, we will gloss over a few of the tools we can use to
write scripts. One of these tools is the LScript Editor—the custom-
made text editor provided by NewTek. You can find the editor
program, named LSED.exe, in the LightWave Programs directory.
The LScript Editor is the programming environment where we will
write and debug our scripts. An entire section of this manual is
dedicated to using the LScript programming environment, so feel
free to expand on what we show you here.
Hello World
If you have ever tried to learn a programming language, you know
that one of the first programs you write is the “Hello World!”
exercise. Its only purpose is to show you how to make a script and
execute it—in this case, in Modeler.
So here it goes:
1 Open the LScript Editor.
2 Create a New script by selecting File > New. The large area of the
interface will display a new LScript file. This is where you type your
code.
3 Enter the following code into the workspace:
main
{
info(“Hello World”);
}
4 Save the script by selecting File > Save As..., and name it
“hello.ls.” Be sure you remember where you saved the file! In
fact, to help you get organized, make a new folder for storing your
own scripts. That way you will have a quick and easy place to store
and find your scripts.

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.

The main() function


ALL Modeler scripts have a main function. As the name implies, it
is the main function of the script. When Modeler first loads the
script, LScript will immediately look for the main() function. It will
then attempt to execute all of the code within the body markers. In
fact, the following script works:
main
{
}
It does absolutely nothing, but this little piece of code is all that
Modeler needs to execute a script.
As we learned in the chapter on Functions, we can often pass
variables to functions for them to use. The main() function is an
exception to this rule; it cannot take any variables as arguments. If
you try, you will get an error message… so don’t.
Inside the body markers there is the one line of code:
info(“Hello World!”);
As you can see, we use the info() function to send various
messages to the screen. Usually, it tells the user something they
need to do, or did wrong. You use this function by putting the text
or value you want sent to the screen within the parentheses. You
can use a text message, a variable containing a text message, or a
straight value. To better understand what is actually happening,
let’s use some programming talk here:
The “Hello World!” string is passed to the info() function.
CHAPTER 7: MODELER SCRIPTING BASICS 35

However, if you entered this line instead:


info(Hello World!);
LScript would spit out the following error message:
“In main, line 3, found ‘W’, expecting “)” or “~” (Line:
info(Hello World!);)”
The message is pretty descriptive; it says that on line three,
LScript found an error at the letter “W” and that it was expecting
either the “)” or “~” character. Basically, without the quotation
marks, the info() function thought that you were sending it a
variable. This is perfectly legal. However, it is not legal for the
variable to have a space in it. Granted, we were not looking for this
behavior at all; we never expected LScript to think we sent a
variable. But it just goes to show you how a simple error can really
confuse LScript. These errors will happen, so just smack yourself
on the forehead and move on.
To continue with the idea of variables, if you entered:
info(HelloWorld);
The script will now run without displaying an error message.
When it runs, though, it displays a value of “(nil),” which
indicates that the HelloWorld variable has no value stored in it.
So how do we get the HelloWorld variable to work? Let’s try this
code:
main
{
HelloWorld = “Hello World!”;
info(HelloWorld);
}
On line three, we defined the HelloWorld variable as having the
string value, “Hello World!” Therefore, when we pass that
variable to the info() function, info() displays the value,
“Hello World!”
Remember, the open and closed quotation marks (“ ”) define a
string. If your value does not have these, LScript will consider it a
variable.
Let’s make this piece of code look a little prettier:
main
{
36 V O L U M E 1: M O D E L E R LS C R I P T

text = “Hello World!”;


info(text);
}
By giving the variable a much more general name, we make it
describe a wider range of values. This change is all cosmetic, of
course; we can name the variable anything we want. It could read
something like this:
tfaGxCw = “Hello World!”;
Though the name is quite legal, it is not very descriptive.
Remember that someday other people may need to look at your
code, or worse, you may need to look at it years later. You want to
know instantly what this variable does, so the more descriptive and
easier it is to read, the better. While we are talking about making
scripts more readable, let’s talk about comments.

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.

Note: Using the /* and */ commenting method has no effect on


code that may have been commented using the // method. Any
comments are still commented out.

Comments are completely optional, but you want to put in as


many as you can. You will appreciate this advice later!
Well, those are the basics. In the next chapter, we will move on to
a topic with a little more meat… working with points and polygons.
38 V O L U M E 1: M O D E L E R LS C R I P T
CHAPTER 8: MODELER: POINTS AND POLYGONS 39

CHAPTER 8: MODELER : POINTS AND POLYGONS


At the very core of Modeler’s coolness is its ability to easily create
points and polygons. Modeler then goes one step further and offers
you dozens of incredibly powerful tools to edit this geometry. As
every 3D artist knows, points and polygons are the basic geometric
components used in 3D models. Therefore, it is vital that you know
how to create and edit geometry with LScript.
Creating Points and Polygons
Before we can start, we need to cover an area of LScript that we
really have not addressed yet—operation modes. LScript
commands are organized into two categories: Command Sequence
(CS) and Mesh Data Edit(MD) operation modes. You have already
been exposed to operations, in both modeling and scripting, which
are found in the Command Sequence mode. These are commands
that perform actions on groups of geometry as a collective, such as
move(), rotate(), bevel(), and extrude().
The Mesh Data Edit mode is a little trickier to use, but most of the
down-and-dirty commands are found here. These commands let
you create points and polygons, get information like quantity, and
assign surface names. To execute these commands, you must
switch Modeler to the Mesh Data Edit mode by using the
editbegin() command. When Modeler is in this mode, LScript
cannot run commands from the Command Sequence mode without
generating an error. Some of these commands include
addpoint(), addpolygon(), pointinfo(), and polysurface.
To switch back to the Command Sequence mode we have to
execute the editend() command. This command does two things:
it lets us run Command Sequence commands and it actually
performs the edits sent by the Mesh Data Edit operation mode. Up
to this point, all the code that was run in the Mesh Data Edit mode
was buffered; no action was actually performed on the geometry
until you called the editend() function. In fact, editend() also
accepts a Boolean value that will cancel the changes and ignore the
instructions in the buffer.
To help explain this concept, let’s make a script that creates a
series of three points. Because this is a Modeler script, we need to
start off with the customary main() function.
40 V O L U M E 1: M O D E L E R LS C R I P T

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

polygon's normal faces. If we reversed the order in the array, the


polygon's normal would flip around.
Like the points exercise showed us, the addpolygon()
command is a function as well. It also returns a polygon identifier to
whatever variable is listening. We can use this value with several
other LScript commands to edit the geometry of the triangle. For
now, let's send the value to the polypointcount() function. This
simple function will return the number of points in a polygon.
Granted, we know this already, but it is an easy way to test the
polygon id.
However, we have overlooked one thing—the
polypointcount() function works only in the MeshDataEdit
mode. Remember that with the MeshDataEdit mode all edits are
buffered, and edits are not executed until the
editend()command. That poses a particular problem for our
script. Because the polygon will not be created until the
editend() function is called, we will not get our id in this
MeshDataEdit session, because the polygon is not created yet!
We need to call editend() to create the polygon id, then switch
back to MeshDataEdit using another call to editbegin(). Then we
will call the function, store the return value, and switch back to the
Command Sequence mode.
main
{
editbegin();
point[1] = addpoint(0,0,0);
point[2] = addpoint(0,1,0);
point[3] = addpoint(1,0,0);
polygon = addpolygon(point);
editend();
editbegin();
pntCnt = polypointcount(polygon);
editend();
info(pntCnt);
}

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

Editing Points and Polygons


We have shown you how to create your own points and polygons,
but what if you want to edit some points and polygons that already
exist? Usually, you want to adjust geometry on a current layer, so
you must be able to get at that data. Whether you are working with
points or polygons, accessing and changing the data is easily
accomplished in LScript. To demonstrate this, we will make a few
scripts for welding points together.
Example: weldfast
Problem: We need a way to streamline the welding process by
somehow removing the info()box that shows up after every weld.
Modeler’s weld tool has suited us fine, until we have to weld points
together thousands of times!
Research: We should first flip through the documentation and
release notes for the best way to approach this problem. We can
also search through any scripts that already work either on the
NewTek CD or online. Although these scripts may not do exactly
what we want, they can often offer some insights on how to
approach the problem. A few key areas to look out for are:
1 Ways to get a point’s position information.
2 How to move points around in 3D space.
3 How to weld them together.
Solution: We can recreate Modeler’s weld tool and just leave out
the pesky info()box.
After further researching we can make a general plan of attack to
follow when we make our script.
1 Gather a list of points selected.
2 Determine the coordinates of the last point selected.
3 Move every point to the position of the last point.
4 Merge the points together.
Code:
As always, let’s start off the code with a little main() action:
main
{
// Gather a list of points selected.
44 V O L U M E 1: M O D E L E R LS C R I P T

// Determine coordinates of last point selected.


// Move every point to position of last point.
// Merge points together.
}
Notice that we took the exact text from our attack plan and
entered it in the script as comments. This will help us stay on track,
and keep to the plan. It also lets us see how far we have come, and
how far we have to go.
Save the code as weldfast.ls.
Now we want determine which points and polygons we want to be
looking at. We can choose all geometry or only geometry that is
currently selected. For our purposes, we want to put the power into
the user’s hands, so we will work only on points that the user
currently has selected in the layer. We can use the selmode()
command to instruct Modeler to edit only selected geometry.
main
{
// edit only selected components.
selmode(DIRECT);
// Gather a list of points selected.
// Determine coordinates of last point selected.
// Move every point to position of last point.
// Merge points together.
}
The three options for this command are DIRECT for just the
selected geometry, USER for implicit selections (assumed
selections, for example, Include/Exclude) and GLOBAL for
everything in the layer. In this line, we are actually sending a value
to the selmode() function, but remember that the capital-letter
variables represent constant values. Somewhere in the heart of
LScript they have hard-coded that the variables GLOBAL = 1, USER
= 2, and DIRECT = 3. These variable names are a lot easier to
remember than having to memorize which number represents each
item, so we will use the constant.
Next, we want to eliminate some of the possible user errors by
making sure that some points are actually selected. We will use the
pointcount() command to determine the number of currently
selected points.
main
{
CHAPTER 8: MODELER: POINTS AND POLYGONS 45

// Edit only selected components.


selmode(DIRECT);
// Get number of currently selected points.
pntCnt = pointcount();
// Gather a list of points selected.
// Determine coordinates of last point selected.
// Move every point to position of last point.
// Merge points together.
}
To test the command, throw in an info() box to check the value
of pntCnt. Next, make some geometry in Modeler, select some
points, and run the script. It should output exactly the number of
points you had selected.
We also want to test whether or not the value of the pntCnt
variable equals zero, which would indicate that no points are
selected. If it equals zero, then spit out an error message for the
user.
if(pntCnt == 0)
error(“No points selected.”);
}
The error() function works much like info(). Instead of
opening a dialog box and continuing to run the script, it displays an
error message and stops running the script. Now that we have
taken care of that possible user error, let’s get to the meat of the
script.
We want to get a list of currently selected points. Luckily, we can
use a command that we have already used. The editbegin()
function that we used to switch to the MeshDataEdit operating
mode also creates the points[] and polygons[] variable arrays
for us. When our script switches to the MeshDataEdit operating
mode, LScript automatically creates these arrays and populates
them with the ids of the currently selected components and will
come in very handy with our script.

// Gather a list of points selected.
editbegin();
// Determine coordinates of last point selected.
// Move every point to position of last point.
editend();
// Merge points together.
46 V O L U M E 1: M O D E L E R LS C R I P T


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

1 Start point. We need to initialize a variable as the point where the


loop begins counting. In our case it will be the first point in the
array.
2 End point. We need to know when we can end. That will be the num-
ber of points selected, so we will build a conditional statement to
control the loop.
3 Variable change. How will the loop count? Most loops count one
value at a time, like we do: 1, 2, 3, and so on.
4 Code to run. We will do this after; let’s just worry about the loop for
now. We will use an info() statement to let us watch it count cor-
rectly.
The structure of the for statement looks like this:
for( initialize variable; condition statement; variable
change)
Let’s start plugging in the values we have. We know where to
start, point 1, so we make the counting variable currPnt, start at 1.
for( currPnt = 1; condition statement; variable change)
Just as we used pntCnt to represent the last point index in our
pointinfo() statement, it’s safe to say that we can use the
pntCnt variable to represent the total number of points selected.
Now we can use this number as the value for “how many times we
will do the loop.”
for(currPnt = 1; currPnt <= pntCnt; variable change)
This translates to “if the value currPnt is less than or equal to
the value pntCnt, then do the code that is bound to the for-loop
statement.” Why the “<=” and not just “<,” you ask? Let’s set up an
example to demonstrate exactly what is going on with the
conditional statement:
When currPnt = 1 and pntCnt = 3, then currPnt <
pntCnt is true. So we will run the code.
When currPnt = 2 and pntCnt = 3, then currPnt < pntCnt is
true. So we will run the code.
However, when currPnt = 3 and pntCnt = 3, then currPnt
< pntCnt is false. So we will not run the code.
As you can see, the code will not run on the third point—and we
want it to run. Because pntCnt is equal to currPnt, instead of
48 V O L U M E 1: M O D E L E R LS C R I P T

greater than currPnt, the conditional statement becomes false.


However, if we use <= instead of <, the for-loop will actually
execute three times, rather than two. This is exactly what we want
to do. You can see why, in the beginning, you should test out many
of the loops you make with the info() command. It lets you see
exactly what is going on.
Let’s finish the for-loop statement by instructing it to add a
value of 1 to the variable currPnt after each pass. Remember,
without this increment, the value stored in the currPnt variable
will never change. This will create an infinite loop and freeze
Modeler. So let’s add the increment:
for(currPnt = 1; currPnt <= pntCnt; currPnt++)
currPnt++ is actually translated to currPnt = currPnt + 1.
This makes it appear to count upwards by one every time the code
block is done. This now makes the function complete.

editbegin();
// Determine coordinates of last point selected.
lastPntPos = pointinfo(points[pntCnt]);
for(currPnt = 1; currPnt <= pntCnt; currPnt++)
{
// Move every point to position of last point.
info(currPnt);
}
editend();

Run it and you will see that the script will count up to the number
of points you have selected, just as we planned. We are almost
done—just two more lines of code to go!
Now that we have the for-loop counting correctly, let’s move
some points around. Replace the info() line in the for-loop with
the following line of code:
pointmove(points[currPnt],lastPntPos);
The pointmove() command takes two arguments. First, we pass
to pointmove() the point identifier of the point we want to work
on. We are still using the identifiers in the points[] array, but we
have put in a little for-loop magic here. Notice that the array’s
index value references the variable currPnt instead of using an
CHAPTER 8: MODELER: POINTS AND POLYGONS 49

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

// Gather a list of points selected.


editbegin();
// Determine coordinates of last point selected.
lastPntPos = pointinfo(points[pntCnt]);
for(currPnt = 1; currPnt <= pntCnt; currPnt++)
{
// Move points to position of last point.
pointmove(points[currPnt],lastPntPos);
}
editend();
// Merge the points together.
mergepoints();
}
We can easily create two other scripts based on the weldfast
script. We added comments to areas that differ from the original
weldfast.ls.
WeldAvg.ls

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.

What are Object Agents?


Object Agents can be considered super variables. They contain
two different types of tools for accessing the data stored in these
variable-like containers: data members and methods. An Object
Agent data member contains property information about the stored
data. That information can be names, types, flags, and values. The
Object Agent also contains functions called methods that can
perform tasks or return values. Example methods are count(),
getvalue(), setvalue(), or setcolor().
Let’s try these lines in the main() function:
//Create a VMap Object Agent.
vmapObj = VMap(VMWEIGHT);

//If there are no maps, notify the user.


if(vmapObj == nil)
error(“No weight maps in the mesh!”);
In the previous piece of code we applied two different Object
Agents. In the first instance, we created an Object Agent that
references a particular type of VMap. Let’s look at the line a little
closer.
vmapObj = VMap(VMWEIGHT);
The Object Agent called vmapObj is created by the VMap()
constructor. A constructor is simply a function-like piece of code
that creates an Object Agent for us to use. This particular
constructor creates an Object Agent that references VMap data in
the object. VMap() can take a single argument, which is the type of
VMap we want to reference; the argument can be passed as a string
or a constant. The following list shows the constants, and their
values, that we can pass to VMap():
VMSELECT “select”
VMWEIGHT “weight”
VMSUBPATCH “subpatch”
CHAPTER 9: VMAPS 55

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.

Setting up the Test Object


Often when you make an LScript you want to test your code on a
very basic object. The object should meet the absolute minimum
requirements to show whether your code works or not. As the
script grows and your confidence in its performance grows, the test
should get progressively more complex, until you finally test the
script on a real object.
56 V O L U M E 1: M O D E L E R LS C R I P T

In our case, we want a single polygon, with a single weightmap


assigned to its points named, “testMap.” The script we will make is
not very dynamic, so it should be considered single-use code—
never applicable to anything but our test object. Later in this
manual you will be introduced to interfaces, where you can take a
simple one-shot script like the one we will make, and turn it into a
powerful, multi-use bundle of code.
Create this basic object and save it to disk (you may want to
reload it after a while). Continuing with our example, enter the
following code:
//We are going to scale the VMap 50%
scaleAmt = .5;

//We want to deal only with the geometry selected.


selmode(DIRECT);

//Enter MeshDataEdit mode.


editbegin();
editend();
Nothing is really new here. We made a variable equal to the
percentage (in decimal) that we will use to scale the VMap values.
Then we set the selmode to DIRECT, and entered MeshDataEdit
mode. In the edit block enter these lines of code:
//Get the number of points selected.
pntCnt = pointcount();

//Set up the for-loop to cycle through the selected points.


for(pnt = 1; pnt < pntCnt + 1; pnt++)
{
//Test the loop.
info(“point” + pnt);
}
We used the info() function a bit differently this time. In a large
script you may want to output numerous values, and sometimes
these values can be difficult to distinguish. Here we used a little
string math and created an output that makes a little more sense.
Load your test object, select the four points of your polygon and
run the script.
You should get four info boxes that count up (point1,
point2…).
CHAPTER 9: VMAPS 57

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]);

//Test the output.


info(values);
}
We use the vmapObj Object Agent to check if the WeightMap is
mapped on the current point. We do this by passing a point
identifier to the isMapped() method. The method returns a
Boolean value of true or false, which solves the conditional if-
statement.
In the next line, we pass the point identifier to the getValue()
method. This method returns the value stored in the VMap for the
current point. We then check the data with the info() function. If
you run the script, you should see four values ranging from 0.0 – 1.0
displayed.
All that is left is to set the VMap values to 50 percent of their
current value. Replace the info() lines with this code:
//Loop through all the dimensions of the value
for(x = 1;x <= vmapObj.dimensions; x++)
values[x] *= scaleAmt;
The for-loop will cycle through all the dimensions of the value; in
our case, only one value is associated with a WeightMap. The loop
block multiplies every dimension of the value (still only one) by the
scaleAmt, which equals .5 (or 50 percent).
Remember:
values[x] *= scaleAmt;
Actually means:
values[x] = values[x] * scaleAmt;
The value in the variable values[x] is multiplied by scaleAmt;
then that value is turned around and stored back into values[x].
It’s a simple increment, much like those found in the for-loops.
58 V O L U M E 1: M O D E L E R LS C R I P T

The last line is:


//Scale the Selected VMap values by 50%
vmapObj.setValue(points[pnt],values);
This line passes two variables to the setValue() method:
points[pnt] and values. The value of the variable, named values,
is put into the value of the VMap in the point represented by
points[pnt].
Okay, that’s it—put Modeler’s OGL shade mode into weightMap
so you can see what is going on, and run the script. Notice how the
value diminishes by half. Pretty cool.
Here is the final code:
//Preprocessor Compiler Directives.
@script modeler
@name “tmp”
@version 2.3

main
{
//Create a VMap Object Agent.
vmapObj = VMap(VMWEIGHT);

//If there are no maps, notify the user.


if(vmapObj == nil)
error(“No weight maps in the mesh!”);

//We are going to scale the VMap 50%


scaleAmt = .5;

//We want to deal only with the geometry selected.


selmode(DIRECT);

//Enter MeshDataEdit mode.


editbegin();
//Get the number of points selected.
pntCnt = pointcount();

//Setup for-loop to cycle through selected pts.


for(pnt = 1; pnt <= pntCnt; pnt++)
{
//Check if VMap is mapped to the current point.
if(vmapObj.isMapped(points[pnt]))
CHAPTER 9: VMAPS 59

{
//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;

//Scale the Selected VMap values by 50%


vmapObj.setValue(points[pnt],values);
}
editend();
}
Just as a sneak peek, see what you can do with a little more code
and an interface:
//Preprocessor Compiler Directives.
@script modeler
@name “tmp”
@version 2.3

main
{
//Create a VMap Object Agent.
vmapObj = VMap(VMWEIGHT);

//If there are no maps, notify the user.


if(vmapObj == nil)

error(“No weight maps in the mesh!”);


/* using a while statement, if there’s a VMap value, and
its type is VMWEIGHT add the names to the variable.
This handles multiple VMaps in the object. */

while(vmapObj && vmapObj.type == VMWEIGHT)


{
vmapnames += vmapObj.name;
//Use the .next() method to go to the next VMap.
vmapObj = vmapObj.next();
}

/* This is the elusive interface code, more later.*/


60 V O L U M E 1: M O D E L E R LS C R I P T

reqbegin(“Scale Weight VMap”);


c1 = ctlpopup(“VMap”, 1, vmapnames);
c2 = ctlnumber(“Scale by (%)”,50.0);
return if !reqpost();
vndx = getvalue(c1);
scaleAmt = getvalue(c2)/100;
reqend();

/* Recreate the Object Agent with the name of the VMap the
user chose from the drop-list. */

vmapObj = VMap(vmapnames[vndx]);

//Just in case it can’t make the Object Agent.


if(vmapObj == nil)
error(“Could not instance_VMap`”, vmapnames[vndx],
“`!”);

//We want to deal only with the geometry selected.


selmode(DIRECT);

//Enter MeshDataEdit mode.


editbegin();
//Get the number of points selected.
pntCnt = pointcount();

//Setup for-loop to cycle through selected pts.


for(pnt = 1; pnt <= pntCnt; pnt++)
{
//Check if VMap is mapped to the current point.
if(vmapObj.isMapped(points[pnt]))
{
//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;

//Scale the Selected VMap values by 50%


vmapObj.setValue(points[pnt],values);
}
}
CHAPTER 9: VMAPS 61

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

CHAPTER 10: SURFACES AND LAYERS


Dealing with surfaces in Modeler is a simple task for LScript. A
surface is basically a string that represents the name of the surface
and the polygons that belong to it. We can use dozens of tools to
edit that information. Also, we have a handful of specific, but
powerful commands at our disposal that make creating and editing
surfaces relatively painless.
Layers are a vital aspect of modeling in LightWave. When you
know how to control layers, you can exploit the capabilities of
Modeler and LScript. Layers generally work in LScript the same
way you would expect them to work in Modeler; you determine
which is the foreground layer, which are the background layers,
and what geometry belongs to each.
In this section, we will cover these two aspects of LScript by
making a script that breaks up an object according to the polygon’s
surface name. After we separate the polygon into smaller pieces,
we will place each new object on a separate layer. For a test object
we recommend the NewTek favorite—the cow object. It has a good
amount of geometry, and several surfaces.
So let’s get to it:
@script modeler
@version 2.3
@name “FissureLite”

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);

/* Get the current foreground layer. This should be the layer


that the object is in.*/
64 V O L U M E 1: M O D E L E R LS C R I P T

workLayer = lyrfg();

//Get the first surface from the surface list.


currSurf = nextsurface();
The lyrfg() function returns one or more integers that indicates
which layer is the current foreground layer. We will store this value
in the workLayer variable, so we remember which layer the
original object is in.
Surfaces are arranged in an internal list managed by Modeler. We
can navigate through this list by using the nextsurface()
command. When the command is given no arguments, it returns the
first surface in the list. However, when the command is given a
surface name, it uses this argument as a point of reference and
returns the next surface in the list. In the line above, we are looking
for the first surface name.
The while() command is one of the most dangerous commands
you will use in scripting. If you use it incorrectly, you can create an
“endless loop,” which causes you to manually kill Modeler, and lose
all your data. We will set up our while() command very carefully
to avoid this outcome. Important: do not test this code until we
say that all the necessary code is in place, and safe to use.
//loop until you run out of surfaces.
while(currSurf != nil)
{
}
When this loop is finished, it will run through all the surfaces in
Modeler’s internal list. When the string value in the variable
currSurf becomes nil the loop will end. Therefore, we must
force the currSurf variable to equal nil at some point, or we will
loop forever. We solve this problem by temporarily setting the
condition statement in the while() command to be false, thus
ending the script.
//loop until you run out of surfaces.
while(currSurf != nil)
{
//Terminate the loop.
currSurf = nil;
}
Because we forced the loop to finish, we can safely test the script.
You should not see anything happen when the script runs, but the
CHAPTER 10: SURFACES AND LAYERS 65

script should terminate and give you control of Modeler. We could


have used the break command to get out of the loop, but we
wanted to test what values will make the conditional statement
false. Now we know that we can give currSurf the value of nil,
and the loop will end.
This condition is designed to work this way. We needed a way to
know when to stop looking in the list for surface names. Luckily for
us the last surface in the list always has the nil value for a name. So
it is guaranteed: if we search through the list we will eventually
come to the end, which is indicated by the nil value. Now we just
need to make sure we get to the end!
//loop until you run out of surfaces.
while(currSurf != nil)
{
//Select the polygons with the surface.
selpolygon(SET, SURFACE, currSurf);

//Terminate the loop.


currSurf = nil;
}
The selpolygon() function is a very complex command; it is
probably one of the most complicated commands to use. In our
simple application of this command we pass it three values. The
first two values are constants that represent the major and minor
modes. SET says that we will select polygons, not CLEAR them.
SURFACE is the minor mode, and it says that we will use a surface
name as a parameter for selecting polygons. The third argument is
the actual surface name we are looking for.
Now try running the script on the cow object. When the script
terminates, you should see the eyes selected. That is because the
CowEyes surface is the first in the list, so it was selected when we
used the selpolygon() command.
The list may often contain surface names that do not possess any
geometry. Therefore, we should check that we actually select
polygons when we scan the list. If we do not perform this check,
and the script selects no geometry, the entire object will be
removed when we use the cut() command. That would be bad, so
enter this code after the selpolygon() command:
66 V O L U M E 1: M O D E L E R LS C R I P T


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();

//Find a free layer.


emptyLyr = lyrempty();

//Switch to that layer.


lyrsetfg(emptyLyr[1]);

//Paste geometry from the clipboard into layer.


paste();
First, we will use the cut() command to remove the polygons
from the object and place them in Modeler’s clipboard. Then, we
want to find the next available empty layer. We will do this by using
the lyrempty() function. This function (and lyrfg()) returns an
CHAPTER 10: SURFACES AND LAYERS 67

array of integers representing which layers are empty and safe to


use. To switch to an empty layer, run the lyrsetfg() command
with an integer for the argument. If we use the first index of the
emptyLyr array variable, this represents the first available layer
we can use. Finally, if we use the paste() command we will take
the geometry from the clipboard and put it in the empty layer.
Run this script, and you will see that the cow’s eyes get cut and
pasted to the other layer.
Now, we need to replace the code for terminating the loop with
something a little more logical. Delete the lines that set the
conditional statement to false and put the following code in the
bottom of the while() loop:
// Using the current surface as a reference,
// get the next surface.
currSurf = nextsurface(currSurf);

/* If the loop continues, you want to be back on the original


layer with the rest of the geometry. Remember, we saved this
value in the variable workLayer. */
lyrsetfg(workLayer);
We can use the surface currently stored in the currSurf variable
as a reference in the nextsurface() function. These two lines of
code combine with the while-loop to cycle through all the
surfaces until the name nil comes up. Then the script will
terminate.
That’s it; we’re done. This is the final code:
@script modeler
@version 2.3
@name “FissureLite”

main
{
//Switch to USER mode.
selmode(USER);

/*Get the current foreground layer. This should be the


layer that the object is in.*/

workLayer = lyrfg();
68 V O L U M E 1: M O D E L E R LS C R I P T

//Get the first surface from the surface list.


currSurf = nextsurface();

//loop until you run out of surfaces.


while(currSurf != nil)
{
//Select the polygons with the surface.
selpolygon(SET, SURFACE, currSurf);

pntCnt = pointcount();
if(pntCnt)
{
//Put the selected geometry into the clipboard.
cut();

//Find a free layer.


emptyLyr = lyrempty();

//Switch to that layer.


lyrsetfg(emptyLyr[1]);

//Paste geometry from the clipboard into layer.


paste();
}

// Using the current surface as a reference,


// get the next surface.
currSurf = nextsurface(currSurf);

/* If the loop continues, you want to be back on the


original layer with the rest of the geometry.
Remember, we saved this value in the variable
workLayer. */

lyrsetfg(workLayer);
}
}
CHAPTER 11: DISPLACEMENT MAPS 69

CHAPTER 11: DISPLACEMENT MAPS


Displacement Map scripts allow the scripter to alter an object’s
point coordinates over time. Using the methods for a Displacement
Map Object Agent, a scripter can read and write coordinate data to
the object’s points in order to alter its appearance. A Displacement
Map script could be used to make an object appear to be melting,
create inertia, or handle other complex animation.
Like most of the Layout script architectures found in LScript,
Displacement Maps use a number of predefined functions to utilize
the options available to this script class. While some of these
functions are necessary in order for your script to work correctly,
others merely offer optional functionality.
create() and destroy()
The create() and destroy() functions handle the variable
initialization and cleanup procedures for your script. The
create() function can also receive a Mesh Object Agent as an
optional argument. The Object Agent is created from the object
from which the script is applied.

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.

load() and save()


The load()and save() functions allow Layout to load and save
specific data pertaining to the operation of this script. Any selected
options or settings, which could determine how the script should
run, must be saved within the scene file. This will ensure that when
the scene is saved or loaded, the item’s channel information, as it
pertains to your script, is not discarded.
When Layout loads a scene and comes across an entry for your
script, it calls the load() function. In this function, you should
read and assign settings and values that will enable the script to
perform as expected. Likewise, the save() function is called
whenever the user saves a scene. This function should contain all
the code necessary to organize and save the script’s data to a scene
file.

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
{
}

newtime: id, frame, time


{
}

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

Now, we will put in a few info()’s in the newtime() and


process() functions, in order for us to learn the values we get
from Layout and when we get them.
newtime: id, frame, time
{
info(“newTime: id: “, id, “ frame: “,frame, “ time: “,
time);
}

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

The second info() panel that is displayed is the result of the


info() function we put in process(). The data sent to the screen
represents the x position of the object’s first point. Each of the
subsequent info() panels that appear shows the x position of the
rest of the points. Notice how the box has eight points, and a total
of eight panels show up. That’s why we avoided making an object
with too many points in it.
Viewing every point’s x position is a bit much for our current
testing needs. We really need to see only one of the point’s data to
test our script. We want to set up a counter variable so that we
know which of the object’s points we are currently on. First, set up
the counter as a global variable:
// Global variables;
splatValue = 0;
pntCounter = 0;

newtime: id, frame, time


We no longer need the point information to be sent to the screen,
so remove the info() function from the newtime() and
process() functions. We will add a new info() function to
handle the object’s point data differently. Add the following code.
process: da
{
pntCounter++;
if(pntCounter == 1)
info("process: point: ", pntCounter, " ",
da.oPos[1]);
}
By adding this code, you are keeping track of the current point
number being processed. Run it and you will notice that only the
first point is displayed. However, to see a loophole in our logic,
change the frame. From what you know of Displacement Map
scripts, you expect the whole event to start over again—going
through all the points, and displaying the x position of the first one.
But it does not work like that. Can you see what we did wrong?
The error is this: although the counter is performing correctly, we
have not instructed the script to reset the counter when the frame
changes. The first time the script is run, it counts from one to eight.
However, the second time it runs, it counts from nine to sixteen.
74 V O L U M E 1: L A Y O U T LS C R I P T

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

coordinates, so replace the code in our current process()


function with the following code:

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

// Determine if the point has become less


// than the minimum value allowed.
if(yPos <= splatValue)
da.source[2] = splatValue;
Technically, we are done. However, now that we are done testing
only the first point’s values, let’s get rid of all the pntCounter
references, info()’s and conditional statements. We will also
remove any functions that no longer have any lines of code in them.
Your final script should look like this:
@warnings
@script displace
@name splat

// 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];

// Determine if point has become less


// than the minimum value allowed.
if(yPos <= splatValue)
da.source[2] = splatValue;
}
To better demonstrate the full meaning of this script, change the
two keyframes so that frame zero has a (0,2,0) value, and frame ten
has a (0,0,0) value. Watch as the box object collides with the xz
plane.
As promised, here is the interface code for our script. Simply add
this code snippet after the closed body marker of the process()
function.
options
CHAPTER 11: DISPLACEMENT MAPS 77

{
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

Finally, we have the processs() function. This is where most of


the changes will occur. In fact, it’s probably a good idea to just start
from scratch:
process: da
{
}
We want to access the position data of an animating object, and
use its y value as the point’s minimum value. First, make sure that
the obj Mesh Object Agent has been created.
process: da
{
if(obj)
{
}
}
If the Object Agent was successfully created, we can get at its
position information.
if(obj)
{
pos = obj.getPosition(currTime);
}
The getPosition() method is going to return a vector of the
object’s position within the scene at the currTime time
(remember this global variable was set in the newtime()
function). Now, all we have to do is compare the values, like we did
in splat.ls, and we are done:
{
pos = obj.getPosition(currTime);
if(da.source[2] < pos.y)
da.source[2] = pos.y;
}
Now, we can use another object’s y position to determine where
the splat plane will be placed, just as long as an object with the
name “Null” is in the scene. Here’s the final script with a couple
changes in it to accommodate the new interface code:
@warnings
@script displace
@name dynSplat
80 V O L U M E 1: L A Y O U T LS C R I P T

// Global variables.
obj;
objName;
currTime;

create
{
setdesc("dynSplat!");
}

newtime: id, frame, time


{
currTime = time;
if(objName)
obj = Mesh(objName);
}

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

CHAPTER 12: OBJECT REPLACEMENT


LScript for Object Replacement (LS-OR) lets us control what
objects are used and when they are used in an animation. Objects
can either be replaced based on a specific frame or time, or they
can change based on location. In fact, almost anything can be used
to trigger an object’s replacement. It is entirely up to the
programmer.
LS-OR Methods and Members
The command center for LScript Object Replacement is the
process() function. The process() function receives a single
argument, a Replacement Object Agent. The Replacement Object
Agent provides a series of data members that gives the script
important information about the animation. Most of these data
members are read-only, meaning we can see the information, but we
cannot change it.
The following list contains the available members:
READ ONLY
objID is an Object Agent that represents the object whose
geometry you are replacing. objID gives you access to all the
common Object Agent methods.
curFrame is an integer that represents the frame number for the
current geometry.
curTime is a number that represents the time index for the
current geometry.
newFrame is an integer value that represents the frame number
for the next step. New geometry should be loaded if the object
needs to look different at this new frame.
newTime is a number that represents the time index for the next
step. New geometry should be loaded if the object needs to look
different at this new time index. curTime and newTime may not be
sequential, since network rendering can cause the renderer to jump
around between non-sequential times.
The time index is a floating point number generated for each
frame. Unlike the frame number, the time index is dependent on the
frames-per-second setting in the scene.
82 V O L U M E 1: L A Y O U T LS C R I P T

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.

RENDER indicates that a complete render will be done.

newType is a constant that indicates the type of rendering at the


next frame/time index. This can be one of NONE, PREVIEW, or RENDER.
curFilename is a string value that represents the current object
geometry file, and may be 'nil' if no geometry is loaded. This is the
full path name of the object as it exists on disc.
CHAPTER 12: OBJECT REPLACEMENT 83

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.

Note: You should assign a newFilename only when you want to


replace the current geometry; don’t make assignments with the
current object.

Now let’s look at a sample Object Replacement script. In this


script, we take an object and replace it with up to two different
objects at a specified frame in the animation. We will break the
script down into more manageable sections to see how we perform
this task.
CHANGER.LS
//-----------------------------------------
// Changer
//
As with all scripting types, the ‘//’ characters denote an area for
user comments. See page 107 in Chapter 14 ’for more information
about comments.
@version 2.3
@warnings
@script replace
@name Changer
The pragma directives follow the script label. Because these
directives get processed first, they can be placed anywhere in the
script. However, to make your scripts easier to read, it is good
programming practice to place them at the beginning.
Next comes the Global variable declarations area. LScript
recognizes two types of variable declaration: global and local. A
local variable belongs to a certain localized area of a script. Any
variable declared from inside a function is considered local and is
destroyed when the function terminates. This can be useful when
you want to use a few variables for a counter and have no need to
reference those variables somewhere else in the script.
A global variable, however, is available to all functions and its
values are not destroyed when the function using them is
terminated.
84 V O L U M E 1: L A Y O U T LS C R I P T

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

Figure 12-1. Script name in interface

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

Figure 12-2. Highlight the Changer option in Object Replacement pull-down.

Select the Options button next to the Object Replacement pull-


down and open the Changer interface.

Figure 12-3. Changer Interface

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

Figure 12-4. Triangles open a file requestor

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

CHAPTER 13: CUSTOM OBJECTS


LightWave’s Custom Objects allow the user to attach special, non-
rendering objects to other mesh items loaded in Layout. These
objects can be used to display information to the user, offer tool-
specific data, or visualize effects previously undefined in
LightWave. This functionality allows a simple null to become text,
area-of-effect indicators, and even complex HUDs. These are all
incredibly powerful visualization tools to the user, but more
importantly, invisible to LightWave’s renderer.
Custom objects are constructed from a list of point coordinates
and edges, both of which are provided by the script. These lists
define the wireframe mesh that becomes the Custom Object. Edges
can be drawn using numerous methods including solid lines, dots,
dashes, and long dots.
Custom Object scripts can use the following Layout functions:
create() and destroy()
The create() and destroy() functions handle the variable
initialization and cleanup procedures for your script. For the
Custom Object script type, the create() function can receive an
optional Mesh Object Agent created from the object to which the
script is applied.
init() and cleanup()
The init()and cleanup()functions get called at the beginning
(init()) and end (cleanup()) of each render pass.
newtime()
The newtime() function is called each time the current time
index changes in the scene. In Custom Object scripts, the
newtime() function receives two arguments from Layout: frame
and time. These values are typically assigned to global variables
that are used in several supporting functions.
flags()
The flag() function sets options available to the Custom Object
script class by returning flag values to Layout. Currently only one
flag value is defined, SCHEMA. This flag tells Layout that you
support drawing in Layout's schematic view. If the script does not
support such drawing, then it is best not to define this function.
Typically, only one command is bound to this function: the
return() statement.
94 V O L U M E 1: L A Y O U T LS C R I P T

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.

Note: For a complete listing of data members and methods available


to the Custom Object Agent, please See page 239 in Volume 2, Chapter
29 , Custom Object Agents in the reference section at the end of this
manual.

load() and save()


The load() and save() functions allow Layout to load and
save specific data about the operation of this script. Any selected
options or settings that could determine how the script should run,
must be saved within the scene file. This will ensure that when the
scene is saved or loaded, the item’s channel information, as it
pertains to your script, is not discarded.
When Layout loads a scene and comes across an entry for your
script, it calls the load() function. In this function, you should
read and assign settings and values that will enable the script to
perform as expected. Likewise, the save() function is called
whenever the user saves a scene. This function should contain all
the code necessary to organize and save the script’s data to a scene
file.
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 set up and run the script’s
interface.
Example: custText.ls
For our first example, we will make a simple Custom Object that
will display the name of object to which the script is applied. To
start, let’s get the header information out of the way and set up our
two global variables:
@warnings
@version 2.4
@script custom
CHAPTER 13: CUSTOM OBJECTS 95

@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);

// Using the ca Object Agent, create the Custom Object.


ca.setColor(<100,100,100>);
ca.drawText(objPos, obj.name, X , CENTER);
}
We first defined the color of the Custom Object to be a shade of
gray (<100, 100, 100> ). Finally, we use the drawtext()
method to actually draw the object’s name on the screen. We pass
the variable objPos and use the obj.name data member to define
the position of the label and the text we want to display. Then by
passing the X and CENTER constants we define the alignment of the
text on the label.
Save and apply this script to a Null object. You should see a label
being drawn at the object’s pivot point, displaying the name of the
mesh object. Also notice how the object reacts to animating the
Null, by moving and keyframing the mesh object’s position.
This is the final code:

@warnings
@version 2.4
@script custom
@name custText

// Global variables.
obj;
currTime = 0;
CHAPTER 13: CUSTOM OBJECTS 97

create: ma
{
obj = ma;
setdesc(“custText”);
}

newtime: frame, time


{
currTime = time;
}

process: ca
{
// Get some information from the Mesh object.
objPos = obj.getPostion(currTime);

// Using the ca Object Agent, create the Custom Object.


ca.setColor(<100,100,100>);
ca.drawText(objPos, obj.name, X , CENTER);
}
Example: Barn.ls (as seen in the LightWave SDK)
For this example, we will make a Custom Object that looks like a
very simple barn. Let’s start by creating the script’s header
information.
@warnings
@version 2.4
@script custom
@name custBarn
Next we must define how to draw our Custom Object. This is
accomplished by declaring two global variables: vert and edge.
Each will have a list of values that our code will use to draw our
object:
vert = @ <0.0, 0.0, 0.0>, <1.0, 0.0, 0.0>,
<1.0, 1.0, 0.0>, <0.5, 1.5, 0.0>,
<0.0, 1.0, 0.0>, <0.0, 0.0, -1.0>,
<1.0, 0.0, -1.0>, <1.0, 1.0, -1.0>,
<0.5, 1.5, -1.0>, <0.0, 1.0, -1.0> @;

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");

for(x = 31; x < 35;x += 2)


ca.drawLine(vert[edge[x]],vert[edge[x+1]]);
}
Again, we control our drawing through a loop, but instead we
draw only 15 of the 17 edges. Next, we set the pattern of the line to
dot, indicating that a dotted line will be drawn instead of the
default solid line. The final two edges are then drawn using a similar
for-loop as the previous 15 edges.
Save and apply your script to a Null object. You should see a
simple 17-edged barn being drawn.
Sample Code:
The following code is a Modeler script you can use to translate
existing LightWave models into ready-made Custom Object scripts.
Although it may take you a while to type in, this script will greatly
simplify the process of creating Custom Object Scripts:
//-----------------------------------------------
// Mesh2Custom by Bob Hood
//
// Convert the mesh in the currently visible
// layer(s) into a Layout Custom Object LScript
//

@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();

// optimize the indices by removing duplicate lines


x = 1;
while(x <= indices.size())
{
if(indices[x])
{
y = x + 2;
while(y <= indices.size())
{
if(indices[y])
{
if((indices[x] == indices[y] && indices[x+1]
== indices[y+1]) ||
(indices[x] == indices[y+1] && indices[x+1]
== indices[y]))
{
indices[y] = nil;
indices[y+1] = nil;
}
}
y += 2;
}
}
x += 2;
}
indices.pack();
indices.trunc();

// generate the Custom script from the array data


file = File(filename,"w") || error("Couldn't open output
file!");
file.writeln("@warnings");
file.writeln("@version 2.3");
file.writeln("@script custom"); file.nl();
file.write("vert = @ ");
for(x = 1;x <= vectors.size();x++)
{
if(x > 1)
{
file.writeln(",");
file.write(" ");
102 V O L U M E 1 : L A Y O U T L S C R I P T

}
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

CHAPTER 14: PROCEDURAL TEXTURING


Procedural textures, or Shaders, appear on the surface of an
object, but do not really exist on the object. LightWave calculates
the procedural textures at the time of rendering so they appear in
the final image.
The LScript Procedural Texture (LS-PT) scripts let you control
how LightWave renders a surface.

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

FRAME TIME INDEX (30FPS)


1/30 or .033
2/30 or .066
3/30 or .1

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

LS-PT Functions and Data Members


The main processing point for LS-PT is the process() function.
For Procedural Textures, the process() function is called on a
pixel-by-pixel basis.
LS-PT provides a single argument to the process() function; the
argument is another Object Agent, called Shader. The Shader
Object Agent contains methods and data members to tell you about
shaders, much like the object agent for Scenes in Layout LScripts.
Shader Object Agents provide all the information we need about
the surfaces we will affect. We can control some of these values and
alter the surface. Others give us information only and cannot be
altered.
Let’s look at the read-only information first.
sx is an integer that represents the spot X location in the final
image as pixel coordinates, where (0,0) is at the upper-left corner.
sy is an integer that represents the spot Y location in the final
image as pixel coordinates, where (0,0) is at the upper-left corner.
CHAPTER 14: PROCEDURAL TEXTURING 105

oPos[3] are floating point values that represent the coordinates


of the spot position in object coordinates. Because this is a
coordinate value, it returns three numbers for X, Y, Z positioning.
wPos[3] are floating point values that represent the coordinates
of the spot position in world coordinates. Because this is a
coordinate value, it returns three numbers for X, Y, Z positioning.
gNorm[3]are floating point values that represent the geometric
normal of the in world coordinates, which is the raw polygonal at
the spot, unperturbed by smoothing or bump mapping. Because
this is a coordinate value, it returns three numbers for X,Y,Z
positioning
spotSize is a floating point value that represents the
approximate spot diameter. The value is approximate because
spots on a surface appear long and thin when viewed on edge. You
can use spotSize to compute texture anti-aliasing.
raycast() is essentially a faster version of raytrace(); it
accepts the same parameters, but returns only the ray length.
Shading is not evaluated, and recursive raytracing does not occur.
raySource[3]are floating point values that represent the origin
of the viewing ray in world coordinates. Often this origin will be the
camera, but it does not have to be. Because this is a coordinate
value, it returns three numbers for X, Y, Z positioning.
rayLength is a floating point value that represents the distance
in free space that the viewing ray travels to reach this spot.
cosine is a number that represents the cosine of the angle
between the viewing ray and the surface normal at this spot. It
indicates how glancing the view is and gives a measure of how
approximate the spot size is.
oXfrm[9] is the object-to-world transformation matrix made up
of floating point values. This matrix can be computed other ways,
but it is included here for speed; it is meant primarily for
directional vectors. Because this is a coordinate value, it returns
three sets of three numbers for X,Y,Z positioning
wXfrm[9] is the world-to-object transformation matrix. This
matrix can be computed other ways, but it is included here for
speed and is meant primarily for directional vectors.
ObjID is a pointer to an Object Agent that represents the object
being shaded.
106 V O L U M E 1 : L A Y O U T L S C R I P T

PolNum is an integer that represents the polygon number of the


object being shaded. While this will be the polygon number for
normal mesh objects, it may represent other sub-object
information in non-mesh objects.
illuminate(light,position) -> array[6]
The illuminate function returns an array of six numbers that
represent the light ray (color[1-3] and direction[4-6]) hitting
the given position from the given light at the current instance. The
return value is zero if the light does not illuminate the world
coordinate position at all. The color includes effects from shadows
(if any), falloff, spotlight cones, and transparent objects between
the light and the point.
raytrace(position,direction) -> array[4]
The raytrace function can trace a ray from a given location in a
given direction (in world coordinates). The function returns an
array of four elements that represent the length of the ray [1] and
the color coming from that direction [2-4]. The ray length will be -
1.0 if it is infinite. The direction used is the outgoing direction and
must be normalized to be a unit vector.
While we can read and use these values, we cannot change them.
It’s no fun to just read values, so LS-PT also lets us alter surface
values to our liking. The values we can alter are listed below. The
names of most of the items have control equivalents in Layout. So,
if you are comfortable surfacing in Layout, you should find the
names here familiar.
wNorm[3] is a new geometric normal of the spot in world
coordinates. Modifying this makes the surface look bumpy without
altering the geometry (bump mapping). The shader needs to re-
normalize the vector after perturbation.
color[3] are numbers that represent the percentage of the Red
[1], Green [2], and Blue [3] values of the surface color (1.0 ==
100%).
luminous is a number that represents the percentage of
luminosity of the surface (1.0 == 100%).
diffuse is a number that represents the percentage of diffusion
of the surface (1.0 == 100%).
specular is a number that represents the percentage of
specularity of the surface (1.0 == 100%).
CHAPTER 14: PROCEDURAL TEXTURING 107

mirror is a number that represents the percentage of mirroring


of the surface (1.0 == 100%).
transparency is a number that represents the percentage of
transparency of the surface (1.0 == 100%).
eta is a number that represents the percentage of the index of
refraction of the surface (1.0 == 100%).
roughness is a number that represents the percentage of
roughness of the surface (1.0 == 100%).
Example: (PULSE2.LS)
Let’s look at a basic example of how we can alter the values of a
surface through an LS-PT script.
This script is called Pulse2.ls. It takes the luminosity and diffusion
of a surface and makes it pulse off and on like a blinker on a car. A
small interface lets the user adjust the length of the pulse and
choose whether to preserve the diffuse channel of the surface.
// Pulse2
//
// Cycle the luminosity and diffusion (inversely)
// of a surface so the surface appears to "pulse"
//
// Updated 04.21.98 Bob Hood
// Updated 07.20.01 Scott Wheeler
The top portion of the script is a comment area where you place
the name of the script and any important information. The ‘//’
characters identify the line as a comment; any text on that line is
ignored when the script runs. These characters are handy if you
want to remove a few lines from a script, but you do not want to
delete them entirely: just comment them out.
@version 2.3
Any command that begins with the ‘@’ character is a compiler
directive. Compiler directives give information to the compiler that
either improves the performance of the script or alerts the
compiler of certain conditions needed for the script to run.
The ‘@version’ directive tells the compiler the minimum version
of LScript needed for the script to run properly. You should place
this directive in any script that uses newer command sets, because
the directive tells the user if they need to upgrade LScript for the
script to function .
108 V O L U M E 1 : L A Y O U T L S C R I P T

Although a compiler directive can be placed anywhere in the


script, it is good practice to place all compiler directives at the top
of the script so they are organized and easy to find.
thisFrame;
pulserange = 2;
prog = "Pulse v1.01";
keepdiffuse = true;
Next, you declare any variables that you want to be global. Global
variables can be seen by all script functions as if they are a part of
that function. This contrasts with local variables, which are
declared inside of a function. Local variables cannot be seen by any
function other than the function in which they were declared.
Looping gives a good example of how local variables work. If you
have a small loop inside a function, the variables used for the loop
do not need to be seen outside of the function because their
purpose does not extend beyond the loop itself.
You should understand the purpose of each global, so let’s look at
each one.
thisFrame holds the current frame number.
pulserange is the default value assigned to the length of each
pulse. As you will see later, we assign it to another value before the
script runs. However, we place it at the beginning of the script so it
can be changed in a centralized location, avoiding the need to hunt
through the script to find it.
prog is a text variable that holds the current version of the
script. It is not an important part of the script but it makes setting
the description (as we will see later) easier.
keepdiffuse is a Boolean variable that lets us keep track of
diffuse values set by the user. Later in the script, we will let the user
retain or override the diffuse value on the surface.
create
{
scene = Scene();
pulserange = scene.fps;
setdesc(prog+" is Pulsing on "+
string(pulserange)+" Frame Intervals");
}
The create() function is called when the script is first loaded,
so it is a good place to do any preliminary setup. The create()
CHAPTER 14: PROCEDURAL TEXTURING 109

function differs from the init() function in that init() is called


at the beginning of each render sequence, and the create()
function is called when the script is loaded or activated by
LightWave.
The first line of the create() function is an important one; a lot
of information is passed to the ‘scene’ variable in very little code.
By using the Scene() function we are creating a Scene Object
Agent. This gives us access to several data members and methods
dealing with the current scene.

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.

When the Scene Object Agent is created, all available information


for the current scene is stored in the variable ‘scene.’ One piece of
information that we can now access is the frames-per-second (fps)
of the scene. So, we can set our pulserange variable by assigning
to it the frames-per-second (fps) data member.
The last line in the create() function is a call to setdesc().
This function lets us create a line in the shaders list that describes
the script’s values. Here we use the ‘prog’ variable we filled with the
name of the script. We will use the setdesc() function several
times to give the user feedback about how the script works,
without making the user open the options panel. The script runs
the first time, and if the scene fps is set to 24 then the description
looks like the following example:

Figure 14-1. Script description in interface

newtime: frame, time


{
thisFrame = frame;
}
110 V O L U M E 1 : L A Y O U T L S C R I P T

The newtime() function is called once for every new frame,


which gives the scripter the chance to get information or make
changes on a frame-by-frame basis. In this case, we want to get the
current frame information and assign it to our global variable
‘thisFrame.’
flags
{
return(LUMINOUS,DIFFUSE);
}
In the flags() function, we tell LScript exactly which surface
attributes we will affect. While your scripts will still run if you do
not add the flags(), it is far more efficient to tell the compiler
which surface attributes you will alter so that it loads only those
buffers into memory.
In our example, we will alter the LUMINOUS and the DIFFUSE
values of our surface; so, we ask only for those surface attributes to
be made available.
The next function is process(); process() is where all of our
script action takes place. In LScript Procedural Textures, the
process() function is called once for every pixel in the image.
Because the process() function is called after the newtime()
function we can use any information we gleaned from the frame
change.
process: sa
{
if(thisFrame == 0)
return;
The process() function gives us access to the Shader Object
Agent, which is the variable ‘sa’ that is passed into the function. So,
every time we want to get information on our surface, or change a
value of the surface, we do it through the ‘sa’ variable. This will
make more sense further down in the function.
Because we want to avoid any nasty divide-by-zero errors, we will
exclude the 0 frame from our processing. This is done by testing for
the current frame, and if the frame equals 0, we call the ‘return’
command and hop out of the process() function.
j = thisFrame;
if(thisFrame > pulserange)
{
CHAPTER 14: PROCEDURAL TEXTURING 111

i = integer(thisFrame / pulserange);
if(i != 0)
{
j = thisFrame - (i * pulserange);
}
k = pulserange / 2;
if(j <= k)
{
// pulsing up
sa.luminous = j / k;

// generate the inverse


if (!keepdiffuse) sa.diffuse = 1.0 - (j / k);
}
else
{
// pulsing down
// fold down so percentages are correct
j = j - k;
sa.luminous = 1.0 - (j / k);
if (!keepdiffuse) sa.diffuse = j / k;
}
}
The above code section may seem complicated at first glance, but
let’s look at it as a whole and then break it down. This code changes
the luminosity and diffuse values of our surface over time by
reading the current frame and the length of the pulse. It then breaks
the process into two sections: the ramp up, which is half the time of
the total pulse, and the ramp down again.
Notice that the diffuse value changes are guarded by a Boolean
variable ‘keepdiffuse.’ This is a user-defined value that controls
whether the surface diffusion is ramped down when the luminosity
is increased or whether it stays the same.
A good way to see how a section of code works is to build a grid
from all the variables involved and see how they change over time.
In the example below, we created a grid of sample frames. This grid
assumes a pulse rate of 20 frames (that would be 10 frames up and
10 frames down) and that we want to alter the diffusion of the
surface as well as the luminosity.
112 V O L U M E 1 : L A Y O U T L S C R I P T

frame pulserange j k sa.luminous sa.diffuse

5 20 j = this- 10 j / k = 0.5 1.0 – (j /k)


Frame = 5 = 0.5

7 20 j = this- 10 j / k = 0.7 1.0 – (j / k)


Frame = 7 = 0.3

16 20 j = (j-k) = 10 1.0 – (j / k) (j / k) = 0.6


6 = 0.4

20 20 j = (j-k)=10 10 1.0 – (j / k) (j / k) = 1.0


= 0.0

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");
}

newtime: frame, time


CHAPTER 14: PROCEDURAL TEXTURING 115

{
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

CHAPTER 15: IMAGE FILTER SCRIPTS


Image Filter scripts give you tremendous post-processing control
over LightWave’s rendered output. The filter script can be as simple
as making the image negative, or as complex as needed to achieve a
desired effect.
The goal of this section is to give you a basic understanding of
how the Image Filter system works. Therefore, we will stay more
toward the simple side of the Image Filter controls. More advanced
scripts are included on the LightWave CD and on NewTek’s Web
site. They will help you look more deeply into the Image Filter
controls than we will delve in this text.
Image Buffers
A rendered image from LightWave has a wealth of data attached
to it, and this data is stored in a series of holding spots known as
buffers. Each of these buffers contains information about separate
elements in the image. For example, you can view which pixels are
luminous by examining the data in the LUMINOUS buffer.
Not all of this data is read-only. That is, for a few of these buffers,
we are not limited to only reading the data. We can change those
specific pixels with code that could act as a post-processing effect.
You could even use that buffered information to save out a separate
image of only those luminous pixels for later manipulation in a
compositing package.
For complete information on the Data Members and Methods
available to the Image Filter Object Agent, Volume 2, Chapter 23 in
the Object Agent Reference section of this manual.
Image Filter Functions
An Image Filter script is set up like most of LScript’s Layout script
types. It contains several predefined functions that the scripter can
use to add functionality to a script. The following is a list of these
functions and the roles they play in an Image Filter script:
create() and destroy()
The create() function is called when the script is first activated.
This can be either when the user first activates the script, or when
it is loaded from a scene. Usually, variable initiation and setup
commands are performed here.
118 V O L U M E 1 : L A Y O U T L S C R I P T

The destroy() function is called when the script is removed


from active processing by the user, or when the scene to which it is
associated is cleared. A scripter will often perform some form of
variable cleanup in this function. As with any function, if no code is
present in either of these functions, there is no need to include
them in your script.
process()
The process() function is the workhorse function of the script,
because LScript automatically calls it after every frame is rendered.
Through the use of LightWave’s internal image buffers (see above),
the scripter can easily access the rendered frame’s image data,
perform pixel-altering code, and either re-institute it back into the
image or save it off for later use. These actions all happen in the
script’s process() function.
The process() function gains access to a frame’s buffer
information through the data members and methods contained in
the Image Filter Object Agent. This Object Agent is automatically
passed to the process() function whenever it is called by LScript
and contains all the information needed by an Image Filter script.
process: ifo
{
}
The ifo argument is the Image Filter Object Agent. Through this
Object Agent, the scripter can gather or set information on one of
the several buffers made available, query several scene-specific
data members (i.e. start/end/current frame), and copy or paste
data to and from a new buffer.

Note: For a complete listing of data members and methods made


available to the Image Filter Object Agent, please see Chapter 23 in
the Object Agent reference section of this manual.

load() and save()


The load() and save() functions let the script save information
for future use inside the scene file itself. You would save
information on option states, user colors, or anything you want
reloaded later.
For example, if you created an Image Filter that creates a slate at
render time. You might want to save the shot name, show name,
CHAPTER 15: IMAGE FILTER SCRIPTS 119

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

Structurally, this function is identical to the save() function we


discussed earlier. load() has the same arguments used in save(),
but instead of using the io Object Agent to store information, we
use io’s read() method to read the data from the scene file.

Note: In both load() and save() functions, the variables used to


assign and gather the data should have global scope.

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

Logically, destroy() comes after create(). You do any house-


cleaning here, after the script is finished processing. We do not
need this functionality in our script, so we can delete the following
lines from our script.
destroy
{
// take care of final clean-up activities here
}
As we said earlier, the process() function passes some
important bits of data about the image we will process in the form
of an Object Agent. What we initially want to look at is the size of
the rendered image, represented by the Image Filter Object Agent’s
width and height data members. We will use these values to control
our looping variables in order to make sure we process the entire
image.
process: ifo
{
// Gather the image’s width and height.
imgWidth = ifo.width;
imgHeight = ifo.height;
}
With this data collected, we can process through all the pixels in
the image. We do this by creating two for-loops; one will control the
x image coordinates and the other will cycle through the y image
coordinates. We’ll start with the y:
imgHeight = ifo.height;

// Cycle through the y coordinate.


for(i = 1;i <= imgHeight; ++i)
{
}

Now we’ll cycle through the x:
for(i = 1;i <= imgHeight; ++i)
{
// Cycle through the x coordinate.
for(j = 1; j <= imgWidth; ++j)
{
}
}
CHAPTER 15: IMAGE FILTER SCRIPTS 123

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;

// Assign the average value into the color channels.


ifo.red[j,i] = avg;
ifo.green[j,i] = avg;
ifo.blue[j,i] = avg;
This turns each Red, Green, and Blue array location, to create a
grayscale value.
So here is the code we currently have:
//————————————————————-
// Black and White
//

@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

// Cycle through the y coordinate.


for(i = 1;i <= imgHeight; ++i)
{
// Cycle through the x coordinate.
for(j = 1; j <= imgWidth; ++j)
{
// Compute the average value.
avg = (ifo.red[j,i] + ifo.green[j,i] +
ifo.blue[j,i]) / 3;

// Assign average value into color channels.


ifo.red[j,i] = avg;
ifo.green[j,i] = avg;
ifo.blue[j,i] = avg;
}
}
}

Adding a Progress Monitor


Although our image-processing example above is very simple, it
still takes a few moments for LScript to compute the new image. As
the processing code gets more complex, it can take the longer to
render out the final image. A user likes know where they are in the
process. We can give this information by invoking a progress
monitor to display a percentage of completion on the render status
window.
We will start by placing this code into the Black and White script
right after we collect the size of the image:
imgHeight = ifo.height;

// Setup the progress monitor.


if(runningUnder() != SCREAMERNET)
moninit(imgHeight);
We are doing two things here. First, we are checking to see if this
image is being rendered in a renderfarm. If so, we really do not need
to use the progress monitor because there is a good chance
nobody will see it anyway. If it is not being rendered in a
renderfarm, we pass the height of the image to the function
moninit().
CHAPTER 15: IMAGE FILTER SCRIPTS 125

By calling the moninit() function, we are setting up the script to


notify the Render Status screen of our progress. By passing the
imgHeight variable to moninit() we are determining what the
maximum count of the monitor will be. This is how the Render
Status window will determine what percentage of the task is
complete.
For example, if our image is 640 x 480, our imgHeight variable
will have the value of 480. By passing imgHeight to the
moninit() function, it will know that the maximum count for the
monitor is 480. So when we are on pass 12 of 480, the Render Status
window will display the correct percentage.
We can increase the current count of the monitor by calling the
monstep() function. Since we are basing the percentage-complete
equation on the height of the image, we will put this function at the
end of the height loop. This will increase the monitor by one, as the
height loop is increasing by one.
ifo.blue[j,i] = avg;
}

// Update the progress monitor.


if(runningUnder() != SCREAMERNET)
if(monstep())
return;
This function not only increases the current count of the monitor,
but it also returns a Boolean value. A value of true indicates that
the “Cancel” button or ESC key was pressed. This gives the scripter
a way to let the user cancel a long process.

Note: The progress monitor is not available to all, and works


differently for some, of the Layout plug-in architectures.
126 V O L U M E 1 : L A Y O U T L S C R I P T
CHAPTER 16: ITEM ANIMATION 127

CHAPTER 16: ITEM ANIMATION


The LScript Item Animation (LS-IA) architecture lets you change
an object’s position, rotation or scaling during an animation
sequence, overriding those calculated by Layout.
process()
As with most other LScripts, the main work area of Item
Animation scripts is the process() function. The process()
function for LS-IA takes three arguments, they are :
ma, the first argument, is an Object Agent called MotionAccess. It
is represented in the script as a user-defined variable. For
consistency between scripts we recommend using ’ma.’ This
object agent gives you access to all the methods and data members
associated with the object to which the script is applied. A full list
of the available method information can be found in the Methods
section of this manual.
frame contains the current frame the animation is on.
time is a time index that is a floating-point number generated for
each frame. It differs from the frame number in that the time index
is dependent on the scene's frames-per-second setting.
Example :
In a scene where the frames-per-second setting is set to 24, you
would 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 the time index would be :
FRAME TIME INDEX (30FPS)
1/30 or .033
2/30 or .066
3/30 or .1
LS-IA Methods
In addition to the general methods available to the MotionAccess
Object Agent, there two specific methods built into the Item
Animation architecture. These two methods allow us to read (or
128 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;

Note: ROTATION returns values in degrees. POSITION returns the


location in meters and SCALING returns values as a percentage,
where 1.0 is 100% (or no scaling).

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.

The following items are the global variables we will use:


rotspermeter is a variable that will hold the number of
rotations we want our object to perform in one meter of movement.
controlaxis is the axis upon which we would like the object to
rotate, either Heading, Pitch or Bank. It is an integer value due to
that fact that we will ask the user to choose an axis in the script
interface through the ctrlchoice() command. The
ctrlchoice() function returns an integer value representing the
choice made by the user.

Note: More information on ctrlchoice() is in the Interface section


of this manual.

controlpos is similar to controlaxis, controlpos is an


integer value specifying the positional axis of movement the
rotation should be based on, X, Y or Z.
It is worth mentioning that the variables are named as closely as
possible to the functions that they will perform. While this feature
is not as important in a script with only three or so variables, it
becomes invaluable when you work with scripts that have far more
variables. It also helps make your scripts readable to other people
who may update them in the future.
create
{
setdesc("Axis Rotation Control");
}
The next section of code is run when the script is created. That is
why the procedure name is cleverly called ‘create.’
CHAPTER 16: ITEM ANIMATION 131

The setdesc() command allows us to display the name of the


script in the Motion options panel. This is not a necessary step, but
it is one of those extra items that help make your scripts look more
polished.
Without using setdesc(), the Motion Options panel would show
the name of the script and identify it as an LScript.

Figure 16-1. Default description in interface.

The setdesc() command lets us place a cleaner looking name in


the window instead.
132 V O L U M E 1 : L A Y O U T L S C R I P T

Figure 16-2. Description with setdesc() command

Admittedly, this is not a huge deal in the grand scheme of script


writing, but it does help to give your scripts an air of legitimacy and
professionalism.
There are other things that can be done in the create()
function, such as assigning default values for variables and other
general housekeeping chores that you want preformed once when
the script is added.
process: ma, frame, time
{
rot = ma.get(ROTATION,time);
roth = rot.h;
rotp = rot.p;
rotb = rot.b;
pos = ma.get(POSITION,time);
As with most scripts, the process() function is where the bulk
of the work is performed. In the case of Item Animation, the
process() function is called for each new time that Layout moves
through. This can either be during render, or during preview and
playback.
In the case of our script, we first want to find out what the object
is doing at the current time. The variable rot is assigned the
CHAPTER 16: ITEM ANIMATION 133

rotation information (as a vector) of the object. Then the local


variables roth, rotp, and rotb, are each assigned their portion of
the rot vector. The variable pos then queries the Motion Access
Object Agent to find out the position information for our object.
We use the get() method to retrieve the desired information.
Because we want to be able to affect an individual channel of
rotation, we have divided the ROTATION into its three components,
Heading (roth), Pitch (rotp) and Bank (rotb).
As we will see later in the script, separating out the ROTATION
channels also lets us preserve the values of the other two channels.
This way we can blend key-framed motion on the two unaffected
channels with the constrained channel of the script.
if (controlpos == 1) rotamount = pos.x*(rotspermeter*360);
else
if (controlpos == 2) rotamount = pos.y*(rotspermeter*360);
else
rotamount = pos.z*(rotspermeter*360);
Now we can use the POSITION information we received to alter
the rotation of the object. But, first we need to assign a rotational
amount based on where the object is located. We do this with the
following formula :
Rotational Amount = Object Positional Axis Location *
(Rotations Per Meter User Input * 360)
Because we will ask the user which Axis they want to use for
movement, we must be prepared to compute the formula based on
their choice. The above if() statement controls which Axis is
used: 1 for X, 2 for Y and 3 for Z.
Also note that the variable rotspermeter was multiplied by 360.
This allows the user input of “Rotations Per Meter” to be converted
to degrees of rotation.
if (controlaxis == 1) roth = rotamount;
else
if (controlaxis == 2) rotp = rotamount;
else
rotb = rotamount;
Similar to the if() statement for the positional axis, this if()
statement determines which rotational axis will get the assigned
rotational value.
134 V O L U M E 1 : L A Y O U T L S C R I P T

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

Figure 16-3. Interface generated with options()

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

That’s it for Item Animation. Because it is sometimes hard to see


the big picture in the small chunks listed above, the complete
script is presented below.
ROTATER.LS

///-----------------------------------------
// 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");
}

process: ma, frame, time


{
roth = ma.get(ROTATION,time).h;
rotp = ma.get(ROTATION,time).p;
rotb = ma.get(ROTATION,time).b;
pos = ma.get(POSITION,time);

if (controlpos == 1) rotamount =
pos.x*(rotspermeter*360);
else
if (controlpos == 2) rotamount =
pos.y*(rotspermeter*360);
else
rotamount = pos.z*(rotspermeter*360);

if (controlaxis == 1) roth = rotamount;


else
if (controlaxis == 2) rotp = rotamount;
CHAPTER 16: ITEM ANIMATION 137

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

CHAPTER 17: CHANNEL FILTERS


Channel Filter scripts allow the user to apply animation modifiers
to the individual channels of an item’s motion. These scripts have
both read and write access to a channel’s values. They can monitor
and react to adjustments made within a channel.
A typical Channel Filter script will read a value from a Channel
Object Agent, perform tests and calculations on the data gathered,
and finally, write altered values back into the channel.
Like most of the Layout script architectures found in LScript,
Channel Filters use a number of predefined functions to utilize the
options available to this script class. While some of these functions
are necessary in order for your script to work correctly, others
merely offer optional functionality.
create() and destroy()
The create()and destroy() functions handle all variable
initialization and cleanup procedures for your script. For the
Channel Filter script type, the create() function can receive a
Channel Object Agent created from the channel to which the script
is applied.
process()
The process() function is where most of your important
channel-altering code belongs. This function receives three
arguments when it is called by Layout: ca, frame, and time.
process() is called by Layout every time a frame’s data is either
altered or evaluated.
The Channel Access (ca) Object Agent contains a single data
member and two methods we can use to identify and edit values
stored in a Channel. Like other architecture-specific Object Agents,
the Channel Access agent cannot be created outside of the
process() function.
The Channel Access’s name data member stores the current name
of the channel. For example, if you applied a Channel Filter script to
an item’s X channel, the name data member will have a string value
of “Position.x”.
The get() and set() methods give the Channel Access Object
Agent the ability to retrieve and edit number values stored in an
item’s channel. The get() function accepts a time argument,
while set() simply works on the current frame of the channel.
140 V O L U M E 1 : L A Y O U T L S C R I P T

The second (frame) and third (time) parameters to the


process() function indicate the time index for which the channel
is being evaluted. Keep in mind that these values may or may not
coincide with the scene’s current time index as it appears in the
user interface.
load() and save()
The load() and save() functions allow Layout to load and
save specific data pertaining to the operation of this script. Any
selected options or settings that could determine how the script
should run, must be saved within the scene file. This will ensure
that when the scene is saved or loaded, the item’s channel
information, as it pertains to your script, is not discarded.
When Layout loads a scene and comes across an entry for your
script, it calls the load() function. In this function, you should
read and assign settings and values that will enable the script to
perform as expected. Likewise, the save() function gets called
whenever the user saves a scene. This function should contain all
the code necessary to organize and save the script’s data to a scene
file.
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 set up and run the script’s
interface.

The Channel Object Agent


Both the create() and event callback UDF functions get passed
the channel argument. This Object Agent contains several data
members and methods you can use to retrieve or set keyframe
information, create or delete keyframes, or even query a keyframe’s
spline information.

Note: This Object Agent can also be created by calling an item’s


firstChannel() method.

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

Here we tested the Channel Object Agent’s name data member. If


you apply this script to an item’s x channel, you should get an
info() requester displayed, informing you which channel was
used.

Note: For a complete listing of data members and methods that


belong to the Channel Object Agent, see page 227 in Volume 2,
Chapter 26 in the Object Agent Reference section of this manual.

Let’s move on to the next, and probably most important function


in this type of script, the process() function. We will put all of our
frame-specific code in this function because it gets called anytime a
frame’s motion data changes. Therefore, when the channel
information changes, our script can react to the new changes. First,
let’s see what is available to us:
process: ca, frame, time
{
}
As you can see, the process() function gets passed three
arguments. The first argument, ca, is the channel’s Channel Access
Agent. This exports a single data member (name) and two
methods( set() and get() ) we can use to interact with the
values in the item’s channel. The second and third arguments,
frame and time, are simply values indicating where in the scene
the user is currently editing. Let’s take a look:
process: ca, frame, time
{
info(frame);
}
This little test will do two things. First, it will put LightWave into
an infinite loop, so save your work before you attempt to run it.
Second, it will show you exactly how often the process() function
gets called. You should notice that when the script is run, the
function is constantly and repeatedly being called on every frame.
In fact, you should be able to press the “OK” button, but nothing
else. You will have to kill Layout to stop the script. Be sure to
remove this line from your code after you see how it works.
You can continue to test the other arguments passed to the
process() function by simply altering what is sent to the info()
function. Each passed argument will display different results, but
how often the process() function gets called will not change. It is
CHAPTER 17: CHANNEL FILTERS 143

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”);

// Set the plug-in list’s description.


setdesc(“maxValue”);
}
The event() method we are using accepts the name of a User
Defined Function (UDF) as a parameter. In this case, when the
script’s channel is altered, the newEvent() function within the
script is called. Before we test this, we’ll have to define the new
UDF:
newEvent: channel, event
{
info(event);
}
Now, every time the channel is altered, the newEvent() function
is called, and an info() requester displays what actually
happened. Take a moment and experiment with editing this
channel so you can get an idea of what (and when) the events are
being generated. Also notice that the newEvent() function not
only gets the event passed to it, but it also gets the Channel Object
Agent as well.
The script we are making in this example will not use the
channel’s event() callback. When you finish experimenting with
the event parameters, feel free to remove the newEvent() function
entirely, as well as the call to the event() method in the
create() function.
144 V O L U M E 1 : L A Y O U T L S C R I P T

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);

// Compare the values.


if(currValue >= maxValue)
ca.set(maxValue);
}
We first compare to see if the channel’s current value is greater-
than or equal-to the value we stored in the maxValue variable. If it
CHAPTER 17: CHANNEL FILTERS 145

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");
}

process: ca, frame, time


{
// Get the value from the Channel Access Object Agent.
value = ca.get(time);

// Compare the values.


if(value >= maxValue)
ca.set(maxValue);
}
Example 2: dynLimiter
146 V O L U M E 1 : L A Y O U T L S C R I P T

For this script, we will make a dynamic version of the maxValue


script we just made. We will start by using the same example code
that we wrote above. However, rather than setting a maximum
value for the channel in a variable, we will use another object’s
channel value as the maximum value. This will allow you to animate
the maximum value over time.
We will start by replacing the previous global variable maxValue
with objName to represent the name of the object we will use as a
reference. For now, we will use the string value “Null.” This value
could also be retrieved from an interface.
//Global variables.
objName = “Null”;

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);

// Get the value from the


// Channel Access Object Agent.
Now that we have created the Object Agent, we can get to the
values stored in its various channels. Before we continue, we
should check to see if an Object Agent did, in fact, get created.
Once we know this, we can create a Channel Object agent from the
first channel that exists in the reference object.
if(obj)
{
// Get the object's first channel.
chan = obj.firstChannel();
}
else
error(“No Mesh Object Agent created.);
The firstChannel() method simply returns the first channel in
the Mesh Object Agent. For our current example, we care about
only one of the channels: Position.X. In this case, we got lucky;
the first channel in the reference object happens to be
CHAPTER 17: CHANNEL FILTERS 147

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();

// Compare the names of the channels


// ca and chan.
while(chan && chan.name != ca.name)
chan = obj.nextChannel();
The while() statement allows us to repeatedly create a Channel
Object Agent from the next channel in the object. How does it know
which channel to start with? We used the firstChannel()
method to create the initial chan Channel Object Agent. Each
subsequent call to the Mesh Object Agent’s nextChannel()
method will return the next channel in the object. For example, if
the value of the first channel is Position.X, the value from the
next channel will be Position.Y, and so on.
Only when a match is found in the names of both channels being
evaluated will the while() statement allow the script to continue
processing. This will ensure that we compare the same channels
within the two objects. Just in case, let’s check that the chan
Channel Object Agent was created:
if(chan)
{
}
We now have most of the information we need for this script to
work properly. We have the name of the reference object
(objName) and the Object Agents created from the script’s
channel, the value of the ca Object Agent, and the reference object.
Now all we have to do is retrieve the value from the reference
object’s channel.
We have already written the code that compares and adjusts the
channel’s values back in the previous example. Now, we will
arrange everything in the if() statement we started above:
148 V O L U M E 1 : L A Y O U T L S C R I P T

if(chan)
{
// Get the value from the reference
// object’s Channel Object Agent.
maxValue = chan.value(time);

// Get the value from the Channel Access


// (this channel) Object Agent.
value = ca.get(time);

// Compare the values.


if(value >= maxValue)
ca.set(maxValue);
}
We just did a lot, but it is all very straightforward. First, we got the
value of the reference object’s channel by using the value()
method. Second, we got the value stored in the ca Channel Access
Object Agent. We then compared these two values, and if ca’s value
exceeded the maxValue, we will simply assign the maxValue to ca
using the set() method.
To test this script:
1 Be sure to load the Cow object.
2 Add a Null object named “Null.”
3 Apply this script to the Cow object.
4 Keyframe the Null somewhere along the X axis.
5 Attempt to animate the Cow object past the reference Null.
6 Keyframe it, and change the current frame. You should notice that
the Cow object will snap to the X position of the reference Null.
Final script:
@warnings
@script channel
@version 2.5
@name dynLimiter

//Global variables.
ObjName = “Null”;
CHAPTER 17: CHANNEL FILTERS 149

create: channel
{
// Set the plug-in list’s description.
setdesc(“maxValue”);
}

process: ca, frame, time


{
// Create an Object agent
obj = Mesh(objName);

// Get the value from the Channel


// Access Object Agent.
if(obj)
{
// Get the object's first channel.
chan = obj.firstChannel();

// Compare the names of the channels


// the ca and chan Object Agents.
while(chan && chan.name != ca.name)
chan = obj.nextChannel();
if(chan)
{
// Get the value from the reference
// object’s Channel Object Agent.
maxValue = chan.value(time);

// Get the value from the Channel Access


// (this channel) Object Agent.
value = ca.get(time);

// Compare the values.


if(value >= maxValue)
ca.set(maxValue);
}
}
else
error(“No Mesh Object Agent created.);
}
150 V O L U M E 1 : L A Y O U T L S C R I P T
CHAPTER 18: GENERIC SCRIPTS 151

CHAPTER 18: GENERIC SCRIPTS


The most open-ended of all script types is LScript Generic (LS-
GN). Most types of scripts are tied to a certain aspect of the render
process, but Generic LScripts stand outside everything. These
scripts can access many of the diverse aspects within Layout.
Generic scripts function like Modeler LScripts. These scripts are
“run-once” operations that complete their entire assigned task at
the time they are called. Because they are similar to a stand-alone
program run from inside the LightWave environment, the
scriptwriter can freely perform functions in Generic LScripts that
are not available to other Layout LScript types.
Generic Script Construction
Given that Generic Scripts are “run-once” events, you cannot call
certain functions at certain times in the life of the plug-in.
Therefore, the create(), destroy() or init() functions are
not available. Generic scripts are not tied to any scene or object, so
the save() and load() functions are also not available as they are
in other script types.
Layout’s Generic scripts have a single point of entry with the
generic() function. The script begins with the call to this
function and ends when that function’s work is done.
To further simplify matters, the options()function is not
available to the Generic script. This is not to say that Generics can
not have an interface—all interface controls are placed in the
generic() function. When script processing reaches this code,
the requester is displayed.

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

The Scene Object Agent, housed in the scene variable, contains a


number of data members. One data member is filename. The
filename data member holds the complete path and name
information for the current scene. This is everything LightWave
needs to retrieve and save the scene to disk.
Example:
X:\Scenes\MyProjects\ThisScene.lws
We pull information out of the Scene Object Agent ‘scene’ by
asking for a specific data member element by name: you add the
member name to the end of the Object Agent container (in this
case, scene).
Example:
scene.filename // returns the filename of the Scene
// Object ‘scene’.

scene.polycount // returns the total polygon count within


// the Scene Object ‘scene’.
If the 'scene' variable contained a Mesh Object Agent, accessing
the polycount data member would return the number of polygons
in that object.
generic
{
scene = Scene();
if (scene.filename == "(unnamed)")
SaveSceneAs();
SaveSceneAs(setupscene(scene.filename));
The first line of the generic() function checks to see if a name is
actually associated with the current LightWave scene. Note that we
access information from the Scene Object Agent directly inside the
if() statement. This approach can condense the number of lines
needed in your code, but it can make your script harder to
understand.
Now let’s look at what the if() statement actually does.
LightWave automatically names an unsaved scene “(unnamed).”
So, we just check whether or not the name is set to “(unnamed)”
and perform actions accordingly.
When a scene has never been saved, we make a call to the
Command Sequence command SaveSceneAs(). Command
154 V O L U M E 1 : L A Y O U T L S C R I P T

Sequence commands are direct links to internal functions of


LightWave. They give the script a great deal of control over both
the interface of LightWave and internal functions. However, these
Command Sequence commands are only accessible to Generic and
Master class scripts. You will find a complete listing of the
Command Sequence commands in the back of this manual.
SaveSceneAs() wants to be told the name of the scene to be
saved. Because we don’t have one yet, we make a call and leave the
name empty. This forces LightWave to open a ‘Save As’ file
requestor for the user. The scene is then processed as if it had a
filename all along.
The information we pass to the SaveSceneAs() command this
time is the result of a function we created to alter the scene name.
This User Defined Function (UDF) is called setupscene(), and it
returns the altered scene name to SaveSceneAs(). By nesting the
call to setupscene() in SaveSceneAs(), we again cut down the
number of lines of code.
The separate function that alters the scene name also lets us set
apart the code for better readability. A UDF allows us to place areas
of shared functionality in one section. Then, altering the code that
processes the scene name becomes much easier because all
changes happen in the setupscene() function. We will not have
to look through the main code to find different sections.
It may seem silly to go to this extreme in a script of this small size,
but it is good practice to find ways to make code more readable in
all situations. After all, you may not be the one who makes future
changes, or you may return to a script after a period of time; you
will be happy you made the code readable.
Another good reason to make your own functions is that it allows
you to localize code that you will use over and over. Instead of
repeating the code, you can make one function and call that
function multiple times in a script.
setupscene: sceneName
{
}
A UDF can take a series of parameters that it will use during
processing. In this case, we will pass it one parameter called
sceneName. As we have seen in the generic() function, we pass
the contents of scene.filename to setupscene(). This value is
then inserted into the sceneName variable for local use.
CHAPTER 18: GENERIC SCRIPTS 155

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

In the return() statement the cropped scene name, scenename,


receives the new version number, and we reapply the “.lws”
extension.
After you save out the script and load it into Layout, you can
assign it to a keystroke and save versions of your scenes with the
click of a key.
INCREMENTSCENE.LS
//-----------------------------------------
// Incremental Scene Saver v1.0
//
// Scott Wheeler
// 08-03-01

@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

if (strsub(sceneName,cropsize - 4,1) == "_")


{
ver = integer(strsub(sceneName,cropsize - 2,3)) + 1;
sceneName = strsub(sceneName,1,(size(sceneName) - 5));
}
else
ver = 1;
pad = "_v" + ver.asStr(3,true);
return(sceneName + pad + ".lws");
}
158 V O L U M E 1 : L A Y O U T L S C R I P T
CHAPTER 19: MASTER CLASS 159

CHAPTER 19: MASTER CLASS


LScript’s Master Class (MC) scripts offer an entirely different level
of functionality to a scripter. These scripts can execute Layout’s
CommandSequence (CS) commands (as done in Generic scripts),
and they also have the unique ability to react to actions made by
the user. Actions like object selection, frame changes, and setting
adjustments, are all translated into a CS command whenever the
user performs an action. MC scripts simply monitor when these
commands happen and can be coded to react when certain
conditions are met.
While the user is animating, editing settings, or rendering out a
scene, an active Master script will run continuously in the
background, reporting and reacting. It is this functionality that
makes MC scripts so powerful to the scripter. By giving the scripter
the ability to write tools that assist animators when they are
animating in Layout, we can enhance Layout’s user interface. From
selection tools to macro recorders, the possibilities of MC scripts
are nearly endless.
Although this added functionality is incredibly powerful, your
scripts must be capable of running all the time. A good Master
script will handle numerous conditions that the user could put it
through while animating. More importantly, because these scripts
are running all the time, they must execute their code quickly and
efficiently, making sure to not slow down the interactive feel of
Layout’s user interface. Overlooking this aspect of coding would
greatly defeat the purpose of making useful MC scripts.
Supported Functions
Besides the typical functions found in a Layout script such as
create(), destroy(), save(), load() and options(), the
following functions are also available to a MC script.
flags()
The flags()function is used by Layout to set the options the
MC script will run. It is automatically called by LScript whenever
the script is initiated. Once the flags()function there is no need
to re-inform LScript of the script’s settings, therefore it should not
be called again within a script.
SCENE instructs Layout that this script will be cleared along with
the scene. This is also the default setting if the flags() function is
not defined.
160 V O L U M E 1 : L A Y O U T L S C R I P T

LAYOUT allows the MC script to remain loaded and activated


when a scene is either cleared or a new scene is loaded.
Example:
flags
{
return(SCENE);
}
process()
The process() function in a Master Class script is automatically
called every time Layout issues a new command. When this
function is called, it receives two arguments from Layout that will
help your script determine exactly what the user did in the scene:
The event argument returns the kind of event that was sent to
the process() function. Currently, this argument can return either
the NOTHING, COMMAND, TIME, SELECT, or RENDER_DONE
constant values.
The command string contains not only the command that Layout
most recently issued, but the arguments that pertain to that
command as well. The returned string uses a space to separate the
different pieces of data.
Example:
process: event, command
{
info(command);
}
If an item is selected, the process() function’s info()requester
will appear stating the following:
“SelectItem 10000000”
Example 1: masterTest.ls
Because a Master Class script functions differently from functions
in prior script types, our first script will try to demonstrate only
some of their new features. Though the script may be simple
initially, it will serve two purposes. First, we will get a chance to see
how a Master script’s structure differs from that of the other script
types. Second, our completed script will act as a working tool that
we will use in the second example to help us better understand
commands.
CHAPTER 19: MASTER CLASS 161

Our masterTest.ls script will act similarly to Layout’s


Command History window. Any action that triggers a Layout
command will be caught by our script and displayed with an
info() requester. When we say every action will be displayed, we
mean every action. Therefore, when we decide to run the script,
practically everything we do will trigger an info() requester.
Needless to say it will not be a very practical script, but it will serve
its purpose.
Let’s start by adding a header for a Master Class script.
// masterTest.ls: A script to demonstrate commands
// sent by Layout to Master Class scripts.

@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

interactivity to a crawl. To demonstrate this, replace the info()


command with this piece of code:

for(i = 0; i < 100000; i++)
{
}

This piece of code will put LScript into a timed loop. Granted,
because no commands or functions are bound to the for-loop
statement; it does nothing but take up time. However, by taking up
time, the loop will also prohibit Layout from doing anything else.
This will cause a momentary and noticeable pause whenever
Layout calls the process() function.

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);
}

process: event, command


{
// Display the values of the two passed variables
info(event, " ", command);
164 V O L U M E 1 : L A Y O U T L S C R I P T

}
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

process: event, command


{
// This is where we’ll examine the commands Layout
// sends us
}
You will notice that this looks a lot like the script we wrote in the
previous example, with the info() requester removed. Take a
moment and run the script, nothing should happen of course, but
no news is good news at this point.
Next, we want to set up a way to examine what the user currently
has selected. We can get at these scene settings, such as item
selection, by creating a Scene Object Agent. We need to do this only
once, and because it should be done fairly early in the script, we
will add it to the create() function. This function will run
automatically as soon as the script is called.
create
{
// Create a scene object.
sceneObj = Scene();
}
Because we have declared the sceneObj Object Agent locally in
the create() function, no other function in the script will have
access to its data. We must declare this variable globally instead.
To do this, we insert a declaration between the @name pragma and
the create() function.

@name selected

// Declare global variables here


sceneObj;

create

Now that this variable is declared globally, all the functions in the
script can share the Object Agent’s methods and data members.

Note: For a complete list of Methods and Data Methods in a Scene


Object Agent, please see page 151 in Volume 2, Chapter 11 : Scene
Object Agents.
166 V O L U M E 1 : L A Y O U T L S C R I P T

With the sceneObj Object Agent created, we can use the


getselected() method to retrieve an array of Object Agents
(Mesh, Light, Camera) representing all the items that the user
currently has selected. In the process() function, add the
following lines of code:

process: event, command
{
// Get the currently selected objects.
If(event == COMMAND)
{
selItems = sceneObj.getSelect();

We first want to make sure that the event argument returned the
COMMAND constant. This will weed out all the other types of events
that we do not need to monitor. With this array created, we can
cycle through the different Object Agents, and access their
Methods and Data Members by using a for-loop.

{
selItems = sceneObj.getSelect();
for(i = 1; i <= selItems.size(); i++)
info(selItems[i].name);
This for-loop statement simply counts from the first index (1)
to the size of the selItems[] array. An array’s size is determined
by how many items it has stored. If you have three items selected,
three items are stored in the selItems[] array. Thus, the size of
the array will be three. Setting up this statement will let us use the
for-loop to cycle through every index in the array, giving us access
to its Methods or Data Members.
Right now, for simplicity, we have only an info() function bound
to the for-loop. This line of code is temporary—it lets us see what
kind of data we are getting from Layout. We can then determine
whether or not we are on the right track. In the beginning of your
scripting career, it is important to set up these test situations so
you do not get too far without knowing if your script is working as
planned. It is much easier to back up and fix one erroneous line of
code than it is to debug several lines, or an entire script.
Here is the code we have so far:
CHAPTER 19: MASTER CLASS 167

// selected.ls – This Master Class script


// lists the currently selected items.

@version 2.2
@warnings
@script master
@name selected

// Declare global variables here


sceneObj;

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);
}

process: event, command


{
// Get the currently selected objects
selItems = sceneObj.getSelect();

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

Figure 19-1. The Master Plug-ins panel.

1 Select the Camera item. An info() requester should appear display-


ing the camera’s name.
2 Press the OK button to close the panel.
3 Select the Light item. The same requester with a different item
name should be displayed.
These steps have now tested whether our script works when a
single item is selected. The script appears to be working as we
planned, but we are not done. Let’s test how our script works with
multiple items selected. The easiest way to do this is to load a few
objects into your current scene.

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

that the for-loop selection of our script is working properly. It is


poorly designed, but it works.
We have one major problem with our script. To demonstrate the
problem, simply change from frame 0 to frame 1 of the current
scene. Notice how the sequence of requesters shows up all over
again. That’s because, as we mentioned earlier, everything we do
will trigger the process() function. Although we are going to live
with the multiple info() requesters for a little while, it is
unacceptable to expect the user to put up with this problem.

Determining the CS command


We need to determine when it is actually necessary to run the
code that displays the selected items. As we just demonstrated, we
do not want that code to run every time the process() function is
called. We want the display code to run only when the information
is valuable to the user. In this case, the code should run when an
object is selected. So how do we go about selectively choosing
when to run this code?
We mentioned earlier that one of the arguments sent to the
process() function contains values stored in the command
variable. From the masterTest.ls example, we learned that when
the process() function is called, the command argument will
contain a string. This string represents both the command issued
from Layout as well as its arguments. By analyzing this string, we
can derive which command was issued, and also determine if our
code should pay attention to it.
We can use the masterTest.ls script from the previous
example to determine which of the hundreds of possible
commands are the ones we want our code to react to. Run the
masterTest.ls script and select the Camera item. You should get
an info() requester with text that resembles the following string:
“SelectItem 300000000”
The command used here is SelectItem, and its argument is
300000000. The argument is not important to us right now, but
this value represents the internal number LightWave uses for its
identification system. We will not need to use this argument for
what we want to do in this script. We need only the command:
SelectItem.
So now that we know what command to look out for, let’s have
LScript process our for-loop code only when it comes across this
170 V O L U M E 1 : L A Y O U T L S C R I P T

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

word = parse(“ “, “This is a test.”);


The separator string is a space (“ “) and the string to break
apart is “This is a test.”). This will result in the word array
having the following values:
word[1] = “This”
word[2] = “is”
word[3] = “a”
word[4] = “test.”
Enter the following lines before the if-then statement located in
the process() function:

selItems = sceneObj.getSelect();

//Parse the command variable.


command = parse(“ “, command);
if(event == COMMAND)
{
if(command == "SelectItem")

These lines take the command variable, sent to us from Layout,
and send it through the parse() function. The array returned from
the parse() function is then stored back in the command variable.
Because we used the space (“ “) character to break up the
command variable, we will generate an array with the first index
representing the command and the second index representing the
argument. In our previous test where the command string was:
“SelectItem 300000000”
The command variable would have the following values:
command[1] = “SelectItem”
command[2] = “30000000”
Now by comparing the values stored in command[1] and the
string “SelectItem”, we should generate plenty of info()
requesters. Now we need to change the if-then statement to
compare the first index of the command[] array. The new code
should now read:
//Parse the command variable
command = parse(“ “, command);

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

Note: We broke up what would normally be a single line into three so


we can easily read it. Both ways will work, so how to implement it is
up to you.

In keeping with our fast and efficient theme, we notice on closer


examination that we do not need to create the selitems[] array
every time the process() function is called. Once again, we need
to create this array only when the selection set changes. Therefore,
we should move the two lines of code that create the selItems[]
array into the if-then block.
So here is the working code so far:
// selected.ls – This Master Class script
// lists the currently selected items

@version 2.2
@warnings
@script master
@name selected

// Declare global variables here


sceneObj;

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);
}

process: event, command


{
//Parse the command variable
command = parse(" ", command);
if(event == COMMAND)
{
if( command[1] == "SelectItem" or
174 V O L U M E 1 : L A Y O U T L S C R I P T

command[1] == "AddToSelection" or
command[1] == "RemoveFromSelection")
{
// Get the currently selected objects
selItems = sceneObj.getSelect();

// Display the contents of the items[] array


for(i = 1; i <= selItems.size(); i++)
info(selItems[i].name);
}
}
}

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.



Building an array with the (+=) operator has one major drawback
in this case. Because we are using the same array every time this
function is called, and the array’s values are preserved between
calls, the items[] array exponentially becomes corrupted with
duplicate names. To demonstrate this problem, try running the
script as it currently stands.
To fix this problem, you simply destroy the array by giving the
value of nil to the variable between uses. Then every time the for-
loop stores the item’s names in the variable, it always starts at the
first index.

selItems = sceneObj.getSelect();

// Clear the items variable


items = nil;

// Create the smaller, more efficient items[] array



With that problem solved, we can go back to making our script
more efficient. To truly free up the memory used in the
selItems[]array, we need to destroy the variable. We can do this
the same way we destroyed the items[] array. Simply assign the
value nil to the seItems[]variable.

for(i = 1; i <= selItems.size(); i++)
items += selItems[i].name;

// Free up the memory used by the selItems array


selItems = nil;

// Display the contents of the items[] array



And now there is only one thing left to change, and that is the
array name used in the second for-loop. It should now be set to
cycle through the items[] array instead of the selItems[] array.
176 V O L U M E 1 : L A Y O U T L S C R I P T


// 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();

// Clear the items variable


items = nil;

// Create the smaller, more efficient items[] array


for(i = 1; i <= selItems.size(); i++)
items += selItems[i].name;

// Free up the memory used by the selItems array


selItems = nil;

// Display the contents of the items[] array


for(i = 1; i <= items.size(); i++)
info(items[i]);
}
}
}

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

names at a single time. Unfortunately, the only way to do this is to


create an interface.

Note: Creating an interface can be quite an endeavor. We devoted an


entire section of this manual to the ins and outs of designing and
implementing a user interface. For this reason, we will not go too
deeply into theory or discussion on this subject.

Basically, we will create a requester and use a list box control to


display the contents of the items[] array. This control gives the
scripter a way to display multiple items at once, in one scrollable
list. However, as much as this will help us with our problem, the list
box control is one of the more complex interface components to set
up. It requires the scripter to write several support functions in
order to work correctly.
First we need to make the item[] array global. This setup lets all
the functions, mainly the options() function that handles the
interface code, to have access to its contents. To make the
items[] array global, simply add it before the sceneObj variable,
in the beginning of the script:

@name selected

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:");

// Make the requester 130x300


reqsize(130, 300);

// Create the ListBox control


c0 = ctllistbox("Selected Items:", 100, 290,
"c0_count", "c0_name");

// Make the requester non-modal


reqopen();
}
Then, add the necessary functions for the list box control to work
correctly.
c0_count
{
// This returns the size of the variable
return(items.size());
}
and
c0_name: index
{
// This returns the value of the indexed array
return(items[index]);
}
Final code
Alright, that is it. The script is now complete. Here is the final
script with the added interface code, some additional features, and
some structure changes to finish off some of the bits and pieces:
// selected.ls – This Master Class script lists the
// currently selected items.

@version 2.2
@warnings
@script master
@name selected

items, sceneObj;
CHAPTER 19: MASTER CLASS 179

create
{
// Create the Scene Object Agent
sceneObj = Scene();

// Call the getSelected() UDF


getSelected();

// Set the description to be displayed in the


// Plug-in list.
setdesc("Selected Items: ", items.size(), " item(s)");
}

flags
{
// This function contains the code necessary to tell
// Layout to clear the script if the scene is cleared
return(SCENE);
}

process: event, command


{
// Parse out the command string
command = parse(" ", command);

// Determine which commands will trigger a refresh


if( command[1] == "SelectItem" or
command[1] == "AddToSelection" or
command[1] == "RemoveFromSelection")
{
// Call the getSelected UDF
getSelected();
if(reqisopen())
requpdate();
}
}

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:");

// Make the requester 130x300


reqsize(130, 300);

// Create the ListBox control


c0 = ctllistbox("Selected Items:", 100, 290,
"c0_count", "c0_name");

// Make the requester non-modal


reqopen();
}
}

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

// Destroy the items[] array.


items = nil;

// Get the currently selected objects


selItems = sceneObj.getSelect();
CHAPTER 19: MASTER CLASS 181

// Populate the item[] array with the names of the items


for(i = 1; i <= selItems.size(); i++)
items += selItems[i].name;
}
182 V O L U M E 1 : L A Y O U T L S C R I P T
CHAPTER 20: INTERFACE INTRODUCTION 183

CHAPTER 20: INTERFACE INTRODUCTION


Throughout this manual we have demonstrated that LScript can
offer incredible power to the LightWave artist. At this point we can
now write a variety of scripts that can alter points and polygons,
adjust surfaces, makes shaders, and even ones that help us
animate. Although you may think that you are ready to properly
serve the Lightwave community, your scripting abilities are about
to be catapulted to a new level of usefulness.
In this section, we will cover the ins and outs of writing interface
code. As in the previous chapters, we will start by discussing some
simple examples to introduce us to new commands and functions
unique to interface code. We will not discuss the entire list of
interface commands, but by learning a small subset of commands,
you will get a general idea of how the rest will work. By the end of
this section, you should be ready to tackle a fairly advanced User
Interface (UI) or at least know how one works.
What is an Interface?
We all know what an interface looks like. We use them every time
we turn on a computer or run an application like LightWave.
Interfaces have areas where the user can press buttons, move
sliders, or even enter text. These little inventions make our lives on
the computer easier—just ask someone who has had the
misfortune of using software without an interface. It usually
involves a lot of typing, and staring at a black screen for hours. Not
fun.
When you add an interface to your script you add a level of
friendliness to your script, and you also add a new level of
complexity to your code. Interfaces usually require long and often
repetitive code that can easily clutter your once beautiful scripts.
This clutter could also generate errors that you will eventually
need to debug for your script to work correctly. So, why do we want
to use an interface? More importantly, what does an interface do for
our scripts?
An interface is simply a window or doorway into your code. This
doorway allows the user to view or change specific values stored in
variables. Yes, you can do this now by simply changing the
variables in the code, but interfaces allow this to happen while the
script is running. Imagine the following situation:
184 V O L U M E 1 : I N T E R F A C E S

You’ve made a really cool polygon reduction script. Like many of


the tools found in Modeler, your code allows you to set a level (1, 2,
3, or 4) to direct your code just how much of a reduction you want
to perform. This number-of-passes value is controlled by a variable
that controls a for-loop statement. So how do you make the script
satisfy the user who wants only one pass, as well as the one who
wants four passes?
You could write four different scripts, each with the loop variable
that has a different value. This would solve our problem, but it is
hardly an efficient use of your code. What happens when a user
wants five passes? So what is the answer?
The answer is to link the variable that controls the loop to an
interface. Your interface can then request from the user the number
of divisions to perform. Because the variable from the interface is
linked to the variable in your code (they can actually be the same
variable), by changing the value on the screen, you change the
value in the code. This functionality adds an incredible amount of
flexibility to your scripts. Now instead of making 101 different
scripts, which all do basically the same thing, you have only one
that is incredibly flexible. This is the difference between writing a
Static and a Dynamic script.

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

information to point to another object, the script’s function would


change: it would load a different object.
main
{
objFile = “c:/newtek/objects/animals/spider.lwo”;
load(objFile);
}
Although this has now made the script much more useful—it now
loads a spider object—it is still a static script because you need to
alter or create a different script every time you want a different
object loaded. We want a way to alter this variable while the script
is running. This would make the script run differently depending on
what value was stored in the variable. This would make our script
Dynamic.

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

// Get object’s path information here from the


// user and store it in the variable objFile
load(objFile);
}

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.

Instead of specifically stating what the variable objFile


contains, we can create an interface and request the information
from the user. After the interface runs, the path information will be
stored in the objFile variable. Regardless of how we go about
getting the data, as long as the objFile is a valid LightWave object
file, Modeler will load whatever path is stored.
The script is now considered Dynamic. Although the script’s
function doesn’t change every time it’s run, the end result can. It is
186 V O L U M E 1 : I N T E R F A C E S

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.

Note: It is not wise to create enormous interfaces that contain


hundreds of elements. This style can appear to be overwhelming and
confusing to the user.

As the requester window is moved around the screen, all the


controls that belong to the requester move with the panel.
Essentially, they are linked to the requester. LScript will
automatically assign an added control to the current requester that
is being edited. Therefore, it is impossible to create a control
without binding it to an interface.
The level of interaction a user has with a requester’s interface
depends on what your scripting needs are. For example, the much
used info() function simply creates a requester and displays a
string value. There is nothing else to interact with, just an “OK”
button to close the window. However, another script could have
several different controls where the user could enter or view data.

Figure 20-1. A simple info() requester.

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

Figure 20-2. A blank requester.

LScript automatically handles all other panel properties, such as


placement on the screen, color schemes for the panel and controls,
and all the required buttons to open, close, minimize, and maximize
the requester. Therefore, all requesters are created to work
correctly and look similar to other scripts and plug-ins found in
Lightwave. Without these defaults, the process of creating an
interface would take much longer, and be more prone to accidental
errors.
This panel does not do us much good alone, but when it is
finished, it will contain all the controls that the user needs to
interact with. The above requester uses the bare minimum for an
interface to work properly.
Controls
Controls are the elements in an interface that the user interacts
with. Each control is specifically designed to either gather and/or
display data from your script. LScript has dozens of different
controls available to your scripts including: buttons the user can
press, sliders to scroll through data, text fields where text can be
entered, and custom-made images. These controls are placed on a
script’s requester.
188 V O L U M E 1 : I N T E R F A C E S

Figure 20-3. A requester and sample controls.

The above requester demonstrates some of the different types of


LScript controls you can choose. Some of these controls allow you
to directly enter data into a field, while others make you select from
a list of options. You can retrieve all of these values by your running
script, which will determine the options and values the user has
selected.

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.

Note: Of course a Modeler or Generic class script can call several


other User Defined Functions (UDF’s) during its execution.

When this type of script is run, it simply performs its coded


actions then immediately returns control to either Layout or
Modeler. When the code is done running, the script is done. The
script will run again only when the user activates it again.
Alternatively, a Layout script may use several functions to
support all the features you want added. In a Layout script, the
code that does most of the work is found in the process() function.
Although most of the important code is found here, other functions
can set up variables, handle loading and saving, or even clean up
after the script finishes. It all depends on what you want to do.
CHAPTER 20: INTERFACE INTRODUCTION 189

Note: Throughout the following chapters we will refer often to a


Layout Class script. This script class is one that is found in Layout and
uses several specialized functions to perform the script’s actions.
However, a Generic script’s structure resembles a Modeler script
more than a Layout script. For this reason, when we refer to a Layout
script, we are referring to all Layout’s scripting classes minus the
Generic class.

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.

Figure 20-4. An instance of an Item Animation script.


190 V O L U M E 1 : I N T E R F A C E S

By performing either of these actions, LScript searches for the


options()function throughout your script’s code. If the function is
in the script, Lscript will perform its code, terminating the script
when it is through. However, if the function is not present in the
script, LScript will simply assume that the author did not want an
interface drawn for this script and politely inform the user that no
options are available.

LSIDE: Interface Designer


The LScript Integrated Development Environment (LSIDE)
contains the very powerful Interface Designer application. Using a
What-You-See-Is-What-You-Get (WYSIWYG) coding environment you
can layout and create an interface quickly and easily. By drawing,
dragging, and aligning controls on requesters, the Interface
Designer helps you make simple or complex interfaces. When you
are done, the Designer will export the requester as LScript code to
be copied-and-pasted into your script.

Figure 20-5. The LScript Interface Designer.

Although this process is incredibly fast and efficient, we believe


you should know what happens under the hood. In some situations
you will need to know and understand the purpose and function of
the code exported by the Designer. This will also allow you to make
additions or changes to the code without re-entering the Designer
and without the hassle of cutting-and-pasting new code over old
code.
CHAPTER 20: INTERFACE INTRODUCTION 191

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.

Note: More information on the Interface Designer can be found in the


“LSIDE” chapter at the end of this section.
192 V O L U M E 1 : I N T E R F A C E S
CHAPTER 21: MODELER AND GENERIC CLASS INTERFACES 193

CHAPTER 21: MODELER AND GENERIC CLASS


INTERFACES
Although the Generic and Modeler script classes use drastically
different functions and commands, the interfaces for these two
types of scripts are coded in the same manner. Because both script
types function as utilities, their interfaces are designed to get the
needed information from the user and make way for the code that
actually performs the function. Because of this simplicity, coding
their interfaces is much easier, with much less structure and
concerns. This is not to say that these interfaces are any less
powerful than those found in other script types, they are just a little
more straightforward.

Note: It is important to mention that interface code is bound to the


same limitations that their Class of script possesses. For example, a
Modeler Interface cannot access current frame information that would
be found in a Layout Interface.

Modeler Class Interfaces


Modeler scripts are tools designed to aid the user in modeling
geometry. These scripts are a good place to start learning interface
code because they do not have any access to a scene’s frame, light,
or camera settings. Nor are they processed every time a scene’s
frame information changes. This simplifies the structure of the
script immensely, especially the interface code.

Note: If you need a refresher course, see Volume 2: Modeler LScripts.

Creating an interface for a Modeler Script is very straightforward.


Typically in a Modeler or Generic script, interface code is started
once the usual scripting formalities such as headers and variables
are taken care of. Once the interface data is gathered from the user
and the proper variables are set, the script’s main processing code
should start. Unless you want to institute another panel, the
interface portion of your script is through.

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.

// Get the values from the interface.


val1 = 10;
val2 = 5;
CHAPTER 21: MODELER AND GENERIC CLASS INTERFACES 195

// Add the two values together.


val3 = val1 + val2;

// Display the sum.


info(“sum = “, val3);
}

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:”);

// Get the values from the interface.

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

the interface and to display it. We do this by using the reqpost()


function:

reqbegin(“Sum of two numbers:”);

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.

Figure 21-1. Not much of an interface but it’s a beginning.

Note: The “OK” and “Cancel” buttons are created automatically.

Now let’s put some controls on this requester. Because we want to


get some numbers from the user, let’s use the ctlinteger()
control. This function takes two values when it is called—a string
representing the label of the control, and an integer representing
the default value. Insert these two new lines into the existing
example script:

reqbegin(“Sum of two numbers:”);
c0 = ctlinteger(“Value2: “, 5);
c1 = ctlinteger(“Value1: “, 10);

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.

Figure 21-2. A near complete interface.

You probably noticed that we put the control labeled Value2


above Value1. We did this to demonstrate two things. First, the first
control created is the first that gets placed on the requester (from
top to bottom) by default. Secondly, we want to show you how to
move and size controls within the requester. To do this, we will use
the ctlposition() function:

c0 = ctlinteger(“Value2: “, 5);
c1 = ctlinteger(“Value1: “, 10);

ctlposition(c0, 35, 30);


ctlposition(c1, 35, 5);

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:

Figure 21-3. We can achieve a totally different look by manually


placing the controls on the Requester.

Notice that the controls’ positions are now arranged correctly.


The ctlposition() function gives us the flexibility we need to
perfect the look of an interface when we create our scripts.
However, in many simple scripts, with good planning and proper
coding, you can avoid this step by letting LScript automatically
place the controls on the Requester.
Now all we have to do is retrieve the values from the controls. We
can do this by declaring a variable equal to the value returned by
the getvalue() function. This function requires the Handle of the
control we want to grab the values from. So in our case:

// Get the values from the interface.
val1 = getvalue(c0);
val2 = getvalue(c1);

reqend();

// Add the two values together


Note: Be sure to delete the temporary val1 and val2 variables we


used earlier.

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();

// get requester control values here


val1 = getvalue(c0);
val2 = getvalue(c1);
reqend();

// Add the values together.


val3 = val1 + val2;
200 V O L U M E 1 : I N T E R F A C E S

// Display the result


info("sum = ", val3);
}

Generic Class Interfaces


There is little-to-no difference in coding an interface for a Generic
and a Modeler script. Because they are so much alike in structure,
coding their interfaces is practically identical. To prove it, simply
change the previous example from a Modeler Class script to a
Generic Class script and execute it from Layout. You will notice that
everything appears and functions like its Modeler counterpart.
The comparisons apply only to coding interfaces. Naturally, the
functions and commands available to the Generic Class scripts are
limited to those found in Layout.
CHAPTER 22: LAYOUT INTERFACES 201

CHAPTER 22: LAYOUT INTERFACES


An interface has two distinct functions. A script can either
retrieve settings the user wants to make or change, or it can display
stored data to the user and inform them of current values or
settings. A good interface will do both. This is where Layout and
Modeler scripts are similar.
However, as we previously discussed, the major difference
between a Modeler and Layout script is its structure. Modeler
scripts contain one pre-defined function while Layout scripts may
need several functions to work correctly. This directly affects how
we will approach coding an interface for a Layout script. Let’s look
at a template of an Item Animation script. This example does
absolutely nothing but set up the script’s skeleton code.
@version 2.2
@warnings
@script motion

create: obj
{
}

destroy
{
}

process: ma, frame, time


{
}

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)
{
}
}

Note: If you aren’t performing any tasks in a function, there is no need


to declare them. However, for our purposes, you should be exposed to
the more thorough way of scripting so you see how it is supposed to
look. Once you have seen it done this way a few times you can trim
back the code to use only the functions that contain code.

As you can see, the functions create(), destroy(), process(),


load(), and save(), all have a specialized function in the script’s
execution. While some are more important than others, all these
functions make the script’s abilities complete. LScript knows when
each of these functions should be run, and calls them according to
what part of the script is running. So where do we put our interface
code?
As we discussed in the first chapter of this section, we cannot
simply add our interface code to the process() function. This will
constantly bring up the requester and quickly irritate the user.
However, by declaring a function called options(), we are
indicating to LScript exactly where to look to find our interface
code. Simply add the following code snippet to the end of the
previous example script.
options
{
reqbegin("An Animation Test");
return if !reqpost();
reqend();
}
Do these commands look familiar? As we have mentioned, the
commands and functions used in the Modeler and Layout
interfaces are identical. The commands we used above are the bare
necessities for our interface to work initially. Actually, in this
particular situation, the reqend() function does not really need to
be there. LScript will automatically end the interface when it leaves
the options() function. However, it is good practice to manually
close the interface in case additional panels or interface code is
added later.
CHAPTER 22: LAYOUT INTERFACES 203

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:

Figure 22-1. A blank Options interface.

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

//Insert global variables here.

option1 = “Interface Test.”;


option2 = .01;
option3 = 1;

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.

Note: This function is not made available to Modeler scripts.

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);

// If item’s position is greater than maxXPos,


// change it to maxXPos.

if(currPosition.x > maxXPos)


currPosition.x = maxXPos;

// Set the position information.


ma.set(POSITION, currPosition);
}

Note: The comments should describe any unfamiliar code we are


using in this function. If not, see Item Animation or Motion Animation
in Volume 2. These sections outline everything we used in this
function.

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

After sufficient testing, and after we conclude that the code is


working, we can start to add the interface. First let’s start by
creating the options() function and an initial requester.
options
{
// Open the Requester.
reqbegin("Clamp");
return if !reqpost();

// Close the interface.


reqend();
}
Because we want to represent the X coordinate in 3D space, we
will use a floating-point value. Add a number control.

reqbegin("Clamp");

// Create a number control.


c0 = ctlnumber("Max. X Position",xPos);

Notice that the default value of the number control is set to be the
value of the xPos variable. Because we declared xPos to have the
value of 1.0, the default value of the control will be 1.0.
Now let’s collect the values of the control after it is closed and
store it in the global variable xPos.

return if !reqpost();

// grab the value from the interface and store it


// in the global variable xPos.

xPos = getvalue(c0);

// Close the interface.



Run the script and you should get an interface that looks like the
panel below:
CHAPTER 22: LAYOUT INTERFACES 207

Figure 22-2. The Clamp Interface.

Now let’s add the descriptive line to the plug-in list.



reqend();

// Send the xPos value to the plug-in's description.


setdesc("xPos:" + xPos);
}
The interface is now complete.

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:

Figure 22-3. An expanded Clamp interface.

Because we added the two other number fields, and their


supporting code, we have made our script more useful by
supporting the complete coordinate system. You can take this even
further by creating a script that clamps the maximum and
minimum values. The interface could look like this:
208 V O L U M E 1 : I N T E R F A C E S

Figure 22-4. Even more interface possibilities.

The code to create this script and interface is merely a derivation


of the example script we worked on previously. No new concepts,
functions, or commands were introduced.
The way we created this interface reinforces our method of
building scripts in sections and stages. By first writing the
functional code, then the interface, and finally taking it one step
further, we created something very flexible and robust. If you follow
this method, you will have fewer headaches as you create larger
scripts and interfaces.
CHAPTER 23: RULES FOR DESIGNING INTERFACES 209

CHAPTER 23: RULES FOR DESIGNING INTERFACES


For your initial scripts, you should make your interfaces very
simple in concept and design. Just learn and understand the basic
controls and how they function before you attempt to implement
any complex behaviors for your interface. Coding an interface is
not incredibly difficult. Like all other aspects of scripting, it just
takes time and some applied knowledge to get it all to work
together. The more interfaces you write, and the more examples
you study, the better your own interfaces will look and behave.
There are a few rules you should follow while you are learning to
code interfaces.
Rule #1: Functionality First
Interfaces are fun to design because the user sees and interacts
with it. However, do not be fooled into believing that your scripting
skills are judged solely on your interface design. Some novice
scripters make the mistake of taking more time in programming
their interface than they do writing the actual code for the script.
Interfaces are nice and they can bring an added level of
professionalism to a once dull-looking script, but they do nothing
more than service your code.
Because you are just beginning to code, it is safe to say that your
script might not work as you initially planned. Rest assured that
you will make mistakes, due to inexperience. These mistakes can
cost you hours of your time, but it is all part of the learning
process.
Because mistakes are inevitable, you should make sure that your
code functions properly before you add an interface. You want to
avoid wasting time writing needless interface code. That is not to
say that making an interface is not worth your time. But, nothing is
more aggravating than throwing away part or all of an all-powerful
interface because your script does not work as it was designed!
If at all possible, try to write your script to work with dummy
objects and hardwired variables before you attempt to write your
interface. By using these values instead of controls, you can focus
on the main task at hand, writing and testing your script. Once this
is done, it will ensure that your underlying code, which is doing all
the real work, is solid and will function as expected. When you have
written the functional code and it is bug-free, you can go to work
making your interface. At this point any interface code you write
210 V O L U M E 1 : I N T E R F A C E S

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.

Rule #2: Keep It Simple


An interface is a visual medium and like most things visual, less is
more. We can all relate to some script or plug-in that we used that
had an awful interface design. Whether there were too many
buttons on the screen, or the layout was scattered and
disorganized, the interface was an eyesore. Bad design actually
made the tool difficult, and in some cases, painful to use.
When you minimize the amount of information on the screen, the
interface will become easier to use. You can achieve this by adding
separators to organize the controls, or by adding tabs to reduce the
overall size of the requester. The trick is to make your interface
only as complex as it needs to be.
Before you even start writing interface code you should make a
list of values you will need to get from the user. You compile this list
from the hardwired settings and variables used to test the
functionality of your code (see rule #1). This information can
determine which controls need to be used. Then you organize how
they will be laid out on the requester, or determine if tabs can
group controls together. Consider this an outline for your interface.

Rule #3: Speed Over Beauty


The golden rule of “Less is More” can be taken to the extreme: if a
script does not need an interface, don’t make one! Do not make an
interface just because you can. Make an interface because it will
make your script more dynamic and useful. Our business requires
the tools we use to be fast and efficient. Nothing is more annoying
to a user than adding another step to a quick-and-dirty process. So
be smart in your designs. Use common sense: if the process is a
“once-in-a-while” script, which may be run only once a day, or even
once a month, then you do not need the efficiency that you would
for a script you use hundreds of times a day!
Rule #4: One Item at a Time
Whether it is the size of the panel or the placement of a control,
write code for only one item at a time. You will then be able to test
your work often, and catch mistakes faster. This will drastically cut
down on the time you spend debugging your interface and make
CHAPTER 23: RULES FOR DESIGNING INTERFACES 211

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.

Rule #5: Industry Standards


Determining which controls to use and how to arrange them on
the screen is an art form. Most software companies have specialists
who design and implement software interfaces. They have designed
their interfaces, from years of research and customer feedback, in
order to aid the creative process. You can learn a lot from these
people. In fact, you already have.
Through countless hours on the computer, you have come to
expect software to act in a certain way. Whether it is on Windows or
a Mac, from one application to another, certain functions behave
the same. This should hold true to your own creations. More
importantly, pay particular attention to how LightWave plug-ins
and other scripts set up their interfaces. Users are accustomed to
interacting with these components, and they will be more receptive
to your script if it handles in a way they expect.

Rule #6: Study Examples


If you see a script’s interface that you like, or wonder how one
acts a certain way, take a good look at the code. Break it down into
areas that you understand, and areas that might need more
research in order for you to incorporate it into your own scripts.
This is not only applicable to interfaces but coding in general. Take
a moment and learn from another scripter’s hard work.

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!).

Rule #7 Listen to User Criticism


Animators will try to use your code and interfaces in ways you
never thought possible. Do not take this as them not understanding
the purpose of your code, just the contrary; they just want to push
the limits of the script. Use their comments to get fresh ideas for
your script’s next version, as well as let you start dialogs with some
of your intended audience. These people can be invaluable
resources when it comes to bouncing off script ideas or beta
testing code.
212 V O L U M E 1 : I N T E R F A C E S

Rule #8: You Are Your Best Critic


Use your own scripts often; test them out. Besides helping
yourself debug your own code, personally running your own script
also offers immediate user feedback. If you do not like the way
something works, then there is a good chance the Lightwave
Community will not either.

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

CHAPTER 24: LSIDE


The LScript Integrated Development Environment (LSIDE) is a
suite of tools that helps the LScript programmer create and debug
scripts.
With the LightWave toolkit and technologies developed within
LScript, LSIDE is truly platform independent. This means that all of
the tools in LSIDE will perform identically on all platforms that
support LightWave.
As we will see in this section, the tools in LSIDE are also truly
integrated. They communicate with each other at key times in the
script development process to improve your workflow. As you use
the integrated tools in LSIDE, creating scripts becomes faster and
easier.
LScript Editor
Let’s first look at the backbone of LSIDE, the LScript Editor.
Although you can create scripts in any text editor, when you see the
power the LScript Editor places at your fingertips, you will want to
use it to create all of your scripts. The following list highlights some
of the cool features:
• color syntax highlighting
• mouse support
• LScript templates
• multiple documents
• LScript syntax checking
• search and search/replace
• block text shifting
• multiple levels of undo
• cross-document search/replace
The six main areas of the LScript Editor are: Menus, Text,
Command, Messages and Status. Let’s go over what each area does,
in order.
Menu Area
The four main pop-ups that make up the Menu Area work as you
would expect.
214 V O L U M E 1 : I N T E R F A C E S

Figure 24-1. The menu area of the LScript Editor.

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

Figure 24-2. The Edit menu

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-3. Font changes the size of your text

The Highlight section lets you set whether the LScript-specific


syntax is colored for readability or not. When you choose to
highlight syntax, different types of code are shown in different
colors so that each one stands out. This kind of highlighting makes
searching through your code much easier, because you can use the
color as a reference.
216 V O L U M E 1 : I N T E R F A C E S

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-5. Choose a template to start scripting

We can get a pre-made template for any type of script we want.


This is a great way to jump-start the scripting process, because you
do not have to worry about the script structure.
CHAPTER 24: LSIDE 217

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

And of course you must be able to Quit out of the application.

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

Figure 24-9. The Text area for editing scripts

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

Figure 24-10. Top: Script drop menu. Bottom: Function List


220 V O L U M E 1 : I N T E R F A C E S

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

match (brace, bracket or parentheses)


filename
new
close
Although you may not use any of the above commands because
they are available as buttons on the interface, one command may
be most helpful:
help
The help command will spill out the following list of keyboard
shortcuts for LSED:
CTRL+\ Toggle between the current edit document
and the command window
CTRL +] Match an open or closed character
CTRL +[ Toggle syntax highlighting on the current script
SHIFT + TAB Shift selected block of text four columns to the left
TAB Shift selected block of text four columns
to the right
CTRL + F7 Shift selected block of text one column to the left
CTRL + F8 Shift selected block of text one column to the right
CTRL + X Cut the selected block of text to the clipboard
CTRL + C Copy the selected block of text to the clipboard
CTRL + V Paste the contents of the clipboard into
the current document
CTRL + D Delete the selected block of text
CTRL + Q Quit
CTRL + O Open a new document
CTRL + S Save the current document if it has changed
CTRL + R Toggle the write state of a buffer and
its associated file
CTRL + N Next document
CTRL + G Go to a specific line number in the
current document
CTRL + 1-9 Select one of the first nine documents
222 V O L U M E 1 : I N T E R F A C E S

CTRL + F9 Increase the font size


CTRL + F10 Decrease the font size
Message Area
The display area at the very base of the LSIDE editor is the
Message Area.

Figure 24-12. The Message area where LSED will display messages about your script

If LSED needs to tell you anything about your document it will


appear in this Message Area.
Position and Mode Area
At the bottom right of the window are the Position and Mode
areas; look in the Position area to find the current location of the
cursor in the script. The position is given by line number and
column number.
CHAPTER 24: LSIDE 223

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

Use the debug() command to execute the LScript Debugger from


within an LScript. You can place this command anywhere in the
script’s code. This allows you to look more closely at the executing
code.

process: width, height, frame, starttime, endtime
{
debug();

var red[width];
var green[width];
var blue[width];
var alpha[width];
var i,j;
}

LScript Debugger Interface


The Debugger interface has a similar design to the LScript Editor.
The LScript Debugger interface is broken into seven different areas:
The Menu Area, Source Area, Status Area, the Watch List, and the
Message List.
Menu Area
From the Menu area, we have menu control over the debugger
functions. Those familiar with the Microsoft Visual C++
environment will find the keyboard equivalents in the Debug menu
familiar.
CHAPTER 24: LSIDE 225

Figure 24-14. The Menu area for the Debugger

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.

Figure 24-15. The View menu for the Debugger

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

The UDF information is important for watching values in the Watch


List area as the script executes.

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

Figure 24-19. The Watch area

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

Figure 24-21. Changing a value through the Watch area

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.

LScript Interface Designer


The LScript Interface Designer (LSID) is a What-You-See-Is-What-
You-Get (WYSIWYG) tool where you can design interfaces for your
scripts. LSID lets you arrange requestor controls; you can
interactively place controls wherever you like and dramatically
reduce the time to create complicated interfaces.
CHAPTER 24: LSIDE 231

Figure 24-22. LScript Interface Designer

As with the other members of the LSIDE family, LScript Interface


Designer is separated into several different areas: the Menu Area,
Component Tree Area, and the System Message Area. Let’s look at
what each area does for us.
Menu Area
The menus, along with the Interface designer, relate to the
functions that you can perform on an interface. To begin working,
you must first create a blank template for your interface or load one
that was previously saved.

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

The commands to load or save a template are found under the


File menu, and they are fairly self-explanatory. The one major
departure from this is the Export submenu. Unlike the Save and
Save As menu options, which save an interface file that only the
Interface Designer can load, Export lets you save actual script
code.

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-26. The New Layer option in the Tools menu

You can select and align any group of components on an interface.


The Align sub menu lets you choose how you want them aligned.
The Align options
If your controls are stacked vertically, you can align them on the
Left or Right.

Figure 24-27. Left: Select the items to align. Right: Items aligned Left

If your controls are arranged horizontally, you can align them on


the Top or Bottom.
CHAPTER 24: LSIDE 235

Figure 24-28. Top: Select the items to align. Botton: Items aligned with Bottom

SHIFT-click to select multiple controls. The Space menu options let


you evenly space multiple controls either vertically or horizontally.

Figure 24-29. The Space Menu

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-30. The Component Tree

You can access all of the editable values of any control by


expanding the control. You expand the control by clicking on the
downward-facing arrow. To edit the values, simply double-click on
the value you want to change and a dialog box appears.
CHAPTER 24: LSIDE 237

Figure 24-31. Click on downward-facing arrow to expand a control and see its values.

Figure 24-32. Double-click on the value to open its dialog box

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

On the interface itself, when you select the parent element, it


shows a line to each 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.

1 We start by adding a Tab control.


CHAPTER 24: LSIDE 239

Figure 24-35. Add a Tab control to Layer 1.

2 The default has three tabs, so let’s add three different controls
under each tab: Boolean, Number, and Choice.

Figure 24-36. Add the Boolean, Number and Choice controls.

3 Because none of the controls are assigned, everything on the inter-


face is visible. To parent a control to a tab, click on it and drag it up
and under one of the Item lines. For example, put the Boolean
under Tab1, the Number under Tab2, and Choice under Tab3 and
your interface and Component tree should look like the image that
follows.
240 V O L U M E 1 : I N T E R F A C E S

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.

Figure 24-38. Click on Tab1 to see its control


CHAPTER 24: LSIDE 241

Figure 24-39. Select Tab2

Figure 24-40. Click on Tab2 to see its control

Figure 24-41. Select Tab3


242 V O L U M E 1 : I N T E R F A C E S

Figure 24-42. Click on Tab3 to see its control

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.

Figure 24-43. Choose Export, then Layout LScript

LSID creates the code in the image that follows, which you can
then cut and paste into your script.
CHAPTER 24: LSIDE 243

Figure 24-44. Code created when you export

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

savescene 152 placing code 202 masterTest script 160, 163


semicolons 16 problems 189 maxValue script 145
user defined 154 requesters 186 Scripts
user-defined 13 required code 202 maxValue 140
Funtions rules 209 MC Script 159
loadscene 151 types 188 Menus
Introduction Edit 214
G Modeler LScript 31 File 214
Tools 216
generic function 151
Generic Interfaces 193, 200
L View 215
Mesh Data Edit 39
Generic Scripts 151 Layers 63
Mesh2Custom script 99
Global variable 83 Layout Interfaces 201
Message Area 222
grid of sample frames 111 Light Types 12
Mode
load function 70, 94, 118
Insert 223
H loadscene function 151
Overwrite 223
local variable 83 Mode Area 222
Handle 197
Looping Statements 25 Modeler
Hello World 33
Looping statements Points and Polygons 39
help command 221 for-loop 25
Highlight 215 Modeler Interfaces 193
while loop 27
Modeler LScript
lower case names 6
I LScript Debugger 223
Comments 36
main 34
If-then 19 LScript Editor 213 Modeler LScript Introduction
If-then-else 21 LScript Generic 151 31
Image Buffers 117 LScript Integrated Develop- Modeler Scripting Basics 33
Image Filter Functions 117 ment Environment 190, 213 ModInterfaceTest script 199
Image Filter Object Agent LS-GN 151 monitoring scripts 159
118 LSIDE 190, 213
Image Filter Scripts 117 LS-OR N
Incrementscene.ls script 152, Methods and Members 81
Read only 81 necessary interface code 202
157
write enabled 83 newtime function 69, 93, 103
index 9
LS-PT 103, 117, 127, 139, Number 7
infinite loop 142
151, 159, 183, 193, 201, 209,
init function 93, 103
Insert 223
213 O
LS-PT Data Members 104
Integers 7 Object Agents 54
LS-PT Functions 104
Interface channel 139, 140
controls 187 Image Filters 118
Interface Designer 190
M punctuation 55
Interface Introduction 183 main function 15, 34 Scene 109
Interfaces Shader 104, 110
Manual sections 2
generic 193 Object Replacement 81
Master Class 159
Layout 201 Members 81
master event handler 143
Modeler 193 Methods 81
LScript Index iii

Read only 81 Requester version number 155


write enabled 83 creating 195 Search 214
Object Replacement script 83 Requesters 186 Sections of Manual 2
operation modes 39 required interface code 202 selected.ls script 164, 178
Operators 20 Rules for Interfaces 209 Semicolons 16
options function 70, 94, 120 Rules for Variable Names 6 setting up Test Object 55
Overwrite 223 Setting variable names 6
S Shader Data Members 104
P sample grid 111
Shader Functions 103
Panel Shader Object Agent 104, 110
save function 70, 94, 118
creating 195 Shaders 103, 117, 127, 139,
savescene function 152
panel controls 187 151, 159, 183, 193, 201, 209,
Scene Object Agent 109
panel properties 187 213
scope of variables 203
panel size 186 Splat script 70
Script
panels 186 Changer 83 Spot light 12
Passing arguments 18 custText 96 Static Scripts 184
pause Layout 163 dynSplat 77 String 8
placing interface code 202 FissureLite 67 String Math 11
point coordinates Pulse2.ls 114 strings and variables 35
changing 69 replacer 91 Surfaces 63
Point light 12 Splat 70 Switch 23
WeldAvg.ls 50 Symbols for expressions 20
Points 39
weldfast 43
Polygons 39
Position Area 222
script debugger 223 T
Script drop menu 219
Pragma 53 table of sample frames 111
Scripting Basics
Preprocessor 53 Templates 216
Modeler 33
Procedural Texturing 103, Scripting Introduction 1 Test for conditions 19
117, 127, 139, 151, 159, 183, Scripts Test Object set up 55
193, 201, 209, 213 Barn.ls 97 Text Area 218
process function 70, 94, 118, Black and White 120 Texture 103, 117, 127, 139,
160 custText 94 151, 159, 183, 193, 201, 209,
Pulse2.ls script 107, 114 dynamic 185 213
dynlimiter 145, 148 Texture Data Members 104
R Generic 151 Texture Functions 103
incrementscene.ls 152, 157
remove extension from filena- timed loop 163
masterTest 160, 163
me 155 Toggle Write command 214
maxvalue 145
Repeating statements 25 MC 159 Tools menu 216
Replace 214 Mesh2Custon 99 Tracking errors 5
Replacement Object Agent ModInterfaceTest 199 trim filename 155
81, 85 monitoring actions 159 True-false
Replacer script 91 Pulse2.ls 107 if-then-else 22
replacing objects 81 selected.ls 164, 178 True-false variables 8
reporting scripts 159 Static 184
iv 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

You might also like