Making Life InterestingYour First Ten (or So) Words of MXSMAXScript can take a big bite out of routine tasks. With a few simple commands at your disposal, you can frequently automate your way out of tedious clicking and even approach some problems from an entirely new angle. The goal of this section is not to make you a scripting whiz but instead to provide a few tools to help you automate tasks without involving a technical director or searching ScriptSpot. Your tool for interacting with MAXScript is called the Listener (MAXScript > MAXScript Listener), a window into which you can type script commands that are immediately executed, and into which MAXScript writes its output (Figure 6.1). Your commands appear in black, while MAXScript's feedback appears in blue (for output) and red (for error messages). Figure 6.1. The MAXScript Listener window.Your starting vocabulary is the following:
Let's dig in. The "$" Symbol$ means the current selection. For now, it is most useful when only one object is selected, as it behaves a bit differently with multiple objects selected. Why do you need this? Well, one of the simple things MAXScript is good for is looking at and changing properties of objects in your scene. You access properties of the current object with $.propertyfor example, $.radius or $.height. Like most things in 3ds max, properties are organized into hierarchies. This means that properties can have properties of their own, and so on, all separated by dots, as in $.material.diffusemap.coords.U_Tiling, the U Tiling parameter of the Diffuse Color map of the selected object's material. If you just type a property name into the Listener, it will tell you the value of that property. To assign a new value, use $.property = newvalue. For relative values, you can use the operators += , -= , *= , and /=, as in $.radius /= 2, which halves the radius of a selected object (as long as it has a radius property to change). Note
The value you set doesn't have to be a simple number. You can use mathematical expressions, other properties, or even math that operates on other properties. $ has another, similar use as well: Follow it with an object name, like $Box01, and you are referring to that node, and all the access to properties described above is just as valid (for example, $Box01.height). By using the wildcards * and ?, as in $Box*, you can set properties for a whole list of objects. (We'll get to handling multiple objects in more depth later.) Note
The "show" CommandHow do you know if an object has a particular property? As you might guess from the examples above, there are a lot of property namesfar too many to memorize. That's what the show command is for. show generates a list of an object's properties in the MAXScript Listener. For example, if you select a box and type show $, you'll get the following: .height : float .length : float .lengthsegs : integer .width : float .widthsegs : integer .mapCoords : boolean .heightsegs : integer false The word to the left of the colon is the property name, and the word on the right describes the type of value it's expecting (The false at the end is a MAXScript hiccup, not a property name; you can safely ignore it.) If you try to set a property to the wrong kind of value, as in $Sphere01.radius = false, MAXScript will complain with an error message. The sidebar explains some of the common value types you're likely to encounter and how to format them. Bear in mind that not all the object's properties are shown. All objects, as well as certain classes of objects, share some properties, such as name, material, and renderable, which do not appear for clarity's sake. Also, properties of the object's modifiers, materials, and other "extras" are not shown. Full listings are available in the MAXScript Reference, under "General Node Properties," as well as in the "common properties, operators, and methods" entry for that object class (see, I wasn't kidding when I said you had to use the Reference!). Note
When an object's properties have properties of their own, you can use show to list those subproperties as well. For example, if you needed to find out how to view or adjust properties of the box's material, you would type show $Box01.material. If you then wanted to find properties of the material's Diffuse Color map, you'd find the MAXScript property name for the diffuse map in the output of the previous command and use it to write show $Box01.material.diffusemap, and so on. Putting a Two-Word Vocabulary to WorkSo what can you do now, with only two words of MAXScript, that you couldn't do before? You can do the following:
Pretty good for just two words, huh? Armed with your amazing vocabulary, see if you can figure out what each one-line script below does: $Box01.height = $Box02.height $Box01.height = $Box01.length = $Box01.width = 10 $Sphere01.radius = $Box01.height/2 $Box*.height = 10 $Box*.isHidden = false $Omni*.multiplier /= 2 $Box*.material = $Sphere01.material $Spot*.on = false The answers in plain English are below:
"move," "rotate," "scale," and "in coordsys"Now we're going to add four more words, to give you some more verbs to use in your MAXScript sentences. You know what move, rotate, and scale mean already; we just need to cover syntax for those, and I'll bet you have your suspicions about coordsys. For move commands, you provide XYZ coordinates in square brackets, separated by commas (MXS calls this kind of value a point3): move $Box01 [10,0,0] moves the box 10 units in the positive X direction. Note that this is a relative move. For an absolute move, you set an object's position property instead: $Box01.position = [10,0,0] moves the box to 10,0,0 in world coordinate space. Rotation values are a little more complicated, and they require us to sneak an extra word into your vocabulary. To avoid the extremely ugly math of true 3D rotations, MXS lets you input rotation as a eulerAngles value: an X rotation plus a Y rotation plus a Z rotation. Like the similarly named Euler XYZ rotation controller, this method has problems with arbitrary rotation axes and with gimbal lock (the condition in which, after a lot of Euler rotation, the XYZ axes are no longer mathematically perpendicular and not all rotations are possible), but the gain in usability is well worth it. rotate $Box01 (eulerAngles 90 90 0) rotates the object 90 degrees in X and 90 degrees in Y. scale transforms take a point3 number, just as move does, but considers 100% scaling to be equivalent to the number 1, so a 50% uniform scale would look like this: scale $Box01 [.5,.5,.5] Now, all these transforms happen in world coordinate space by default. In order to do a transform in local, parent, or any other coordinate system, you preface your transform command with in coordSys some coordinate system: in coordSys local move $Box01 [10,0,0] moves Box01 10 units in Local X rather than World X. Automating with "for" LoopsLet's move on to what is arguably the most important automation tool in MAXScriptthe for loop. As pseudocode: for variable in a list of things do something In other words, perform a task (or sequence of taskssomething can mean a whole list of commands, including other loops) on each member of a list. This is where the "automating repetitive tasks" aspect we keep talking about really takes off. for i in selection do (i.wirecolor = color 0 128 0) In this example, the variable is named i (this is arbitrary, but using i as the loop counter is a programming convention), the list of things is the keyword selection, which as you may have guessed means "the currently selected objects," and the something is to set one of the object's properties to a specific value, in this case the wireframe color to bright green. In other words, the pseudocode reads as follows: for (each object) in (the selection) do (set the wireframe color to green) See how that works? Now, one thing to keep in mind is that each time through the loop above, i serves as a placeholder for the object itself. So anything you know how to do to a single object (transform it, query a property, change a property, or any combination) you can now do to a whole list of objects automatically, by putting the command after do, and substituting i for $ or $objectName. The example above uses selection as the set to work on, but there are many other possibilities. These include predefined keywords (objects, geometry, lights, cameras, helpers, shapes, systems, sceneMaterials, meditMaterials, and spacewarps), wildcarded names (for i in $Box* do i.height = 10), numbers, or explicitly defined lists of objects called arrays (more on those later). Now that you have for in your arsenal, you can really start automating tedious tasks: for i in $*Omni* do (i.multiplier = i.multiplier*0.5) means "Dim all lights with Omni in their name by half." for i in selection do i.material = $Box01.material means "Set the material of all selected objects to Box01's material." You can also enter multiple commands on a single line in the do statement; surround it with parentheses and use a semicolon to separate the commands: for i in geometry do (i.motionblur = #image;i.motionBlurOn = true) means "Turn motion blur on and set the type to image motion blur for all scene geometry." Note
"where" and "classOf"Now let's try a for loop that doesn't work. Try a one-liner to turn off all the lights in your scene: for i in lights do i.on = false If your scene contains any targeted lights, you'll get an error similar to the following: -- Error occurred in i loop -- Frame: -- i: $Target:Spot01.Target @ [-11.212518,-15.219044,0.000000] -- Unknown property: "on" in $Target:Spot01.Target @ [-11.212518, -15.219044,0.000000] What the error message means, line by line, is the following: The code in the for loop using the variable i had a problem. If this had been an animation problem, the frame number would go here. When the problem occurred, i represented the target object Spot01.Target at these coordinates. The problem was that there was no "on" property to set on the object. In other words, our problem is that by using the keyword lights, we included the light target objects as well, and MAXScript complained and gave up because targets don't have an on/off switch. To do what we want, we need to exclude the target objects from processing. That's where where and classOf come in. where lets you filter a for loop's actions by inserting a true/false statement that is checked each time through the loop. If the statement is true for that iteration of the loop, the code in the loop executes. If false, that iteration is skipped. In its purest form, for i in selection where (false) do something does nothing, while: for i in selection where (true) do something acts on everything in the selection. To make this useful, though, we want to put an expression after the where that evaluates to a true/false, or Boolean, value. And for that we need some operators that test truth. There are several ways to compare two values (Tables 6.1 and 6.2).
for i in $Box* where (i.height > 10) do i.height = 10 means "Set all objects whose names start with Box that are taller than 10 units to a height of 10." for i in $Box* where((i.height > 9) and (i.height < 11)) do i.height = 10 means "Set all objects whose names start with Box that are between 9 and 11 units tall to a height of 10." for i in geometry where (i.material == undefined) do i.isHidden = false means "Unhide all geometry that doesn't have a material assigned." for i in shapes where (i.baseobject.renderable == true) do i.name = "RS_" + i.name means "Add RS_ to the start of the name of all renderable splines (to make selection by name easier)." Note
But what about our broken code from the start of this section? We need a way to test what kind of object something is, so that we can plug it into our where clause: classOf. Every object in max belongs to a class, a category whose members share identical parameters and behave in the same way. Boxes are a class, as are spheres, and both classes are distinct from the editable mesh class. Take our beloved Box01, for example: classOf $Box01 returns Box Like show, classOf can be used as a reference tool, except for class names rather than property names. Unlike show, the value it returns can also be used in a truth test expression: classOf $Box01 == Box returns true. To use it in a where clause, just add parentheses: for i in geometry where (classOf i == Box) do i.height *= 2 which means, "Double the height parameter of all box primitives, regardless of name." One more thing about classOf before we debug our lighting script. If you add a modifier to the box, such as Bend: classOf $Box01 returns Editable_mesh instead of Box. The class name reflects the current state of the object, not the one it was born with (if you deactivate the modifier, for example, classOf $Box01 will return Box once again). If you want to ignore all modifiers when checking class, use: classOf $Box01.baseobject which looks only at the bottom entry in the modifier stack and returns Box as before. Turning Off the LightsLet's debug our previous failed turn-off-all-lights script command using where and classOf. Previously we tried: for i in lights do i.on = false which bailed because it couldn't set the on property for target objects. To get around this error, we want to change the command so that it skips over targets. This technique turns out to be essential when doing anything that batch-processes lights or cameras. All the lights turn off, and MAXScript doesn't complain a bit. Now the power of for is really unleashed for scene management. You can conditionally change properties for thousands of objects at a time, if needed. Here are a few highly useful sentences to get you started: for i in cameras where (classOf i != targetObject) do i.mpassenabled = true means "Turn Multi-Pass Effects on for all cameras." for i in geometry where (i.material == $Box01.material) do i.isHidden = false means "Unhide all objects sharing Box01's material." This gets around the limitations of the Material Editor's Select by Material button, which only considers visible objects. for i in geometry where i.material == undefined do print i.name means "Output the names of all objects with no material assigned." for i in lights where (classOf i != targetobject) do i.multiplier *= 1.05 means "5% brightness increase on all lights in the scene." [View full width] means "Turn off Show Cone and Show Attenuation Ranges for all targeted spotlights." for i in shapes where (i.baseobject.renderable == true) do (i.isHidden = false;selectMore i) means "Unhide and select all renderable splines." Doubtless you have a number of least-favorite chores of your ownstart using that show command to add your own. "random" NumbersProducing high-quality 3D imagery often means overcoming the tendency of the computer to make things look too perfect and orderly. Scenes that are too regular, ordered, or idealized break the illusion, and many of our most effective techniques involve "dirtying up" our scenes in some way. Disorder added by hand has a way of not looking random enough, however. If you've ever done any particle work, you know that particle systems' controls for adding randomness are the key to making a believable effect. The MXS command random is a fantastic tool for adding chaos to other parts of your scene (in a carefully controlled manner, of course!). You use it with the expression: random value1 value2 where the two values are of the same type, and they define the upper and lower limits of the range of possible values. We're now going to use your burgeoning MXS vocabulary to add some chaotic believability to a scene. Adding Chaos to a Scene with "for" and "random"
See the difference? That little bit of chaos has given the scene a lot more life, without altering the narrative truth of "four tables, each with four chairs, pushed in." It's just that now employees, rather than robots, did the organizing. Viewers don't see the difference, but they do feel it. And all it cost you was the time to type four sentences of MAXScript! The scene in its final state is on the DVD as cafe_01.max. If you were developing this scene further, you could take a similar approach to positioning the place settings on the tables, rotations and scale of flowers on the tables, and so on. Just a couple more words for your MAXScript vocabulary, and you'll find yourself using it like a Swiss army knife. #( ) and [ ]Working with ArraysAn array is a list of data. Arrays are indicated by parentheses preceded by a number sign, and commas separate each element: myArray = #($Box01,$Box02,$Box03) The above code defines a variable named myArray as a placeholder for the list of three boxes. To access a member of the array, you type the array's name followed by an index number in square brackets. The number indicates the position in the array of the object you that want to access: myArray[2] returns something like: $Box:Box02 @ [6.800152,13.702519,0.000000] and: myArray[2].height accesses Box02's height parameter. A lot of built-in data structures are stored as arrays, and being able to access them by index number in this way makes them accessible to for loop processing. Instead of processing the array itself, though, it is often preferable to have the for loop iterate over a series of numbers like so: for i in 1 to 3 do myArray[i].height = i*10 Each time through the loop, the value of i increases by 1, and so Box01's height is set to 10, Box02's to 20, and Box03's to 30. In other words, we just built a simple staircase generator in two lines of code. Using random and the built-in meditMaterials array, you can use this technique to randomly assign materials to a group of objects. Say you have a crowd shot to finish that uses textured planes for the individual people. You load your 12 people materials into the first 12 slots of the Material Editor, select the planes, and type the following: for i in selection do i.material = meditMaterials[(random 1 12)] Since (random 1 12) evaluates to a number, a random index is being generated each time through the loop, and thus a random material from the first 12 Material Editor slots is assigned to each plane in the selection. An object's modifier stack is an array as well, with modifiers[1] being the top modifier in the stack. When you don't know how long your array is going to be, you can use the array's count property to find out. for i in 1 to $.modifiers.count do $.modifiers[i].Enabled = true turns on all of a selected object's modifiers. When processing modifier stacks for multiple objects, it becomes necessary to go beyond a single line of code (actually it is possible in a single line, but it's harder to read and debug). To create multiline scripts, you need to open the MAXScript Editor rather than the Listener (MAXScript menu > New Script). To execute the Editor code, use the window's File menu > Evaluate All. for i in geometry where (i.modifiers.count > 0) do ( for j in i.modifiers where ((classOf j == meshsmooth) or (classOf j == TurboSmooth)) do ( j.useRenderIterations = true j.renderIterations = 3 j.iterations = 1 ) ) Can you see what's going on in the above code? The parentheses and line breaks are mostly for clarity's sake. They help you visually separate sections of your code. The first for loop is exactly what we've been using in our examples so farfor i in selection do something. The something in this case, though, is a second for loop, using j instead of i as the index variable. j is used because i already has a meaning within the loop, so we need to pick another variable name to avoid conflicts. In pseudo-code, our script reads like the following: for all geometry with at least 1 modifier do ( for each of this object's modifiers that's a meshsmooth or turbosmooth do ( turn on the modifier's render iterations set the modifier's render iterations to 3 set the modifier's view iterations to 1 ) ) The j loop looks at each object (represented by i) and puts each modifier into the value j. It then checks whether that modifier is a MeshSmooth or TurboSmooth, and if so, it performs a series of tasks: It checks the Render Iterations check box, sets the Render Iterations to 3, and sets the Viewport Iterations to 1. Note the two parentheses at the end as well. All parentheses have to balance out in order for code to execute properly. The practice of indenting your code at each parenthesis helps you keep track visually of whether your parentheses are properly closed. Note "at time"Animating with MAXScriptWe've reached the last term you need for your basic vocabularycongratulations! With at time, you can use MAXScript to animate, as well as to change static parameters. The usage is the following: at time frame followed by any valid MAXScript. If (and only if) you have Auto Key turned on, this will cause your code's actions to set a keyframe. Note
In the following exercise, we'll use the power of at time and some MAXScript you already know to animate 20 objects in a hurry. When the specifics of the animation aren't critical (such as for background objects or for a large number of objects at once), animating with MAXScript can be a huge time-saver. Haunting the Cafe with "at time"
Obviously, the choice of actual numbers to plug in is an aesthetic decision. The basic program structure above, however, is extremely flexible. Bear in mind, also, that unlike particle- or modifier-driven animation, this approach can serve as a starting point for manual keyframing. If you like the motion of three of the tables but not the last one, simply adjust the keys by hand. You'd be doing a lot more manual work if you hadn't used a scriptyou've saved work even if the output isn't perfect. With only a few words of MAXScript, we've managed to batch-process objects, randomize aspects of your scene, and even generate animation! Even if you don't go any deeper into scripting than this, it's still a versatile and powerful tool to have. |