LScript for Layout
by Jeremy Hardin

Background/Purpose:
Parent_to_Null. A script that parents the selected object to a null.
download required files: gendata.ls | parent_to_null.ls

I first thought about this script when animating a ship crashing. It was a fairly straightforward shot that had no secondary animation. Just one big moving ship. The problem was that the flying up to the time of the crash had been animated in another package and was baked (i.e. it had a keyframe on every frame). The condition of my animating the ship was that I not alter the original flight animation.

So I thought, "It would be nice to be able to put this motion onto a null object, then parent the object to the null. I could leave the keyframes up to that frame intact on the null and lock the null." As it turned out, for all the animation in this shot, I had two translation nulls and two rotation nulls above the ship in the heirarchy. That's not including the original baked motion null. Thus the idea for this script was born.

Introduction:
This tutorial is a project based approach to LScripting in Layout. Our goal is to employ several different techniques. We will introduce concepts such as:

Scene Object Agents
File Object Agents
If Statements
Arrays
For Looping Statements
Error Messages
String Addition
Layout Commands

For the concept behind LScript and Programming in general, I recommend the Chrome Cow Tutorial. It gives great insight into the simple idea of programming.

http://www.chromecow.com/oldcow/tutorials/TUT-ProgramGI.html

Unfortunately, programming is no simple topic. There is a reason that people go to school for years to do it. I will not belittle their time spent by claiming to explain the what and why of all of LScript and programming. I will however cover everything we need for this script. So when we're done, we will have a working and useful script for the production environment, as well as a little more experience.

Preparation:
The type of script we're writing works like a to-do list. Unless we tell it to linger in a certain area, it will work from top to bottom, beginning to end. For this reason, it can be beneficial to write out a plan. What will it do?

Well, I first thought about this script when animating a ship crashing. It was a fairly straightforward shot that had no secondary animation. Just one big moving ship. The problem was that the flying up to the time of the crash had been animated in another package and was baked (i.e. it had a keyframe on every frame). The condition of my animating the ship was that I not alter the original flight animation.

So I thought, "It would be nice to be able to put this motion onto a null object, then parent the object to the null. I could leave the keyframes up to that frame intact on the null and lock the null." As it turned out, for all the animation in this shot, I had two translation nulls and two rotation nulls above the ship in the heirarchy. That's not including the original baked motion null.

The process is very straightforward. Select the object I want to parent to a null. Save the motion via File >> Save >> Save Motion.

Add a null object. Load the motion via File >> Load >> Load Motion. Parent the original object to the null object. Then remove the motion from the original object. Seems simple enough, right? Make a dang list then!

1. Select object
2. Save Motion
3. Add Null
4. Load Motion
5. Parent the original object to the null
6. Remove the motion from the original object

To remove the motion from the original object, you could delete all of it's keyframes. I opted for a different route. I added a null object. At this point the null has no motion. So I saved that null's motion to a file and loaded it to the original object.

We're going to do something similar in the script. Let's get a motion file with nothing in it. And let's make the motion file part of the script itself. We'll do this for two reasons. One is because it will save the step of saving the motion in the script. The other reason is because the method of embedding the file into the script is useful it quite a few other situations.

So enough talking. Let's get started on the prep work. Open Layout. Add a null object. Save it's motion. I called it "nomotion.mot". You can clear the scene. We got what we needed there. But don't close Layout yet.

Our next step is to convert the "nomotion.mot" file to binary data. This will work on any file. An image, a Lightwave object or scene, even a movie or executable. We're going to use another brilliant script for this, but I can't take the credit on this one. Bob Hood is the man. And we won't be going into his techniques. This one is just a tool. (But keep it around. It's quite handy.)
Run gendata.ls (incuded). Utilities >> LScript .

Select your motion file, in my case, "nomotion.mot". Then select a save location for your text file. Finally, call the data id "nomotion". We're going to copy and paste the text file contents into our script later, so remember where you put it.

On to the LScript-ing!!

The Basics:
Don't ask me why. These are the rules. Remember them.

1. a double slash, "//" tells lscript to ignore what ever comes after it.
2. lscript doesn't pay attention to when you press the enter key and end a line. it knows the line ended if you put a semicolon, ";" conditional statements are the only exception (if, else, for, and while).
3. what happens inside brackets "{ } " is kind of self contained. the conditional statements use them. and the main body of your script will happen inside them.
4. the @ symbol denotes what's called a Preprocessor Feature. We'll use it at the beginning of the script to help Layout compile the script to run it. And we'll use it after the body to denote the location of a portion of data to be used in the script.
5. the script type is called Generic. So we'll have to tell Layout that.
6. I'm going to be referring to Object Agents later. Picture an Object Agent like putting a sticky note on the different elements of your Lightwave scene. Now imagine that Lightwave only sees the sticky notes, not the elements themselves. And on the sticky notes, you've got a basic description of the item it's on. Different types of Object Agents have certain descriptions in common. For Example, ".name" works on most if not all types. Other descriptions are type specific (i.e. a Mesh Object Agent has a ".filename" description. this obviously wouldn't tell you much about a Camera Object Agent). These descriptions are called Data Members and Methods.
7. Variables are our friends. Variables are like your own little notepad you can jot down temporary information in to use in your script. To jot info into variables, or declare them, use this syntax.
variable = "What I want to store.";
So what you stored is the text in the quotation marks. Using the info() command, you can view the contents of any variable.
info(variable);
The above command would pop up an information box with the variable information in it. In this case, the text.

8. Arrays are also our friends. They are a type of variable that can hold more than one proverbial note jotted into it. So when you add information to the array, "coolarray", you'll be able to access different parts of it by using it's name name, a set of open and closed brackets ([]), and a sequential integer called an index. 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. So viewing the info in the first part of "coolarray" would be achieved like so.
info(coolarray[1]);

More information on these topics is available in the Lightwave 8 Help PDF file.

The Scripting:
Open a text editor. I use Smultron for Mac because it has syntax highlighting and line numbers beside each line. Lightwave also comes with it's own LScript Editor called, well, LSED. Go figure. Use what you like, but line numbers will be very helpful when you screw up and Layout tells you that the error with line 157 of your script. Yeah. I've counted in Windows Notepad. Not Fun.
Save the file as "parent_to_null.ls". And save your progress often!

First we're going to put some header information in our file. This will give basic information about the script and help Layout compile the script using Preprocessor Features.

//--------------------------------
//Parent_to_Null 0.1
//by Jeremy Hardin

@warnings
@name "Parent_to_Null"
@script generic

Remember that the double slash, "//" means that a comment is following. So I basically just put some comments at the beginning. Then I used 3 Preprocessor Features. The first, "@warnings", tells Layout to bypass the display of run-time warning messages. The second, "@name" is the name that Layout will recognize. If you add this script to Layout as a plugin, you will see it as the name you type here. The third tells layout that this is a generic script. That way it won't work as a channel modifier or whatever.

Next we'll specify the main body of our script. And to do this, we'll tell layout what type of script body this is, then put down the opening main bracket.

generic
{

Ok. All of our line by line commands will take place within the main brackets. Notice we haven't used semicolons ";" yet. This is because we haven't issued any LScript commands just yet. We've only given Layout the ability to follow those commands so far.

Now, we're going to declare the current scene as a Scene Object Agent so we can get some info about it. Specifically Parent in Place. We want this option specifically because we will be assigning parenting in this script and will need Parent in Place off. To declare a Scene Object Agent, we use the "Scene()" command on the other side of a variable.

scene = Scene();

I called the variable "scene" because it's something thats clearly understandable. Now we want to see if parent in place is off or on. So we use the generalopts[] data member.

par_in_place = scene.generalopts[3];

So now we've jotted down the current scene and it's Parent in Place setting into variables. If Parent in Place is on, the value of "par_in_place" will be 1. Otherwise it will be 0. Sweet.

Now We'll be making use of the user's temp directory for object's motions. First, We'll need to find the directory. then We'll assign it to a variable. We'll also get the content directory into a variable for future reference. Using the "temp_dir variable", We'll change our current working directory to the the temp directory. We'll then make a directory called "parent_to_null" for all the motion files. Afterward's we'll change that to the current working directory.

temp_dir = getdir("Temp");
content_dir = getdir("Content");
chdir(temp_dir);
mkdir("parent_to_null");
chdir("parent_to_null");

To remove motion from the objects before parenting them to nulls, we are loading a motion file with no motion in it. This is going to be stored in binary data below after we copy it from the text file we made. Using the "write binary" mode of the File Object Agent, we'll output the motion file into our current working directory, taking care to close the File Object Agent afterwards.

output = File("nomotion.mot","wb");
output.writeData(nomotion);
output.close();

Notice above that we simply assigned the File Object Agent to a variable, then used that variable's methods. Those methods are not available to all types of Object Agents. Just File Object Agents. So the variable is a file, "nomotion.mot", that will be written from binary, "wb", in our working directory, "parent_to_null" in the temp directory. The writeData method is referring to the "nomotion" data that will be below.

If parent in place was on initially we need it off for the time being. So we're going to use our first conditional statement. We basically want to say, "If Parent in Place is on, toggle Parent in Place". Let's do it.

if(par_in_place == 1)
{
ParentInPlace();
}

If you look above, you'll notice that there is no semicolon, ";" after the if statement. Conditional statements don't get them. Also, the double equal sign, "==" means "exactly equals". If you use only one equal sign in an if statement, you'll get an error because you'll be trying to assign an variable in an if statement. Finally, how did I know how to toggle Parent In Place? Well, in Layout, run Utilities >> LS Commander. Then open your options panel and toggle Parent In Place. In the LS Commander Window, you'll see "ParentInPlace" appear in the list. Click that text and drag it up to the top part of the window. This will put "ParentInPlace" in the top part as well. Then, in the LS Commander window, click Session >> Convert to LScript. And it converts the top "ParentInPlace" into an LScript Command!

Ok. Next on our to-do list is to save the motion of our selected object. Seems straightforward enough, right? Well, not if more than one object is selected. You can't just Save the Motion for all of the object at once. We'll have to use our proverbial notepad and jot down the selected objects. Then perform our operations on each object on our list.

Alright, we need to make an array of the selected objects. After declaring the array, we assign the size of the array to a variable.

s = scene.getSelect();
arraysize = size(s);

So we took the Scene Object Agent, "scene", and said, "Hey Mr. Scene. What's selected right now? Mind if I jot it down in my Array? I'll call it 's'." Then we make a note of the number of items that were selected by assigning the size of array, "s" to the variable, "arraysize".

We have a list of selected objects, As far as scripting is concerned, we want to depart from the top-to-bottom execution that generic scripts follow. We want to perform certain operations on an object from our list. Then we want to go to the next object in our list and do the operations again. And so on until we've gone through our entire list. Sounds complicated, but it's not too bad.

We going to use our next conditional statement. Instead of if, though, we're going to use a looping one. Specifically a "for" statement. The for-loop statement is the most common of the looping statements. When the for statement goes through everything in brackets once, 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.

for(x = 1; x <= arraysize; x++)
{

We said, "Start out with x equalling 1. We'll stop if x isn't less than or equal to the size of our array. And when you loop, go ahead and add one to x. Like I said. Do this until x isn't less than or equal to the length of our list, 'arraysize'. " Now we'll start the processing of the objects in our list.

First, we'll need the object's name. Remember that the object list is called "s". So we'll use x to tell it which object in the list we're working on.

itemname = s[x].name;

Now we've got the name. Next we want to use the Mesh, Light, or Camera Object Agents to access different properties of the item we have select. But we need to know what kind of item it is, since those Object Agents are declared in different ways. We'll use another if statement for this as well as the ".genus" data member of the "s" object. If genus returns 1, it's a mesh. 2 is a Light. 3 is a Camera. And 4 is a Bone.

if(s[x].genus == 1)
{
selected = Mesh(itemname);
}
if(s[x].genus == 2)
{
selected = Light(itemname);
}
if(s[x].genus == 3)
{
selected = Camera(itemname);
}
if(s[x].genus == 4)
{
error("Bones must remain parented to the object they belong to.");
}

Two things about the above code. The first is that we've introduced the error() command. It's basically the same as the info() command. It displays whatever text, information, or variable we put into the parentheses. The difference is that the information window is now an error window. And if the error() command is encountered, it ends the script after displaying the information you wanted. Game Over.
The second thing is why we introduced the error command. It's because one of the things in our to-do list is re-parenting the selected items. Well, bones can't be re-parented. The must remain the child of their current object. So we're letting the user know that.

Now that the current item is assigned to an Object Agent, we can select it by it's id. The item's id holds the integer identifier of the Layout object for which it serves as proxy. This integer identifier is the same one used by Layout to uniquely identify objects in the scene file. Then we'll construct the command to save it's motion. After we employ the newly constructed command, we'll detect if it's parented or targeted to anything. We do this so that the object will move exactly as it did before, regardless of it's place in the heirarchy. And last in this section we'll add the null that will become the current item's parent. We don't give the null it's final name yet in case that name already exists in the scene. This could cause potential conflicts. So we'll call it "parent_to_null_parentnull". It's unlikely that this will already exist in the user's scene.

SelectItem(selected.id);
savestringadd = ("SaveMotion " + itemname + x + ".mot");
CommandInput(savestringadd);
item_parent = selected.parent;
item_target = selected.target;
AddNull("parent_to_null_parentnull");

SelectItem() tells layout to select the item in parentheses. Then we put a command together in a variable that will save the item's motion to our current working directory. After this, we use the CommandInput() command. This is exactly like clicking Utilities >> Command Input. So whatever is in parentheses will be "typed" into Layout's command input. For a list of commands, use LS Commander, or click Utilities >> Save Cmd List and look at the resulting text file. Then we use the ".parent" and ".target" data members to get the current item's parent and target (if they exist). We can then access them as the variables.

Since we added the null, it is selected. We need to reselect our current object. Then we'll load the static motion file that we created above. Following this we parent the current object to the "parent_to_null_parentnull". We then select the "parent_to_null_parentnull" object again and construct the Load motion command in the same way we saved it above and execute the command.

SelectItem(selected.id);
LoadMotion("nomotion.mot");
ParentItem("parent_to_null_parentnull");
SelectItem("parent_to_null_parentnull");
loadstringadd = ("LoadMotion " + itemname + x + ".mot");
CommandInput(loadstringadd);

If the original item was parented and or targeted, we parent and or target the new null. Finally, we rename the "parent_to_null_parentnull" to have the same name as the current item with "_ParentNull" appended to the end of it.

if(item_parent != nil)
{
ParentItem(item_parent.name);
}
if(item_target != nil)
{
TargetItem(item_target.name);
}
namestringadd = ("Rename " + itemname + "_ParentNull");
CommandInput(namestringadd);

Alright. Our to-do list is done for this item. Remember, we're currently still in a for() statement. So we need to put the closing brackets down and be done with it. Since conditional statements can exist within each other, you can end up with 4 or 5 brackets in a row and it's easy to lose track. So I usually throw a comment in telling what conditional statement is ending with that bracket. I recommend you do the same.

//our for statement ends here
}

We could stop here, but remember we did two things to change the user's Layout settings. One was that we turned Parent in Place off. The other is that we changed Layout's working directory from the user's content directory. ALWAYS change the user's current working directory BACK to the content directory when you changed it in the script.

if(par_in_place == 1)
{
ParentInPlace();
}
chdir(content_dir);

That's almost it. Let's close the main body of the script. Notice I comment again to tell why there's a closing bracket.

//end of the generic script
}

Finally, open the text file we created a while ago. It has the binary information in it for the "nomotion.mot" file. We refer to it above so without this, the script will return an error. Copy the information onto the end of our current script. Make sure it's after the closing bracket for the body of the script. It should look something like the code below, though it won't have any brackets and it will have a whole lot of numbers.

@data nomotion [lot's of numbers]
[even more numbers on a bunch of lines]

So the completed script (minus the @data portion. You're on your own there). I've spaced things a little bit, but it's just for organizational purposes. It's not necessary. Though it's probably a good habit.

//--------------------------------
//Parent_to_Null 0.1
//by Jeremy Hardin

@warnings
@name "Parent_to_Null"
@script generic

generic
{

scene = Scene();
par_in_place = scene.generalopts[3];
temp_dir = getdir("Temp");
content_dir = getdir("Content");
chdir(temp_dir);
mkdir("parent_to_null");
chdir("parent_to_null");
output = File("nomotion.mot","wb");
output.writeData(nomotion);
output.close();
if(par_in_place == 1)
{
ParentInPlace();
}
s = scene.getSelect();
arraysize = size(s);
for(x = 1; x <= arraysize; x++)
{
itemname = s[x].name;
if(s[x].genus == 1)
{
selected = Mesh(itemname);
}
if(s[x].genus == 2)
{
selected = Light(itemname);
}
if(s[x].genus == 3)
{
selected = Camera(itemname);
}
if(s[x].genus == 4)
{
error("Bones must remain parented to the object they belong to.");
}
SelectItem(selected.id);
savestringadd = ("SaveMotion " + itemname + x + ".mot");
CommandInput(savestringadd);
item_parent = selected.parent;
item_target = selected.target;
AddNull("parent_to_null_parentnull");
SelectItem(selected.id);
LoadMotion("nomotion.mot");
ParentItem("parent_to_null_parentnull");
SelectItem("parent_to_null_parentnull");
loadstringadd = ("LoadMotion " + itemname + x + ".mot");
CommandInput(loadstringadd);
if(item_parent != nil)
{
ParentItem(item_parent.name);
}
if(item_target != nil)
{
TargetItem(item_target.name);
}
namestringadd = ("Rename " + itemname + "_ParentNull");
CommandInput(namestringadd);


//our for statement ends here
}
if(par_in_place == 1)
{
ParentInPlace();
}
chdir(content_dir);


//end of the generic script
}

//below is our motion file with no motion in it. it has been converted to
//binary data and is being stored here until called upon above as "nomotion".

@data nomotion [lot's of numbers]
[even more numbers on a bunch of lines]

Go To Top