Wiki
Clone wikiCore / RollerCoaster
Back to Codea's Example Projects
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:
Tab | Comment |
---|---|
Colour | This tab provides some functions for basic colour manipulation. |
ColourNames | This tab defined two arrays of colours, one according to the SVG definitions and another according to the x11 definitions. |
Vec3 | Establishes 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:
Operator | Returns |
---|---|
v1 + v2 | v1:add(v2) |
v1 - v2 | v1:subtract(v2) |
-v1 | v1:scale(-1) |
n1 * v2 | v2:scale(n1) |
v1 * n2 | v1:scale(n2) |
v1 * v2 | v1:cross(v2) |
v1 / n2 | v1:scale(1 / n2) |
v1 ^ q2 | v1:applyQuaternion(q2) |
v1 == v2 | v1:is_eq(v2) |
v1 .. v2 | v1:dot(v2) |
s1 .. v2 | s1 .. v2:tostring() |
v1 .. s2 | v1: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