Wiki

Clone wiki

Core / RollerCoaster

Back to Codea's Example Projects


ProjectRollerCoaster-Small.png Roller Coaster by Andrew Stacey

Introduction


Roller Coaster is one of the example projects supplied with Codea. It simulates a thrilling roller-coaster ride showing off the 3D capabilities of Codea. The user touches the screen to look around, double-taps to pause.

The example also illustrates the implementation of an application that has two 'states' (paused and not paused) and that is moved from one state to another by touching the viewer.

Tabs in the project


In addition to Main the project comprises the following tabs:

TabComment
ColourThis tab provides some functions for basic colour manipulation.
ColourNamesThis tab defined two arrays of colours, one according to the SVG definitions and another according to the x11 definitions.
Vec3Establishes the class Vec3.

Main tab


The Main tab implements three callback functions (which are explained further later in this section):

setup()        -- called once
draw()         -- called up to 60 times a second
touched(touch) -- called whenever a touch on screen begins, ends or moves

The Main tab also implements the following functions:

AddPlank(t)        -- Calculates and adds the vertices and their colours for a plank
TrackPoints(a)     -- Calculates evenly-spaced points on a track
AddStar(t)         -- Calculates and adds the vertices and their colours for a star
tangent(a)         -- Estimates the tangent to the track on a point on the track
Tracks.torus(p, q) -- Returns the track (and its normal) function for torus tracks
Tracks.mobuis()    -- Returns the track (and its normal) function for a mobius track
Tracks.loop(p, q)  -- Returns the track (and its normal) function for loop tracks

Tracks.loop(p, q)

Tracks refers to a table that holds functions that return a trackFunction function, a trackNormal function and a height h. Tracks.loop(p ,q) is one of the three functions provided in the code, and the one that is used in setup().

By default, setup() calls Tracks.loop(3, 2). The loop is drawn in the x-z plane, with the height of the loop undulating above and below the plane in the direction of the y axis.

function Tracks.loop(p, q)
    local r = 30                      -- Radius of the 'loop'
    local h = 10                      -- Half the height of the 'loop'
    local w = Vec3(0, 30, 0)
    local trackFunction =
        function(t)
            local a = 2 * math.pi * t -- 't' is the number of revolutions
            return Vec3(
            r * math.cos(a),          -- Set x-coordinate of the track
            h * math.sin(p * a),      -- Set y-coordinate of the track
            r * math.sin(q * a))      -- Set z-coordinate of the track
        end
    local trackNormal =
        function(t)
            return w - trackFunction(t)
        end
    return trackFunction, trackNormal, h -- The function returns multiple values
end

The shape of the loop can be understood with this separate project:

supportedOrientations(LANDSCAPE_ANY)

function setup()
    -- Functions of r (radius) and theta (angle in radians)
    xFunction = "x = r * cos(theta)"
    yFunction = "y = r * sin(2 * theta)"
    
    points = {}            -- Table of points of curve
    step = 5               -- Step (in degrees)
    speed = 100 / step     -- Speed for animation
    deg2rad = 2 * pi / 360 -- Factor to convert to radians
    title = xFunction.." and "..yFunction
    r = math.min(WIDTH, HEIGHT) / 2 * 0.9
    fx = loadstring(xFunction)
    fy = loadstring(yFunction)
    for angle = 0, 360, step do
        theta = angle * deg2rad
        fx()
        fy()
        table.insert(points, vec2(x, y))
    end
    strokeWidth(5)
    fill(255)
end

function draw()
    background(0)
    translate(WIDTH/2, HEIGHT/2)
    stroke(0, 255, 0, 255)
    line(-WIDTH/2, 0, WIDTH/2, 0)
    line(0, -HEIGHT/2, 0, HEIGHT/2)
    text(title, 0, HEIGHT/2 - 20)
    stroke(255)
    n = math.floor(ElapsedTime * speed) % #points + 1
    if n > 1 then
        for i = 2, n do
            p1 = points[i-1]
            p2 = points[i]
            line(p1.x, p1.y, p2.x, p2.y)
        end
    end    
end

-- Shorthands for math library
pi =math.pi
cos = math.cos
sin = math.sin
tan = math.tan

tangent(a)

The arguments of the tangent() function are passed as the fields of a table a: delta, pathFunction and time. The function returns an estimate of the tangent of the pathFunction at time based on the locations of the path at time plus and minus delta:

function tangent(a)
    local s = a.delta / 2 or .1      -- Default half-delta is 0.1
    local f = a.pathFunction or      -- Default is along positive x axis
        function(q)
            return q * Vec3(1, 0, 0)
        end
    local t = a.time or 0            -- Default is 0
    local u = f(t - s)               -- Point at time minus delta 
    local v = f(t + s)               -- Point at time plus delta
    return (v - u) / (2 * s)         -- Estimate the tangent  
end

TrackPoints(a)

The arguments of the TrackPoints() function are passed in a table a:

function TrackPoints(a)
    a = a or {}
    local pts = a.points or {}     -- Default is an empty table
    local t = a.start or 0         -- Default is 0
    local r = a.step or .1         -- Default is 0.1
    ...
    local s = a.delta or .1        -- Default is 0.1
    local f = a.pathFunction or    -- Default is along positive x axis
        function(q)
            return q * Vec3(1, 0, 0)
        end
    local nf = a.normalFunction or -- Default is in direction of positive y axis
        function(q)
            return Vec3(0, 1, 0)
        end
    local b = a.finish or 1        -- Default is 1
    ...
end

The function then works it way round the track:

function TrackPoints(a)
    ...
    r = r * r                          -- Square the 'step' argument 
    ...
    local tpt = f(t)                   -- Get the first point on the track
    table.insert(pts, ...)             -- Add item to 'pts' table
    local dis
    local p
    while t < b do                     -- While less than the finish
        dis = 0                        -- Reset total distance to 0
        while dis < r do               -- While total distance is less than step squared
            t = t + s                  -- Increase by the delta
            p = f(t)                   -- Get the point on the track
            dis = dis + p:distSqr(tpt) -- Increase the total distance
            tpt = p                    -- Set the track point to the current point
        end
        if t > b then                  -- Greater than the finish?
            t = b                      -- Set to the finish
            p = f(b)                   -- Get the last point on the track
        end
        table.insert(pts, ...)         -- Add item to 'pts' table 
        tpt = p                        -- Set the track point to the current point 
    end
    return pts                         -- Return the 'pts' table
end

The items added to the pts table are themselves tables (arrays):

{
    p,                                                -- The track point
    tangent({delta = s, pathFunction = f, time = t}), -- Result of tangent()
    nf(t),                                            -- Result of the normal function
    t                                                 -- The index on the track
}

AddStar(t)

The arguments of the AddStar() function are passed as the fields of a table t: origin (held in local variable o), size (held in s) and light (held in l).

Each star comprises two interlocking tetrahedra, with the vertices located at the vertices of a cube. The star has a random orientation about its centre:

function AddStar(t)
    ...
    local b = Vec3.RandomBasis()              -- Basis vectors with a random orientation
    ...
    local bb = {}                                      
    for i = 0, 7 do                           -- Generate the eight corners of the cube:
        bb[1] = 2 * (i % 2) - 1               -- 0: -1 -1 -1    4: -1 -1 +1  
        bb[2] = 2 * (math.floor(i/2) % 2) - 1 -- 1: +1 -1 -1    5: +1 -1 +1
        bb[3] = 2 * (math.floor(i/4) % 2) - 1 -- 2: -1 +1 -1    6: -1 +1 +1
                                              -- 3: +1 +1 -1    7: +1 +1 +1

        v = bb[1] * b[1] + bb[2] * b[2] + bb[3] * b[3] -- Location of the corner in question
        ...
        for m = 1, 3 do -- Each triangular face of the tetrahedron has its vertices
            ...         -- at the neighbouring corners to the corner in question:
                        -- v - 2 * bb[m] * b[m]
            table.insert(vertices, (o + s * (v - 2 * bb[m] * b[m])):tovec3())
        end
    end
    ...
end

setup()

The track of the roller coaster is defined by two functions that return its position (held in global variable trackFunction) and its orientation at that position (held in global variable trackNormal). Another attribute of the track is its maximum height (held in global variable maxHeight. All three are established in the setup() function:

function setup()
    ...
    trackFunction, trackNormal, maxHeight = Tracks.loop(3, 2) -- .loop(3, 2) is the default.
                                                              -- .mobius() or .torus(p, q) are
                                                              -- also options.
    ...
end

The maxHeight, gravity (g) and speed determine the energy in the system:

function setup()
    ...
    g = 0.0000981
    trackFunction, trackNormal, maxHeight = Tracks.loop(3, 2)
    ...
    speed = .05
    ...
    energy = speed^2 + 2 * g * (maxHeight + 5)
    ...
end

The energy of the system is conserved, so as the height of the traveller changes, the speed is determined accordingly (in the draw() function).

The setup() function establishes two global variables referring to meshes: plank, representing the planks of the track; and stars, representing the field of stars.

function setup()
    ...
    plank = mesh()
    ...
    for k, v in ipairs(track) do
        ...
        AddPlank(...) -- Add a plank in the track
    end
    ...
    stars = mesh()
    ...
    for i = 1, 200 do
        AddStar(...)  -- Add a star
        AddStar(...)  -- Add a star
    end
    ...
end

This sets global variable paused to true and ptext (the pause text) to "Tap to begin".

function setup()
    ...
    paused = true
    ptext = "Tap to begin"
end

draw()

The draw() function is called by Codea up to 60 times a second. What is does depends, in part, on whether the paused flag is true or false:

function draw()
    background(Colour.svg.Black)       -- Equivalent to background(0)
    perspective(45, WIDTH/HEIGHT)      -- Set the perspective
    if not paused then
        ...                            -- If not paused, process movement
    else
        camera(0, 8 * maxHeight, 15, 0, 0, 0, 0, 1, 0) -- Set the 'camera' to a particular position
    end
    plank:draw()                       -- In all cases, draw the planks
    stars:draw()                       -- In all cases, draw the stars
    if paused then
        ...
        text(ptext, WIDTH/2, HEIGHT/2) -- If paused, render ptext in viewer centre
    end
end

If paused is false then the movement is processed:

time = time + speed * DeltaTime -- Update time, scaled by speed
local pos = trackFunction(time) -- Get the position for that time
local tmp
tmp = tangent({delta = .1, pathFunction = trackFunction, time = time})
if not tmp:is_zero() then
    dp = tmp
end
nml = trackNormal(time)    
speed = math.sqrt(energy - 2 * g * pos.y) -- Calculate speed, by conserving energy
local ob = Vec3.SO(dp, nml)
pos = pos + ht * ob[2]
local dir = pos + size * (sz * (ca * ob[1] + sa * ob[3]) + cz * ob[2])
camera(pos.x, pos.y, pos.z,
    dir.x, dir.y, dir.z,
    ob[2].x, ob[2].y, ob[2].z)

touched(touch)

This function is called when the viewer is touched.

function touched(touch)
    if paused then     -- If paused...
        paused = false -- ... turn pause flag off ...
        return         -- ... and return
    end

    -- Consequently, not paused...
    if touch.state == ENDED and touch.tapCount == 2 then -- If last of two taps ... 
        paused = true                                    -- ... turn pause flag on ... 
        ptext = "Tap to resume"                          -- ... set the pause message ... 
        return                                           -- ... and return
    end
    ... -- Consequently, handle look around 
end

Colour tab


The Colour tab is identified as author: Andrew Stacey (website http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html) and licence: CC0. It provides functions for colour manipulation. The functions act on color userdata values and return color userdata values.

Colour implements:

Colour.blend(cc, t, c)
Colour.tint(c, t)
Colour.shade(c, t)
Colour.tone(c, t)
Colour.complement(c)
Colour.opacity(c, t)
Colour.opaque(c)
Colour.tostring(c)
Colour.byName(t, n)

Vec3 tab


The Vec3 tab is identified as author: Andrew Stacey (website http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html) and licence: CC0. It implements the Vec3 class for handling 3 dimensional vectors and defining a variety of methods on them.

Vec3 implements:

Vec3:init(x, y, z)
Vec3:is_zero()                 -- Test for zero vector
Vec3:is_finite()               -- Check entries against nan
Vec3:is_eq(v)                  -- Test for equality
Vec3:dot(v)                    -- Inner product
Vec3:cross(v)                  -- Cross product
Vec3:applyMatrix(a, b, c)      -- Apply a given matrix (a triple of vectors)
Vec3:len()                     -- Length of the vector
Vec3:lenSqr()                  -- Squared length of the vector
Vec3:dist(v)                   -- Distance of the vector to another
Vec3:distSqr(v)                -- Squared distance of the vector to another
Vec3:normalise()               -- Normalise the vector (if possible) to length 1
Vec3:scale(l)                  -- Scale the vector
Vec3:add(v)                    -- Add vectors
Vec3:subtract(v)               -- Subtract vectors
Vec3:absCoords()               -- Transform between "absolute" and "relative" coordinates
Vec3:isInFront(e)              -- Is vector is in front of the "eye"?
Vec3:stereoProject(e)          -- Project the vector onto the screen
Vec3.stereoInvProject(v, e, h) -- Partial inverse to stereographic projection
Vec3:stereoLevel(e)            -- Returns the distance from the eye
Vec3:rotate(w)                 -- Applies a rotation as specified by another 3-vector
Vec3:toQuaternion()            -- Promote to a quaternion with 0 real part
Vec3:applyQuaternion(q)        -- Apply a quaternion as a rotation
Vec3:tostring()                -- Convert to a string
Vec3:tovec3()                  -- Convert to a vec3 userdata
Vec3:tomatrix()                -- Convert to a matrix userdata 

Vec3 also implements the following in-line operators:

OperatorReturns
v1 + v2v1:add(v2)
v1 - v2v1:subtract(v2)
-v1v1:scale(-1)
n1 * v2v2:scale(n1)
v1 * n2v1:scale(n2)
v1 * v2v1:cross(v2)
v1 / n2v1:scale(1 / n2)
v1 ^ q2v1:applyQuaternion(q2)
v1 == v2v1:is_eq(v2)
v1 .. v2v1:dot(v2)
s1 .. v2s1 .. v2:tostring()
v1 .. s2v1:tostring() .. s2}

Vec3 also implements the following functions:

Vec3.SetEye(...)
GramSchmidt(t)
Vec3.Random()
Vec3.RandomBasis()
Vec3.SO(u, v)
Vec3.isOverLine(a, b, c, d, e)
Vec3.isOverPoint(a, b, c, r, e)

Vec3 also establishes the following fields:

Vec3.eye    = Vec3(0,0,1)
Vec3.origin = Vec3(0,0,0)
Vec3.e1     = Vec3(1,0,0)
Vec3.e2     = Vec3(0,1,0)
Vec3.e3     = Vec3(0,0,1)

GramSchmidt(t)

The Gram–Schmidt process is a method for orthonormalising a set of vectors:

function GramSchmidt(t)
    local o = {}                 -- This table will hold the output vectors
    local w
    for k, v in ipairs(t) do     -- For each input vector 'v'
        w = v
        for l, u in ipairs(o) do -- For each output vector 'u' so far
            w = w - w:dot(u) * u
        end
        if not w:is_zero() then
            w = w:normalise()
            table.insert(o, w)   -- Add to table of output vectors
        end
    end
    return o
end

Vec3.SO(u, v)

Vec3.SO returns a table of three orthonormal vectors given input vectors u and v. The first two output vectors are in the u-v plane:

function Vec3.SO(u, v)
    if u:is_zero() and v:is_zero() then    -- Are u and v both zero vectors?
        return {Vec3.e1, Vec3.e2, Vec3.e3} -- Return the standard basis
    end
    if u:is_zero() then                    -- Is u a zero vector?
        u, v = v, u                        -- Swap u and v (so that v is the zero vector) 
    end
    if u:cross(v):is_zero() then           -- Is v a zero vector, or are u and v co-linear?
        if u.x == 0 and u.y == 0 then      -- Is u in the direction of the z-axis?
            v = Vec3.e3                    -- In error: set v in the z-axis direction
         -- v = Vec3.e1                    -- Correction: set v in the x-axis direction
        else
            v = Vec3(u.y, -u.x, 0)         -- Reset v to be the x and y components of u,
                                           -- rotated 90 degrees clockwise
        end
    end
    local t = GramSchmidt({u, v})         -- Apply Gram-Schmidt process to u and v;
                                          -- the result is two orthonormal vectors
    t[3] = t[1]:cross(t[2])               -- Set third vector as orthogonal to first two
    return t
end

As indicated above, there is an error in the logic of the code provided in the Roller Coaster example project. In order for the Gram-Schmidt process to work, it is necessary for the inputs to the process (u and v) to be non-zero and linearly independent.

Updated