Scripting with Oxidizer

By Ralf Flicker.

Oxidizer uses Lua as an extension language for working with genomes and doing rendering jobs. It is a powerful addition to Oxidizer that allows the user to create new tools that the Oxidizer developer has not implemented as features of the application. The Scripts Page collects scripts that are part of the tutorials on this wiki, and also scripts submitted by users.

An 18-page scripting tutorial (PDF) for Oxidizer written in late 2008 serves as the starting point for the current wiki page. This tutorial contained in addition to the documentation example genomes and example scripts, and the whole package can be downloaded here: scripting_tutorial.zip.

Important to note: The "utils.lua" file that comes with this package is an older version than the one provided on the scripts page, which has been substantially updated for the new animation scripts after this tutorial was written. If you plan to work through the examples in the older scripting tutorial, then this older version of utils.lua is what you must use.

Below, chapters 0-2 of the previous tutorial have been compressed into a condensed introduction, and chapters 3-5 with some corrections and extensions constitute the wiki tutorial. The second part of the original chapter 5 dealt with animation topics, which you can now also explore over at the animation tutorial (Part II). A final note: this part of the tutorial does not deal with object methods enabled by the ObjC-Lua bridge, but sticks to a more old-fashioned (but probably simpler, for non-experts) style of modular programming. A section on object methods may be included later on.

Introduction


scripts.png
Figure 1. Integrated script launcher in Oxidizer 0.5.9.
This tutorial is intended for absolute beginners with no prior experience with Lua scripting, although a modicum of familiarity with Oxidizer is assumed. To run a Lua script with Oxidizer, you need the Oxidizer application and a Lua script. To write a script, you need a text editor. To debug a script, you may optionally want a console to see the Lua output. For testing Lua commands you may optionally want a stand-alone Lua installation. The fully-loaded setup includes all of the above, plus downloading the Oxidizer source via CVS and running Oxidizer via Xcode (mainly for having a better console).

You can run a Lua script in Oxidizer either via the "Scripts" pull-down menu, clicking the "Lua Script" toolbar icon, or using the integrated script launcher/library (my favorite choice) shown in Fig. 1. A useful keyboard shortcut is ⇧⌘L for re-running the last Lua script, for the times when you are running the same randomizing or sequencing script (or whatever you're doing) over and over again. In this basic tutorial we will learn that information is passed between Oxidizer and the Lua environment in the form of two Lua tables named

oxidizer_genomes : contains all the information about the genomes present in the Oxidizer main list (but not the Breeder or the Gene Pool)
oxidizer_status : contains action instructions and status messages.

The oxidizer_status table only has two elements, and they are:

oxidizer_status["action"]
oxidizer_status["message"]

They both take a text string as value. The message element holds the text message that you want Oxidizer to display. The possible values of the action element include:

"not_set", "replace", "append"
"error", "warning", "message"

There are currently two types of actions: to have Oxidizer display informational messages on screen (the "error", "warning" and "message" actions), and to tell Oxidizer how to update its genome list with what Lua has produced (the "replace" and "append" actions). That’s all we need to know about oxidizer_status for the moment; the rest of this tutorial will be all about oxidizer_genomes.

Lua primer


To start with, here are some useful Lua programming resources, manuals and tutorials:

Lua - Wikipedia (short overview)
Lua 5.1 Reference Manual (the whole book available online)
Lua Short Reference (PDF document - great quick reference)
Tutorial Directory (good place for beginners to start learning, lots of examples)

This section will present some generic Lua examples that do not make much sense to put in a script and run via Oxidizer. To test these examples, the reader is encouraged to install a stand-alone version of Lua (go to Sect. 7 to learn about this). Otherwise, you just have to look at the examples and learn the rules by inspection (and keep the Lua Short Reference handy).

Some general info


Tables

Lua tables are a big topic. I can only cover the basics that you should have in order to be able to work smoothly with Oxidizer genomes. Have a look at the Lua Tables Tutorial or the Lua Short Reference for some examples. A table is a data type that holds other objects, such as numbers, strings, functions, or other tables. Table elements are defined by key-value pairs, and you access the value by specifying its key, which can be either a string or a number. This is a very flexible formalism, but one drawback is that you can not do mathematical operations directly on a table. To do math on numbers in a table it has to be done element-wise. Below are listed some basic features of tables.

A table is created by the curly brackets constructor {}. An empty table t is created by

t = {}

Table elements are addressed by square brackets []. The pound sign # applied to a table returns the number of top-level elements of the table, for instance:

t = {2,5,10}        -- define a 3-element vector as a table t
print(#t,t[3])      -- print the size of t, and its third element
3   10              -- Lua output

There are often multiple ways of doing something with a table. The following five examples produce exactly the same table (reusing the definitions of a and b given in the first example):

a=math.pi b=math.exp(1)         -- define variables a and b

t = {{2,5,10},{a,b},"Lua is fun"}   -- define table without keys

t = {[1] = {2,5,10},                -- same table with keys (indices) given explicitly
     [2] = {a,b},
     [3] = "Lua is fun"}

Table keys can be given as names or just indices: in the second example above the element indices [1], [2] and [3] are given as keys. In this case, this adds no information and could have been omitted, as it was in the first example. In the following example (still the same table) defining the keys as indices matter, because the table is first defined sparsely, omitting the second element, which is inserted afterward:

t = {[1] = {2,5,10},        -- create table with elements 1 and 3
     [3] = "Lua is fun"}
t[2] = {a,b}                -- define new element at position 2

In the last three examples t is a multi-dimensional table; for instance, the number 10 is now the third element of the first element of t:

print(t[1][3])  -- one more square bracket for each dimension
10              -- Lua output

The table.insert function from the table library can be used for inserting new elements into an existing table, as illustrated in these two examples:

t = {{2,5,10},{a,b}}
table.insert(t, "Lua is fun")       -- inserts element at the end of the table

t = {[1] = {2,5,10},
     [3] = "Lua is fun"}
table.insert(t,2,{a,b})             -- insert new element at position 2

Instead of indexing by numbers, table keys can also be named, as in the following Oxidizer-oriented example:

t = {quality = 50,
     vibrancy = 1.25,
     background = {red=0,blue=0,green=0},
     filter_shape = "Gaussian"}

For table elements with named keys, their values can be addressed in two ways, using square brackets around a string or with a period punctuation mark (or "dot" formalism). So for instance t["quality"] is equivalent to t.quality, and setting the background color red to 150 can be done by any of the equivalent statements:

t.background.red = 150     
t.background["red"] = 150
t["background"].red = 150
t["background"]["red"] = 150

Which formalism to use depends on what sort of key you have: the dot formalism can only be used with named keys, and not with indices or variables. So for instance t[1] can not be abbreviated t.1, and if we have the variable k="quality" we can use t[k] to access the element t["quality"] but not t.k. For nested tables with named keys the dot formalism gives the most transparent and readable expression, but in recursive loops through a table hierarchy with mixed keys the bracket formalism must normally be used.

As an example, using the pairs iterator one might sometimes be addressing elements of oxidizer_genomes solely via variables, and have an expression like oxidizer_genomes[n][key][x][k][i][l] in the code. This looks unintelligible until we examine the values of the variables, and maybe find that n=1, key="xforms", x=1, k="variations", i=1 and l="weight", so that what we're really looking at is simply oxidizer_genomes[1].xforms[1].variations[1].weight; the variation weight of the first variation in the first xform in the first genome.

Lastly, but importantly, tables are always passed by reference, to functions and in variable assignments. Hence, if you assign an existing table to a new variable and then modify the new instance, the original is also modified. To copy a table by value, its elements have to be copied one by one into a new empty table.

Oxidizer genomes in Lua


Now that we understand Lua tables, the Lua representation of Oxidizer genomes is pretty much self-explanatory. All the parameters under the Flame and Transformations windows in Oxidizer are stuffed into the Lua table oxidizer_genomes. If there are multiple genomes they are indexed according to their order in the Oxidizer list: oxidizer_genomes[1], oxidizer_genomes[2] etc. Do not confuse this table index number with the time parameter.

The implementation chart of oxidizer_genomes is given in figure 2 below, with required numerical indices indicated by the pound sign (#). An overview with some more explanations of these parameters (though sometimes differently named) is available at the Electric Sheep wiki.

OxiLuaChart_small.jpg
Figure 2. Oxidizer Lua implementation (click on image for full-size version).


Digression : Xform triangles

Since we are going to be working with these a lot, it might be a good idea to review this topic first. The Xform triangle is a two-dimensional visual representation of the six coefficients of the affine transformation. Oxidizer represents these as the three vectors P1, P2 and O, each with two (x and y) components. P1 and P2 are scale factors, while O is a shift of origin. Figure 3 shows the default Xform triangle and its coefficient values, which we may call the "identity" mapping because it effects no transformation at all; if all the Xform triangles are like this there will be no fractal to look at.

triangle.jpg coefficients.jpg
Figure 3. Xform triangle (left) and coefficients (right) of the affine transformation (images taken from the Oxidizer Transform Editor). On this grid, X is right and Y is up

It is worth pointing out that this triangle is a 2-dimensional visual representation of a point in 6-dimensional space, and there is a danger of confusion in this visualization trick. If a translation is applied, i.e. a shift of the origin O, the whole triangle moves across the grid, but the values of P1 and P2 do not change. This is because their values are not coordinates in the real plane like that of O, but scale factors. For future reference, we can list the addresses of the affine coefficients (first xform of the first genome):

oxidizer_genomes[1].xforms[1].coefs[1][1]   -- P1 x component
oxidizer_genomes[1].xforms[1].coefs[1][2]   -- P1 y
oxidizer_genomes[1].xforms[1].coefs[2][1]   -- P2 x
oxidizer_genomes[1].xforms[1].coefs[2][2]   -- P2 y
oxidizer_genomes[1].xforms[1].coefs[3][1]   -- O x
oxidizer_genomes[1].xforms[1].coefs[3][2]   -- O y


Scripting for Oxidizer


Now let’s get on with writing some Oxidizer scripts. The first few may be doing things which could be done quicker with Oxidizer, but we need to get an overview of the principles first.

Example 1 : Xform triangle rotation (1)

You can of course rotate Xform triangles with the rectangle editor in Oxidizer, but for instance to animate the rotation in any non-trivial way, scripting becomes essential. This first example will do everything explicitly in order to demonstrate the principles, and Example 2 will later show a much more efficient way of scripting Xform rotations.
First we define some parameters:

xf = 1      -- index of the Xform to rotate
gnum = 1    -- index of genome to apply script to
ang = 45    -- rotation angle in degrees

We may want to add a sanity check that there is a valid genome in Oxidizer to begin with:

ngen = #oxidizer_genomes
if ngen==0 or gnum>ngen then
   oxidizer_status["action"] = "error"
   oxidizer_status["message"] = "Genome number out of range."
   return
end

Now some math. We will define rotation here to mean a rotation of the P1 and P2 vectors around the origin O. Since we want to add our rotation angle to whatever P1 and P2 currently are, we must start by calculating their present phase angles. Converting to polar coordinates:

p1x = oxidizer_genomes[gnum].xforms[xf].coefs[1][1]
p1y = oxidizer_genomes[gnum].xforms[xf].coefs[1][2]
p2x = oxidizer_genomes[gnum].xforms[xf].coefs[2][1]
p2y = oxidizer_genomes[gnum].xforms[xf].coefs[2][2]
r1 = math.sqrt(p1x^2+p1y^2)     -- P1 radius
a1 = math.atan2(p1y,p1x)        -- P1 angle in radians
r2 = math.sqrt(p2x^2+p2y^2)     -- P2 radius
a2 = math.atan2(p2y,p2x)        -- P2 angle in radians

We invoked the square-root and inverse tangent functions from the math library. Now we add our own phase angle (in radians) and convert back to rectangular coordinates:

a1_new = a1+math.rad(ang)       -- new P1 angle in radians
a2_new = a2+math.rad(ang)       -- new P2 angle in radians
p1x_new = r1*math.cos(a1_new)
p1y_new = r1*math.sin(a1_new)
p2x_new = r2*math.cos(a2_new)
p2y_new = r2*math.sin(a2_new)

The math.rad and math.deg functions can be used to convert angles between degrees and radians if you don’t want to mess around with pi; the trigonometric functions (sin, cos, tan) expect the arguments to be in radians. If we don’t care about keeping a copy of the original genome, we can just modify it directly, and Oxidizer will be automatically updated without sending any status action:

oxidizer_genomes[gnum].xforms[xf].coefs[1][1] = p1x_new
oxidizer_genomes[gnum].xforms[xf].coefs[1][2] = p1y_new
oxidizer_genomes[gnum].xforms[xf].coefs[2][1] = p2x_new
oxidizer_genomes[gnum].xforms[xf].coefs[2][2] = p2y_new

And we’re done. You try this script on a few genomes, and check the orientation of the triangles using the Transform Editor (⌘E) in Oxidizer.
Now, although this method works (technically speaking), it isn’t very practical for much of anything. We can be a lot more clever than this, by defining functions for frequently preformed operations, using FOR-loops to economize on writing, and a few other tricks. So let’s try it one more time, thinking differently.

Example 2 : Xform triangle rotation (2)

We can start by realizing that converting between Cartesian and polar coordinates is something that we might be doing a lot, so why not define a pair of functions to do this job:

function c2p (x,y)          -- convert from Cartesian to polar
   r = math.sqrt(x^2+y^2)
   a = math.atan2(y,x)
   return {r,a}
end

function p2c (r,a)          -- convert from polar to Cartesian
   x = r*math.cos(a)
   y = r*math.sin(a)
   return {x,y}
end

This illustrates one way of defining a function (also see the Lua Short Reference for alternative methods). Since they return a new table, the way to call these functions is by a variable assignment, for instance rphi = c2p(p1x,p1y), and the new variable rphi will be a two-element table containing the radius and phase angle of the Cartesian input arguments p1x and p1y.

Since rotating Xform triangles is also something that we can imagine doing a lot, we might as well turn that into a function also. Since we are doing exactly the same thing to both P1 and P2, we can use a FOR-loop to save a few lines:

function affine_Prot (genome,x,phs)
    for i=1,2 do
    rphi = c2p(genome.xforms[x].coefs[i][1],
            genome.xforms[x].coefs[i][2])
    xy = p2c(rphi[1],rphi[2]+math.rad(phs))
    genome.xforms[x].coefs[i][1] = xy[1]
    genome.xforms[x].coefs[i][2] = xy[2]
    end
end

Since we modified the input genome directly (which is a table, and tables are passed by reference), a return statement is not needed. The way to use this function is then simply the stand-alone function call: affine_Prot(genome,x,phs).

In order for user-defined functions like this to be known by other scripts, one possibility is to just copy them into the top of the script. A much more practical method, however, is to put all these functions in a separate file, which can then be loaded by the main script. One simple way of doing this is with the dofile Lua function, for instance like this:

luadir = '/Users/yourmac/Documents/Oxidizer/tutorial/'  -- replace with actual path to utils file
dofile(luadir..'utils.lua')

The double-dots .. is a Lua operator that concatenates strings. In this example, utils.lua is the name of the file that contains these functions, and luadir is the full path to the folder containing this file. Invoking all these tricks, our new Xform triangle rotation script now becomes as simple as this:

luadir = '/full-path-to-tutorial-folder/' -- change to actual path!
dofile(luadir..'utils.lua')

xf = 1
gnum = 1
ang = 45

ngen = #oxidizer_genomes
if ngen==0 or gnum>ngen then
   oxidizer_status["action"] = "error"
   oxidizer_status["message"] = "Genome number out of range."
   return
end

affine_Prot(oxidizer_genomes[gnum],xf,ang)

Which is to be compared with Example 1, which does exactly the same thing.

There are no comments on this page. [Add comment]

Valid XHTML 1.0 Transitional :: Valid CSS :: Powered by WikkaWiki . Install WikkaWiki