Part II. Sequencing with scripts
By Ralf Flicker.Prerequisites: This part of the tutorial requires the reader to be familiar with the Lua script extension for Oxidizer. For some of the scripts presented below, the utils.lua and mods.lua script packages are required.
Disclaimer: The scripts employed in this article only reflect the contemporary efforts of the author, and are in no way to be understood as definitive or the only way of doing things. The reader is positively encouraged to experiment and come up with their own preferred methods, and to write much better scripts than those presented below.
Update (8 May 2009): The sequencing script described below has received an update, and is documented and demonstrated here: algorhythm.
Overview
Morphing and key frame animation is straightforward, and a good enough approach for short morph sequences and when you don't care precisely how flam3 interpolates between the key frames. It is a less viable approach, however, when:
- you want a morph to follow a specific path
- you want to combine morphing with other types of modulations
- you have long or complicated sequences with many key frames
Obviously, if you want the morphing to be done in a specific way, you have no way of telling flam3 to do this, so we must instead calculate every time step of each genome morph ourselves. Also, if you have been doing everything manually so far, inserting key frames and adjusting parameters etc - at some point in the growing length of the sequence this procedure simply becomes too cumbersome and tedious to carry out by hand. This kind of work is suitable for doing with a script, for efficiency and repeatability (at least for the time being; the day Oxidizer implements an Animation Sequencer GUI all this will become obsolete). Here are a few examples of animations that you would do with a sequencing script:
- beat-syncing an animation to music
- "storyboard" sequences of many separate steps
- randomized looping modulations
- random walk morphing between genomes
There are many ways one could go about these things, and the scripts presented below are merely offered as examples of the types of methods that can be employed. One method of key frame authoring for an interpolating script will be the topic of the final section. This code has gradually grown out of a series of more specialized, non-interpolating animation sequencers that were designed to do just one specific thing, for instance Xform rotations. It may be of interest to the reader to also look at some of these earlier non-interpolating codes first, which is the topic of the next section.
Non-interpolating sequencing
For specific tasks of a limited scope, we may be able to formulate a simple algorithmic description of the motion, for instance: "rotate by 360 degrees over 60 frames," or "modulate the variation weight by a cosine over 100 frames." For such modest sequences, it is a simple matter write a script that generates a sequence of genomes tracing out the motion. Two examples are given below, that apply various modulations to the first genome in the Oxidizer main list (to remind yourself of the Oxidizer Lua implementation, see the scripting tutorial). The first example simply zooms by a factor of two (from -1 to +1) and rotates the image by 200 degrees. This could have been done faster by a simple morph directly in Oxidizer, but just for illustrating the principle. The second example uses the same principle but calculates a full Xform (looping over all xforms) and color map rotation by calling the functions affine_Prot and cmap_rot. This sequence will loop around when animated.
Example 1: simple parameter sequencing
- luadir = "/path-to-utils-and-mods-folder/" -- change to actual path
- dofile(luadir.."utils.lua")
- nframes = 100 -- number of frames
- ang = 200 -- amount of rotation (in degrees)
- zoom = 2 -- amount of zoom
- da = ang/nframes -- delta-angle per frame
- dz = zoom/nframes -- delta-zoom per frame
- genome = oxidizer_genomes[1]
- new_genomes = {}
- for i=0,nframes do -- Loop over frames
- new_genome = clone_genome(genome) -- copy original genome
- new_genome.time = i+genome.time
- new_genome.zoom = genome.zoom-1+i*dz
- new_genome.rotate = genome.rotate+i*da
- new_genomes[i+1] = new_genome
- end
- oxidizer_genomes = new_genomes -- update Oxidizer genome
- oxidizer_status["action"] = "replace"
Example 2: Xform and color rotations
- luadir = "/path-to-utils-and-mods-folder/" -- change to actual path
- dofile(luadir.."mods.lua")
- dofile(luadir.."utils.lua")
- nframes = 180 -- number of frames
- da = 360/nframes -- delta-angle per frame
- dc = 256/nframes -- delta-color per frame
- genome = oxidizer_genomes[1]
- nx = #genome.xforms
- new_genomes = {}
- for i=0,nframes-1 do
- new_genome = clone_genome(genome)
- new_genome.time = i+genome.time
- for x=1,nx do affine_Prot(new_genome,{x,"a"},i*da) end
- cmap_rot(new_genome,new_genome.colors,dc*i)
- new_genomes[i+1] = new_genome
- end
- oxidizer_genomes = new_genomes
- oxidizer_status["action"] = "replace"
The arguments of the affine_Prot function on line 14 in example 2 has a special structure that will be important later on. The second element of the second argument {x,"v"} is a modulation mode selector, which tells the function how to apply the numeric value given in the third argument to the function. This argument takes three different values:
"v" value: set the genome parameter to this value
"a" additive: add this number to the current parameter value
"m" multiplicative: multiply the current value by this number
"a" additive: add this number to the current parameter value
"m" multiplicative: multiply the current value by this number
These modes offer a wide range of possibilities for different types of parameter modulations, and all the Xform related functions in the mods.lua package have this structure (the morphing function morph and the color adjustment functions, e.g., cmap_rot and cmod_hsl, work differently). We could also sequence a genome morph between different genomes in this type of script, by using a Lua morphing function, although we pay the price of not being able to exploit all the special tricks that are coded into flam3, and which can not be reproduced on the scripting level. Consequently, the Lua morph function (in the utils package) only interpolates the continuous genome parameters, and currently does nothing with discrete parameters like genome symmetry and final xform. But if you have no jumps in the discrete parameters, the benefit of using the Lua morph function instead of letting flam3 do the genome interpolation is that you can now add other parameter modulations on the scripting level, simultaneously with the genome morph. We will see some examples of this in the next section.
To introduce the terminology of the next section, we shall henceforth think of the genome parameters being actively modified by the script as "modulation channels," and the starting and stopping configurations as "control points" (essentially key frames, but with a more general role than in Part I). The above script examples show how to easily script a frame sequencer for simple morphs and loops, where the modulation channels and control points are hard-coded into a FOR loop. But if we want to have many more control points for a longer animation sequence, all of which might address different modulation channels and be of different lengths, etc. - then doing it this way will be just as much work as doing key frame authoring by hand in Oxidizer. We need a more efficient interface for authoring movie sequences without being troubled by all the details that goes on under the hood.
Interpolating sequencer
One script that goes some way towards such a simplified interface goes by the unglamorous name seq9.lua. This code is a parsing and interpolation engine that will do all the heavy lifting, while the movie director can focus on authoring "control scripts" for it. The technical details of this code is documented on the seq7/seq9 home page, which also contains several examples of animations produced with it.
It must be emphasized that while I offer these scripts as examples of tools that facilitate animation authoring, they are by no means trivial to use, and a modicum of manual scripting is still required. Those of you who have worked with audio or video sequencers like Logic or Final Cut will know what I am talking about: the timing, shaping, blending and cross-fading of events and segments - even with scripts in Oxidizer, we can not escape the job of authoring the events of the sequence in some abbreviated form like this.
Having said all that, let's look at what the control scripts for the two examples given previously would look like:
Example 1 as a control script for seq9
mdef = { -- modulation channels
[1] = {"zoom","v"},
[2] = {"rotate","a"}
}
cp = {} -- control points
cp[1] = {
[1] = {0, -1},
[2] = {100, 1}}
cp[2] = {
[1] = {0, 0},
[2] = {100, 200}}
[1] = {"zoom","v"},
[2] = {"rotate","a"}
}
cp = {} -- control points
cp[1] = {
[1] = {0, -1},
[2] = {100, 1}}
cp[2] = {
[1] = {0, 0},
[2] = {100, 200}}
Example 2 as a control script for seq9
nx = #oxidizer_genomes[1].xforms
mdef,cp = {},{}
mdef[1] = {cmap_rot,genome.colors}
for i=1,nx do mdef[i+1] = {affine_Prot,{i,"a"}} end
cp[1] = {
[1] = {0, 0},
[2] = {180, 256}}
for i=1,nx do
cp[i+1] = {
[1] = {0, 0},
[2] = {180, 360}}
end
mdef,cp = {},{}
mdef[1] = {cmap_rot,genome.colors}
for i=1,nx do mdef[i+1] = {affine_Prot,{i,"a"}} end
cp[1] = {
[1] = {0, 0},
[2] = {180, 256}}
for i=1,nx do
cp[i+1] = {
[1] = {0, 0},
[2] = {180, 360}}
end
seq9 file paths and control script
In order to test these scripts, you need to edit seq9 to set up the file paths and file names for the external packages and the input control script, as indicated on lines 22-26 in the box on the right. In this setup, mdir must be the full path to the folder containing the utils and mods packages, and infile must be the full path to the control script file, but otherwise you may rearrange things to better reflect your directory structure (for instance, you do not have to use the intermediate luadir, sdir and qfile variables if you don't want to).- luadir = "/Users/rflicker/Documents/Oxidizer/" -- change to your dir
- mdir = luadir.."scripts_dev/" -- my utils and mods directory
- sdir = luadir.."scripts_seq/" -- my control script directory
- qfile = "cs1.lua" -- control script file name
- infile = sdir..qfile -- full path to control script
Looking at the example control scripts above, we see that the animated parameters are defined in the table mdef (modulations definition), and the key frames are defined in the table cp (control points). A modulation channel is a table containing the name of the genome parameter or a function, followed by another table containing any required arguments (which vary between different functions) and the mode selector already encountered. The element cp[i] defines the control point sequence for the genome parameter with the same-numbered modulation channel definition mdef[i]. Each control point is a table of time-value pairs, with optional curve and interpolation keywords (discussed below). These are the basics of a control scripts for seq9.
Compared to the non-interpolating formulation in the previous section, what was gained by all this? For one thing, adding more modulation channels and new movie segments is as easy as inserting additional table elements in the mdef and cp tables. For repeating segments (for instance, for beat-syncing) it is also easy to use FOR loops over control points to define repeating modulations of indefinite length (which may also include a random element, to make them less predictable). Lastly, the interface is substantially cleaner, giving an easier overview of the sequence of control points and the parameters being modulated.
The mdef and cp tables are required objects within a control script. Two more optional tables that seq9 will parse if presented with are the morph sequence mseq and global environment genv tables. The morph sequence table is required if the morph function is specified as a modulation channel. If no morph sequence is present, seq9 will by default apply its modulations to the first genome in Oxidizer, that is, oxidizer_genomes[1]. The global environment parameters are just static adjustments that are not animated; for instance, in case some of the genomes of the sequence do not have consistent rendering parameters, you can make sure from the control script that they are all set consistently. This also makes it easy to alternate between generating preview sequences and final rendering sequences, by switching off density estimation in the former.
Morphing, curves and interpolation
Example 3: morphing with seq9
To see an example of morphing, and to introduce curves and interpolation, let's look at the control script in the box on the right, Example 3. This script morphs consecutively between four genomes using the morph Lua function mentioned previously, while simultaneously applying XForm rotations and other things. This marks the difference between seq7 and seq9: the former hands over the morphing to flam3, which disables the scripting action for the duration of the morph, while seq9 sequences the genome morph from the scripting level. These two implementations were so different that it broke the code into two versions, which I have opted to kept as separate scripts. The ability in seq9 to combine curves with morphing on the scripting level means that you can make smooth, flowing morphs even where flam3 would default to linear, like when you don't have enough boundary points for Catmull-Rom interpolation. The following video clip was produced by applying the control script in Example 3 to the same genome that was sequenced with Oxidizer/flam3 in Part I.- nf = 80 -- frames per morph
- mdef,cp = {},{}
- mdef[1] = {morph,{"pol","hsl",0}} -- 0=alternating polarity, 1=fixed
- mdef[6] = {cmod_hsl,"s"}
- mdef[2] = {affine_Prot,{2,"a"}}
- mdef[3] = {affine_Prot,{4,"a"}}
- mdef[4] = {affine_Orot,{3,"a"}}
- --mdef[5] = {affine_Prot,{1,"a"}}
- mdef[7] = {affine_Prot,{3,"a"}}
- mseq = {
- [1] = {0, nf, 1, 2},
- [2] = {nf, nf, 2, 3},
- [3] = {2*nf, nf, 3, 4}}
- mc,mv = "exp",-1
- mi,mt = "smooth",0
- cp[1] = {
- [1] = {0, 0, curve=mc, a=mv, inter=mi, t=mt},
- [2] = {nf, 1, curve=mc, a=mv, inter=mi, t=mt},
- [3] = {2*nf, 0, curve=mc, a=mv, inter=mi, t=mt},
- [4] = {3*nf, 1, curve=mc, a=mv, inter=mi, t=mt},
- [5] = {3.5*nf, 1, curve=mc, a=mv, inter=mi, t=mt}}
- nt = 3.5*nf
- cp[6] = {
- [1] = {0, 1.25, curve="lin", a=1},
- [2] = {nt, 1.25, curve="lin", a=1}}
- --cp[5] = {
- -- [1] = {0, 0, curve="tanh", a=1.5},
- -- [2] = {nt, 720, curve="tanh", a=1.5}}
- cp[2] = {
- [1] = {0, 0, curve="tanh", a=1.5},
- [2] = {nt, 720, curve="tanh", a=1.5}}
- cp[3] = {
- [1] = {0, 0, curve="tanh", a=1.5},
- [2] = {nt, -720, curve="tanh", a=1.5}}
- cp[4] = {
- [1] = {0, 0, curve="tanh", a=1.5},
- [2] = {nt, 720, curve="tanh", a=1.5}}
- cp[7] = {
- [1] = {0, 0, curve="tanh", a=1},
- [2] = {2.5*nf, 600, curve="tanh", a=1},
- [3] = {nt, 400, curve="tanh", a=1}}
- genv = {
- supersample = 3,
- filter_shape = "Gaussian",
- filter = 0.6,
- quality = 1,
- estimator_radius = 4,
- estimator_curve = 0.8,
- estimator_minimum = 0,
- temporal_samples = 60,
- temporal_filter_type = "Exponent",
- temporal_filter_exp = 4,
- interpolation = "Linear",
- interpolation_type = "Linear"}
| The genome (sequence4.flam3) from Part I sequenced with seq9. |
The elements of the morphing sequence mseq in seq9 have the following definition:
{start_time, morph_length, source_genome, target_genome}
with one such table per morphing instance (seq7 has a different definition). We also see a first example of the curve and interpolation keywords being present in the cp table. The complete specification of a control point element is the table:
{time, value, curve=, a=, b=, inter=, t=}
where curve is a string specifying the type of curve to use (by default linear, if this keyword is omitted), and a and b are adjustable parameters of the curve. The interpolation key inter can be set to either smooth or linear (default linear), and t is the tension parameter of the Catmull-Rom spline discussed in Part I. Currently available curves are exp, tanh, sinh, cos, cos2, sina and cosa. All curves take the parameter a, and sina and cosa also take the second parameter b. Plots of the curves are shown on the seq7 home page. Lastly, curves and smooth interpolation can be used independently, but also combined together, as illustrated in the interpolation section.
Some more comments on the control script in Example 3 and features of seq9 (see also the mods.lua and utils.lua packages for more details):
- (line 4) morph takes three arguments specifying the spatial interpolation ("pol" or "lin"), color interpolation ("hsl" or "rgb") and polarity, which can be either 1 (fixed polarity) or 0 (alternating). The "distance" argument to the morph function is normalized to [0,1], where 0 means the source genome and 1 means the target genome. Under alternating polarity, these roles will invert after each morph.
- (line 5) cmod_hsl can do either hue, saturation or lightness correction by giving it the argument "h","s" or "l". Their value modes are hardwired as follows: hue is additive, saturation and lightness are multiplicative. By ramping the lightness you can implement cheap fade-to-black intros/outros, if you don't have a video editor.
- one could have set the mdef and cp definitions for affine_prot with a FOR loop just like in Example 2, if all channels were doing the same thing. One reason to define them separately, however, is that:
- (line 9, 32-34) you can then easily comment out channels, and insert them back in again. The channel definitions do not have to be consecutively numbered (you do not even have to start at 1 if you don't want to).
- (line 17-18) the morph here uses a combination of an exponential curve with a=-1 and smoothing with t=0.
- (line 28-30) the saturation modulation is here just a constant adjustment by +25%, but it could also be varying or peaked (e.g. using the cos2 curve) to compensate for some particular genome with washed out colors compared to the rest.
I hope the above discussion, along with the example control scripts, have given you a flavor of one possible way to compose animation sequences via a scripting interface. Listing all the details and caveats of this particular code (seq7/seq9) is beyond the scope of this brief tutorial; for more information, the interested reader is referred to the seq7/seq9 home page, and the doq7.lua example control script (for seq7). See also the scripts page for more example scripts relevant to this tutorial. I leave you with a video clip of a random-walk beat-syncing experiment that was sequenced with seq7, enjoy!
| Random-walk beat-syncing with seq7. |
There are 2 comments on this page. [Display comments]