Play Compilation Video

Technical Goals

The Lua Scripted Ray Tracer extends the ray tracer completed in part 4 by adding scriptability via the Lua scripting language. Lua is a very lightweight language specifically designed for embedding into larger compiled applications. In many ways, it's a more modern Tcl.

In roughly 1,300 lines of C code, every property available through our working subset of the POV-Ray syntax is available and scriptable via Lua. To facilitate assigning Lua scripts within a .pov file, the following modifiers were added to the camera, light_source, sphere, plane, triangle, and box blocks:
name <some arbitrary name>
script <my_lua_script.lua>
The name modifier is used to attach an arbitrary string to an object which can come in handy for identifying a specific object in Lua scripts. The script modifier attaches a Lua script to a camera, light_source, or geometric object. Predefined functions within this Lua source file are then called before each frame is traced.

The Lua scripts must contain at least one of the following functions:
function initial(t, l, n)
    -- do some scripty things
end

function final(t, l, n)
    -- do some more scripty things
end
The parameters to these functions are as follows: When constructing the scene (before tracing it), the ray tracer calls the initial function of each camera, light_source, and geometric object, followed by calling the final function of each camera, light_source, and geometric object. A script need not define both an initial and final function. The two functions are provided so that scripted objects can react to changes in other scripted objects. For example, a collection of spheres may update their positions based solely on their internal velocity in their initial function, and then check for collisions with other spheres and update their position accordingly in their final function.

Global variables may be used outside of these two functions to store values common to both functions. Each object in the scene gets it's own, isolated copy of the Lua interpreter, so even if many objects share the same script the values of their global variables may be wildly different. Because of this, special functions must be used to pass and retrieve global variables to and from other objects in the scene. More on that later...

These scripts wouldn't be very interesting if they weren't able to communicate back with the data structures in the C++ ray tracer. The following special global variables are available in your scene:
-- Unique ID for geometric objects.
obj_num

-- Unique ID for light_sources.
lt_num
To get the object type (if the object is a geometric primitive) or the object or light name (assigned with the name <> modifier), you can use the following functions:
-- For geometric objects
obj_name_get(obj_num) -- For example, might return "my object".
obj_type_get(obj_num) -- For example, might return "triangle".

-- For light sources.
lt_name_get(lt_num) -- Returns empty string if no name assigned.
To get or set the properties of an object (for example, the location of a light or the center and radius of a sphere), you can use the obj_props_get() and obj_props_set() functions for objects or lt_pos_get() and lt_pos_set() for lights.
-- Get and set properties of a sphere
center_x, center_y, center_z, radius = obj_props_get(obj_num)
obj_props_set(obj_num, center_x, center_y, center_z, radius)

-- Get and set the position of a light
lt_x, lt_y, lt_z = lt_pos_get(lt_num)
lt_pos_set(lt_num, lt_x, lt_y, lt_z)
To get or set the pigment of an object, you can use the obj_color_get() and obj_color_set() functions and use the lt_color_get() and lt_color_set() for light color.
-- Get and set pigment of an object
red, green, blue, filter = obj_color_get(obj_num)
obj_color_set(obj_num, red, green, blue, filter)

-- Get and set the color of a light
red, green, blue = lt_color_get(lt_num)
lt_color_set(lt_num, red, green, blue)
Similarly, you can use obj_finish_get(), obj_finish_set(), obj_xform_get(), and obj_xform_set() to get and set the finish properties of an object and its transformation matrix. Since you are manipulating the raw matrix, helper functions are available in xform_helper.lua to generate common matrices for translations, rotations, and scales, as well as multiply matrices. For vector helper functions, such as length, dot and cross products, and reflection vectors, see vector_helper.lua.

As mentioned above, each object receives its own state of the Lua interpreter, so global variables are essentially sandboxed from each other. If you want objects to react to one another, you need a way of passing values back and forth.

For this, you can use the obj_grab_var() and obj_pass_var() functions (lt_grab_var() and lt_pass_var() for lights).
-- Get the value of other_global_var from object number i, add one to it, and send it back.
my_var = obj_grab_var(i, "other_global_var")
my_var = my_var + 1
obj_pass_var(i, "other_global_var", my_var)
Lastly, there are functions for setting the POV-Ray camera location, up, right, and look at vectors. Since these are usually very painful to use and not intuitive for animating, it's suggested to use the functions provided in camera_helper.lua to generate these vectors from more intuitive ones.

The following example creates a camera that rotates around the origin with a radius of 10 units, hovering 5 units above the ground, and wobbling back and forth (rotating about it's gaze vector) once every quarter-turn.
-- Include the camera helper.
dofile "camera_helper.lua"

function initial(t, l, n)
    -- Rotate the camera in a circle 10 units away from the origin.
    local cam_x = math.sin(2 * math.pi * t) * 10
    local cam_z = math.cos(2 * math.pi * t) * 10
	
    -- Wobble the camera about the gaze vector 15 degrees either direction once every quarter-turn.
    local cam_wobble = math.sin(8 * math.pi * t) * 15

    local cam = { eye = { x = cam_x,    -- Eye location.
                          y = 5,
                          z = cam_z },
                  look = { x = 0,       -- Look location.
                           y = 0,
                           z = 0 },
                  up = { x = 0,         -- Natural world up, NOT CAMERA UP!
                         y = 1,
                         z = 0 },
                  rot = cam_wobble,     -- Rotation about the gaze vector (look - eye)
                  aspect = 1.77777778 } -- Aspect ratio of the frame (width / height)

    -- Generate POV-Ray camera vectors from more intuitive ones.
    local location = camera.location(cam)
    local up = camera.up(cam)
    local right = camera.right(cam)
    local look_at = camera.look_at(cam)

    -- Set the camera vectors.
    cam_loc_set(location.x, location.y, location.z)
    cam_up_set(up.x, up.y, up.z)
    cam_right_set(right.x, right.y, right.z)
    cam_lookat_set(look_at.x, look_at.y, look_at.z)
end
Finally, some animation helper functions can be found in anim_helper.lua. These include simple easing functions that can be chained for basic keyframing. They return an interpolated value during the passed time range, and zero outside the time range, so they can be chained with simple addition. For example, here's how to animate a value linearly from t = 0.0 to 0.3, hold a value from 0.3 to 0.7, and use a cubic out easing function from 0.7 to 1.0:
-- Include the animation helper.
dofile "anim_helper.lua"

function initial(t, l, n)
    -- Animated value.
    local my_value = anim.linear(-5, 5, 0.0, 0.3, t) +    -- Linearly interpolate from -5 to 5 on the time range 0.0 to 0.3
                     anim.hold(5, 0.3, 0.7, t) +          -- Hold at 5 on the time range 0.3 to 0.7
                     anim.cubic_out(5, 12, 0.7, 1.01)     -- Cubic out easing from 5 to 12 on the time range 0.7 to 1.0
end

Usage Instructions

Usage: raytrace -W<width> -H<height> -I<input file> [-F<frame count> -S<start frame> -R<duration in frames>
-D[ambient|diffuse|specular|shadows|reflection|refraction|antialiasing]]
Rendering still images like all previous programs can still be accomplished as usual. Here's a brief rundown of the new command line options:

References