Line by Line through the Random Envelope Macro by Grant Boucher One of the most powerful features of the Amiga operating system is the ARexx programming language. Essentially, ARexx lets you write your own programs that control other programs according to your needs. For example, you can use ARexx to write programs that will convert large numbers of image files from one file format to another. With the new Modeler Arexx and Macro functions, we can program our own custom LightWave and Modeler effects without having to know how to program in C or how the Modeler actual does the things it does. Just take a look at the vast numbers of powerful ARexx Macros, both public domain and commercially available, and the great deal of functionality they have added to the entire LightWave/Modeler combo. Recently, I was involved in some special effects shots that required at least a hundred lights which needed to randomly undulate in intensity. We also wanted the option of having them fade out towards the end. Since the shots were as long as 900 frames and we wanted to avoid repeating any motions which might make the lights look mechanical or programmed, I was asked to whip up a quick and dirty ARexx script to do the job. Once the LightWave envelopes were generated, I was then asked to give the macro a gui interface and add some features that might make it a little more useful for future effects shots. That's where Modeler came in and and I felt the finished Modeler Macro seemed a perfect tutorial for those not yet initiated into Arexx programming the Modeler. ARexx - Basic principles ARexx is actually just a typical programming language with an attitude. You'll find all of the standard programming tools of a Basic, C, or Pascal style language that you'd expect, but also the extra added bonus of commands that come from virtually all other Amiga programs. Image processing programs like ImageFX and ADPro come with entire reference manuals listing special commands that allow you to load, process, and save image data...all from YOUR ARexx script! So, I like to think of ARexx as just a new version of all the programming languages I have used in the past, with Amiga software and hardware control functions thrown in. Until recently, I used ARexx primarily for batch processing of files, so I didn't have to manually Load Object, Flip Polygons (wait), Reduce Faces (wait), and Save Object for every Public Domain Imagine object I wanted to convert to LightWave using Pixel 3D Professional, for example. Writing ARexx Scripts Since ARexx programs are just text files that the RX program interprets, you can use any word processor like Final Writer or a text editor like CygnusEd to write and edit your ARexx script. If you use a Word Processor, make sure you import and export your file as ASCII text, as the word processor's own internal file format will certainly add strange control characters that mean nothing to ARexx and make your script unusable. The Program - line by line Line 1 - The Author Line The first line of any ARexx script must be a comment line. It is just ARexx's way of signifying that this is an ARexx program, and not a letter you are sending to your mother. Comments begin with the /* characters and end with the */ characters. Comments are usually used to document your script for others or yourself if you come back to it three years later. Since the first line of any ARexx script must be a comment, most programmer's find this a convenient place to put their name for posterity. Note that comment lines don't affect the operation or function of your program, and are simply ignored when it is run. Lines 3 - 12 For most Modeler macros, you'll want these lines at the start of your script. Basically, they initialize special ARexx math functions, turn on some special debugging tools, and generally check to make sure everything is set up right to start. Many initialization functions like this can be just grabbed from the start of another ARexx script and left alone. I don't worry about them, so probably neither should you. Line 15 This line calls (or performs) Modeler's requester and interface definition, named REQ_BEGIN(). The text that follows this command in quotations becomes the name of the requester. The Modeler automatically formats this and all requester commands for you! Line 17 This is the first of the requesters, where we actually want some user-input to come back. Again, we use the REQ_ADDCONTROL() function to set up our new feature, but this time we pass only two arguments to the function. The first is a text message describing to the user what this function means. The second is the letter 'B' which tells the function that this is a BOOLEAN, or ON/OFF, style requester. The modeler automatically creates the Check-Mark gadget for you! I gave this feature the name RampID so I would know that this is my Ramp to Minimum toggle. This feature, if on, tells the macro to decrease the overall intensity of all of the envelope key frames until they equal the user's lowest intensity value by the end of the envelope. In other words, the intensities fade towards the end of the envelope. Lines 18-25 All of these lines are essentially the same. This time, the REQ_ADDCONTROL() function is passed a letter 'N' (as the second variable again) to signify that this is going to be a numeric requester, asking for a value from the user. The first argument is again a description of what number the user needs to enter, and the third argument tells the requester what units the number will be in. Since we are working with arbitrary values like frames and intensities, not meters, we send a 0 for all of these, which means 'unitless'. Note the strange %1 and %2 characters attached to these functions. The % character stands for INTEGER DIVIDE, meaning it is the same as a standard DIVIDE command (i.e. '/') but that the result is always an integer. When I take a number and INTEGER DIVIDE by 1 (i.e. %1) I am just forcing whatever value the user entered to become an integer. We have no use for Frame 10.333 for example! The MINFRAME and MAXFRAME variables are INTEGER DIVIDE by 2 (i.e. %2) because if our envelope peaks are going to be, say 30 to 50 frames apart, each key frame is actually going be half that amount from every other key frame. Two key frames, or twice that value, gets you back to where you started from, either a peak or valley. I kill two birds with one stone as I make sure these values are integers and they are reduced by half so our program works properly. Definitions of our main variables... MINFRAME = The minimum distance between peaks of our undulating envelope. For example, a value of 20 here means that no two high points (or two low point for that matter) will be closer than 30 frames apart. MAXFRAME = The maximum distance between peaks of our undulating envelope. For example, a value of 50 here means that no two high points (or two low point for that matter) will be farther than 50 frames apart. NOTE: If you want the envelope key frames to step forward evenly, say every 40 frames between peaks, then MINFRAME and MAXFRAME should be equal (i.e. 40 in our example above). Having these two values random means the intensities vary from high to low over a random time frame, giving them a more organic and natural feel. ENDFRAME = The length in frames of our envelope. Because we are generating our steps between key frames, this value is a minimum guideline only. For example, a value of 1000 means the last key frame of the envelope will certainly not be less than 1000, but it might be slightly higher. LOWMIN = This value determines the lowest intensity of our envelope. LOWMAX = This value determines the maximum range of the lowest intensities. NOTE: If LOWMIN is 0 and LOWMAX is 10, the valleys (or low key frames) will never be lower than 0 and never higher than 10. If LOWMIN and LOWMAX are equal (for example, both are 0), then there will be no variation of the low values (i.e. the low key frames will always be 0). Again, having these values different makes our lights dim in a more unpredictable fashion. UPPERMIN = This value determines the minimum range of our highest intensities. UPPERMAX = This value determines the highest intensity of our envelope. NOTE: Similar to LOWMIN and LOWMAX, UPPERMIN and UPPERMAX give us a range for the peaks (or maximum values) of our intensity envelope. The peak values will never be higher than UPPERMAX (i.e. 40) and never lower than UPPERMIN (i.e. 20). ENVELOPES = The total number of Envelopes you want to generate using these settings. Lines 27-35 Once we have set up the requesters, we usually need to send them some default values. Note that we CALL the REQ_SETVAL() function for each requester to do this. The first argument is the ID name of the requester (for example, RampID). The second is the actual value (for example, a 1 or 0 for the Boolean ON or OFF). The rest of the functions and there default values should be self-evident. Line 37 The function REQ_POST() sets up the requester with the Okay, Cancel, and Reset options already enabled. It waits for the user to input everything and select either Okay or Cancel. Reset automatically clears all variables. If okay is pressed, then X is set to 1 by this function. If Cancel is pressed, then X is set to 0. Line 39 A standard IF - THEN conditional statement. If X=1 (therefore we know Okay was pressed) then DO everything that follows. If this check was failed (therefore, Cancel must have been pressed and X must be 0) then the program drops to the END statement that matches this IF statement (i.e. line 130). The next command after that is EXIT and so the program ends. With just those few lines, we set up an entire program front-end, prompted the user for lots of input, with default values given as examples, and waited for the user to either abort the program, reset the values, or tell us to proceed! The Modeler and ARexx do all the work for us and our interface is guaranteed to behave and look professional like all of the other Macros written for the Modeler. Lines 40-48 Now that the user has told us to begin, we need to read back in the values he or she entered using the REQ_GETVAL() function. The only argument we pass to the function is the name of our ID (for example, EndFrameID). We then assign the value returned by the function to a variable (for example, ENDFRAME). We do the exact same thing for all of our requesters. Lines 51-55 We cannot always be sure our users give us exactly what we want. To avoid generating an error later on in the program, we check to make sure that the LOWMIN value is indeed less than the LOWMAX value. From the user's stand point it doesn't matter, as long as the range is between two values, but internally our own functions require these two values to be ordered correctly. First we check to see IF LOWMIN is greater than LOWMAX. If it is, then we DO the next series of commands. If it isn't then we skip to the END of the IF statement (i.e. line 55) and proceed on with the program. Assuming our user entered in a LOWMIN value that was greater than the LOWMAX value, we need to switch these around before proceeding. First we assign the LOWMIN value to a temporary variable called TEMP. Now that is is saved for later, we set LOWMIN equal to LOWMAX, and then assign LOWMAX the old LOWMIN value stored in TEMP. The old variable switcheroo is a classic programming example. Now we can proceed on with our program Lines 57-61 These lines perform the exact same function as Lines 51-55 above, except they assure than HIGHMIN is always less than HIGHMAX. Line 63-67 These lines again perform the same function as the previous two examples, but instead insure than our MINFRAME offset value is less than our MAXFRAME offset value. Lines 69-70 Now that we are sure all our data is useful, we need to prompt the user for some additional input. We use the GETFILENAME() function to get a path and root file name for the envelope(s) we are about to create. The first argument passed to the function is a title for the requester, while the second is a default path, which I chose to be LightWave's Envelopes drawer. As before in our program, the Modeler does all the work with the requester in one line! The entire path and filename is returned to the program and we assign that to a variable called ROOT_NAME. We then check to see IF ROOT_NAME equals "(none)", which is the function's message for "no response." If it is then we EXIT the program, otherwise we proceed with our program. Lines 72-73 Using the GETFILENAME() function, we can ask our user to input a path and a root name for our envelope(s). The first argument passed to the funtion lets us name our requester for the benefit of our user. The second argument lets us automatically log in to a default path (you will need to change this to suit your needs). The second line checks to see if the user failed to enter anything or selected abort, in which case the requester returns the string "(none)" and therefore we exit the program. Line 76 Here begins the main body of our program. This first line begins a loop (called a DO loop) where we will step the variable N from 1 to the variable ENVELOPES which our user entered earlier (Default is 1). All of the program between this line and the matching END statement for this DO loop (i.e. line 129) will be run again and again until N = ENVELOPES, at which point the program drops to line 130, which ENDS the main IF conditional, and then EXITs the program. Lines 77-80 Users who have previously programmed will recognize the following lines as one of those annoying little file-management necessities. Since we might be creating hundreds of envelopes, we can give them all unique names by appending a number to the end of the root name. However, when you append the number 2 directly to a filename (i.e. Random.2) and a 10 to that same root name (i.e. Random.10) programs and requesters that sort those values return them in the wrong order (i.e. Random.10, Random.2). What we really need is to 'pad' out the number with leading zeros (i.e. Random.002, Random.010, Random.100). The first line sets our PAD variable to a null string "", with nothing in it. Then we check to see if N, our envelope number, is less than 100. If it is, add a "0" to our PAD string. Next, if N is less than 10, we need to add another "0" to our PAD string. Finally, our FILE_NAME is set to equal our ROOT_NAME combined with our PAD string and finally combined with N, our envelope number. NOTE that strings are combined, or concatenated in programmer's parlance, with the || characters (i.e. strike shift '\' twice). Lines 82-85 We are now ready to open our new envelope file! We call the ARexx OPEN() function to do this. The first argument (i.e. 'out') is the name our program uses to address this file. It can be anything we want, but make sure that each file you have open at the same time has a unique name, to keep from getting the open files confused). The second argument is the actual AmigaDos path and file name of the file we are opening (i.e. the variable FILE_NAME we just created above). The third argument 'W' tells the OPEN() function that we want to Write to this file. To read from a file use the 'F' argument. Now that are file is created and open, we send it some initializing lines that LightWave requires to recognize this text file as a legitimate LightWave envelope file). The WRITELN() function sends a complete line of text to our file. We call the function with the ARexx file name of our file (i.e. 'out') as the first argument, and the actual text we want to write out (i.e. 'LWEN') and the second argument. The next two lines write additional data required by LightWave. Do not change any of these three lines or LightWave will not recognize your file! Line 87 This line initializes our LASTFRAME variable to 0 each time we begin writing a new envelope. The LASTFRAME variable is used to keep track of the last frame number we have processed during our program. Line 88 This line initializes our K variable to 1 each time we begin writing a new envelope. K is used to keep track of the actual number of key frames we have created in a given envelope. Line 89 We need to decide whether our envelope begins within a valley or a peak (i.e. using LowMin/LowMax or UpperMin/UpperMax as values). We could have just picked a value, either 0 for valley or 1 for peak for all of our envelopes, but again I chose to make the results as random as possible. My variable MINMAX is going to alternate between 0 and 1 throughout the program, so I want it to start with a random value between 0 and 1. I assign MINMAX the result of the ARexx RANDOM() function, using the arguments 0 and 1 as the minimum and maximum values for the function. The result of the RANDOM() function is always an integer, so it can only return a 0 or a 1. Line 91 Before we begin our loop to generate random values and key frames (i.e. line 92), we are going to get a little fancy and take advantage of one of Modeler's built-in interface features, the progress meter! We call the METER_BEGIN() function with two arguments. The first one is the number of steps we expect our meter to be updated by. For example, if we knew we were going to be processing 10 key frames, we might send a value of 10 here, so that we'd see the meter update 10 times during our upcoming loop. The second argument is the name or title that we pick for the meter requester. Our only problem here is that we don't really know exactly how many key frames we are going to create because the offsets between them are generated randomly (see below). Since the meter will error if we step it too many times, but not if we step it too few, it is better to err on the side of caution. We know that the minimum distance between key frames is the variable MINFRAME and the maximum frame count of our envelope is the variable ENDFRAMES, so the maximum number of key frames we could have in our envelope is ENDFRAMES%MINFRAME (remember % is just an INTEGER divide). We pass that function as the first argument to our meter function. Line 92 Now we are ready to begin generating the key frames and their intensities one after another, until we reach or exceed the EndFrame value of our program. Our control for this DO loop is the UNTIL keyword and LASTFRAME > ENDFRAMES condition. In other words, the program lines contained with the DO loop and its accompanying END statement (i.e. line 102) will be repeated forever until the value of the LASTFRAME variable exceeds the value of the ENDFRAMES variable. Line 94 If our MINMAX variable = 0 then we have a valley and our INTENSITY should be based on the RANDOM() value between LOWMIN and LOWMAX. Otherwise, proceed to the next line of our program. Line 95 If our MINMAX variable = 1 then we have a peak and our INTENSITY should be based on the RANDOM() value between UPPERMIN and UPPERMAX. Otherwise, proceed to the next line of our program. By this time, we have covered both of the possible values of MINMAX, 0 and 1. Line 96 The actual frame number for the above INTENSITY variable is determined by adding a RANDOM() frame offset between MINFRAME and MAXFRAME to the LASTKEY value of the previously created key frame. In this fashion, our key frames keep increasing one after another by a random amount. NOTE that we are creating an ARRAY of the variables INTENSITY and FRAME by creating variables with the ".k" suffix. For example, if K is 56, then we are working with INTENSITY.56 and FRAME.56. Each of these variables is unique and can have their own distinct values. ARexx automatically sets up variable space for our array as we go! This is usually a BIG pain in most programming languages. Line 97 There is only one instance when we know what the frame value of a given key frame before hand. The first key frame (i.e. K=1) is always 0. In this line we check to see if K=1, and if it is we make the FRAME value equal to 0 automatically. Line 98 We update the LASTKEY to our current FRAME value, so that next time around we build a new key frame starting on the value of this one. Line 99 We add 1 to K, which keeps internal track of our total number of created key frames so far. Line 100 Through a clever and standard programmer's trick, we force MINMAX to constantly toggle between 0 and 1 every time we pass through this program loop by subtracting MINMAX from 1 and then assigning the result to MINMAX again. If MINMAX was 0 before then 1 - 0 = 1, so MINMAX becomes 1. If MINMAX was 1 before then 1 - 1 = 0, so MINMAX now becomes 0 the next time around. Line 101 To update our meter to show we have proceeded one time through our loop, we call the METER_STEP() function. There are no arguments as Modeler again takes care of all its own housekeeping. Line 102 The END of this DO loop. The program now checks to see if the updated LASTFRAME value is greater than the ENDFRAMES value (see Line 92). If it isn't we start this loop again, creating more random intensities, offsetting new frame numbers, toggling MINMAX, increasing our key frame total, K. If it finally is, we drop to the next line of our program. Line 103 Now that we have completed all of our calculations, it is time to close that requester we created in Line 91 that we've been stepping through during that last loop. We CALL the METER_END() function with no arguments to do this. Line 106 We automatically added 1 to K at the end of that last loop, but since LASTFRAME has to be greater than ENDFRAMES by this point in the program, there never were any values assigned for INTENSITY and FRAME for this last value of K. Since this is our total number of key frames pointer, we should make sure this is accurate by subtracting 1 from the final value of K. Everything matches up now. Line 107 Remember that envelope file we opened up and initialized way back at lines 82-85? Well, LightWave requires one more thing before it starts reading all your intensity and key frame values...the total number of key frames in the envelope - our variable K. Good thing we kept track of that, huh? We call the WRITELN() function and send the value of K as the second argument to the program file identified as 'out' by us. You should be getting use to the WRITELN() function by now. Line 109 Because of a feature I added in later, I have to run our intensities through some more calculations if the user has requested us to ramp the intensities down to our LOWMIN value across the envelope. Since we did not know the final key frame until after everything was calculated, we could not perform this function until now. We begin with a simple IF statement that checks to see if RAMP=1, meaning 'yes, ramp the values'. If it is, we do everything between lines 110 and 115. If not, we drop through to the END statement on line 116 and proceed with the program. Note that ARexx assumes the line IF RAMP THEN DO means if RAMP is BOOLEAN TRUE (numerical shortcut is 1). You do not need to expressly say IF RAMP=1 THEN DO. Notice that we could change line 37 to read IF X THEN as well, but I wanted to give you an example of both for this programming tutorial. Notice how well Boolean logic works with Boolean requester function like the RampID. Line 110 Let's start a new meter for our ramp operation by calling the METER_BEGIN() function like we did in line 91 earlier. However, since we now know the total number of key frames we have to process (i.e. K), we can tell the meter that we are sure to step K times through the meter as our first argument. Line 111 We set up a DO loop that will iterate the variable 'I' starting from 1 and ending with K. The END of this loop is on line 114. Line 112 Now we are going to Ramp our intensities lower until the final key frame is always equal to LOWMIN. Our formula does the following: A) First it subtracts the LOWMIN value from the previous INTENSITY value so we are only going to scale in the range greater than LOWMIN. B) Then we calculate the percentage of ramping by dividing the current FRAME value by the maximum frame value represented by LASTFRAME value. This value will range between 0 (at the beginning) and 1 (at the end) during the envelope. C) Since we want to ramp DOWN, we subtract the result from B) from 1 so that the percentage is reversed. D) We then multiply the percentage from C) times the adjusted FRAME value. E) Finally, we add LOWMIN again so that everything is shifted back the way we started and reassign the new value to FRAME. Can you figure out how to ramp up to UPPERMAX across the envelope? This is our only nasty formula requiring some genuine math logic. Sorry about that. Line 113 We tell the progress meter to step forward since we've completed one key frame by calling the METER_STEP() function as before. Line 114 END the loop and step forward until it reaches the final value of K. Line 115 We are done with the ramp progress meter by this point in the program so close the meter with a CALL METER_END() function. Line 116 END of the conditional IF statement that started this whole ramping mess. On with the rest of the program. Line 119 Let's open one final progress meter to begin wrapping up this envelope. Again we'll step K times through the meter. Line 120 We're going to loop from 1 to K again, with a DO loop like above. This loop ENDs on Line 125. Line 121 By calling the WRITELN() function again, we write our INTENSITY values to the 'out' file. We divide the INTENSITY by 100 because that's how LightWave likes it. Line 122 The WRITELN() function sends the FRAME and preset spline control settings (i.e. "0.0 0.0 0.0" is Tension Continuity Bias) to our 'out' file. You should now be able to figure a way for the user to set default or random spline settings in your version of the program. Line 123 Let's step that meter now that we've written out another key frame. Line 125 END of the DO loop in line 120 that sends us back to the next I value until it reaches K. Line 126 We have finished stepping our meter, so let's close it out. Line 127 We CALL the CLOSE() function to close the 'out' file we've been creating all this time. It is now complete and can be loaded in LightWave's intensity envelope editor. Line 129 This is the END of the DO loop in line 76. We'll start creating a new envelope until N reaches the value of ENVELOPES at which point we will go to the next line of the program Line 130 This is the END of our conditional IF that started this whole process. The use has either pressed Cancel or all of the envelopes have been generated. In either case, proceed to the next line of the program. Line 131 EXIT the program and return Modeler control to the user. END OF PROGRAM WHEW!!! Hopefully, this showed you enough of the basic Modeler macro programming functions and philosophy to get you started. In your Toaster/Arexx_Examples/LWM directory there is a text file called ModelerARexx.doc. Everything else you need to know to program the Modeler can be found there and most of it shouldn't look like APL to you after this example. For examples, just hunt and peck through the abundance of Arexx macros until you find something you need. Before wrapping this example up, I'd like to thank Stuart Ferguson for adding these features to the new Modeler. Maybe next program I'll actually use more of the Modeler's features than just its interface handling! I have some old celestial mechanics code I did in high school that's just dying for a rewrite. I would also like to thank Arnie Cachelin of NewTek for his abundance of Modeler ARexx macros I stole from heavily to produce this program. I can only hope you readers do the same from me.