
Amulet Manual
Introduction
Amulet is a Lua-based toolkit for experimenting with interactive graphics and audio. It provides a cross-platform API for drawing graphics, playing audio and responding to user input, and a command-line interpreter for running Amulet scripts.
It tries to be simple to use, but without making too many assumptions about how you want to use it.
Amulet currently runs on the following platforms:
- Windows 7+ (Intel 32 and 64 bit, Direct3D or OpenGL)
- Mac OS X 10.6+ (Intel 64 bit only, Metal)
- Linux (Intel 32 and 64 bit, OpenGL)
- HTML5 (WebAssembly, WebGL)
- iOS (ARM 64 bit only, Metal)
- Android (ARM 32 and 64 bit, OpenGLES)
How to use this document
If you have some programming experience, but are new to Lua then the Lua primer should bring you up to speed.
The quickstart tutorial introduces basic concepts and walks you through drawing things on screen, playing audio and responding to user input.
The online editor contains numerous examples you can experiment with from your browser.
If you see a function argument with square brackets around it in a function signature, then that argument is optional.

Lua primer
Amulet code is written in Lua. The version of Lua used by default is LuaJIT-2 on Windows, Mac and Linux and Lua-5.1 on all other platforms.
What follows is a quick introduction to Lua. For more detail please see the Lua manual.
Comments:
-- This is a single line comment
--[[ This is
a multi-line
comment ]]
Local variables (block scope):
local x = 3.14 -- a number
local str = "a string"
local str2 = [[
a
multi-line
string]]
local bool = true -- a bool, can also be false
local y = nil -- nil value
local v1, v2 = 1, 2 -- create two local variables at once
Global variables:
score = 0
title = "My Game"
If-then-else:
local x = 2
if x > 1 then
print("x > 1")
elseif x > 0 then
print("0 < x <= 0")
else
print("x <= 0")
end
The else part of an if-then-else executes only if the condition evaluates to false
or nil
.
While loop:
local n = 5
while n > 0 do
print(n)
n = n - 1
end
-- prints 5 4 3 2 1
Repeat-until loop:
local n = 0
repeat
n = n + 1
print(n)
until n == 5
-- prints 1 2 3 4 5
For loop:
for i = 1, 5 do
print(i)
end
-- prints 1 2 3 4 5
for j = 10, 1, -2 do
print(j)
end
-- prints 10 8 6 4 2
You can break out of a loop using the break
statement:
for j = 1, 10 do
print(j)
if j == 5 then
break
end
end
-- prints 1 2 3 4 5
Operators:
The arithmetic operators are: +
, -
, *
, /
, ^
(exponent) and %
(modulo).
Note that Amulet also overloads the ^
operator for constructing scene graphs (see here for more details).
The relational operators are: ==
, ~=
(not equal), <
, >
, <=
and >=
The logical operators are: and
, or
and not
.
The string concatenation operator is two dots (e.g. "abc".."def"
).
Tables:
Tables are the only data structure in Lua. They can be used as key-value maps or arrays.
Keys and values can be of any type except nil
.
local t = {} -- create empty table
t["score"] = 10
t[1] = "foo"
t[true] = "x"
-- this creates a table with 2 string keys:
local t2 = {foo = "bar", baz = 123}
Special syntax is provided for string keys:
local t = {}
t.score = 10
print(t.score)
The #
operator returns the length of an array and array indices start at 1 by default.
local arr = {3, 4, 5}
for i = 1, #arr do
print(arr[i])
end
-- prints 3, 4, 5
table.insert(arr, 6) -- appends 6 to end of arr
table.remove(arr, 1) -- removes the first element of arr
-- arr is now {4, 5, 6}
Setting a key's value to nil
removes the key and indexing a missing key returns nil
.
You can iterate over all key/value pairs using the pairs
function. Note that the order is not preserved.
local t = {a = 1, b = 2, c = 3}
for k, v in pairs(t) do
print(k..":"..v)
end
-- prints a:1 c:3 b:2
To iterate over an array table, keeping the order, use ipairs
:
local arr = {"a", "b", "c"}
for k, v in ipairs(arr) do
print(k..":"..v)
end
-- prints 1:a 2:b 3:c
Functions:
function factorial(n)
if n <= 1 then
return 1
else
return n * factorial(n-1)
end
end
print(factorial(3)) -- prints 6
Functions are values in Lua so they can be assigned to variables:
local add = function(a, b)
return a + b
end
print(add(1, 2)) -- prints 3
Special syntax is provided for adding functions to tables:
local t = {}
function t.say_hello()
print("hello")
end
This is equivalent to:
local t = {}
t.say_hello = function()
print("hello")
end
Special syntactic sugar allows you to use function fields like methods in object oriented languages:
The code:
function t:f()
...
end
is equivalent to:
function t.f(self)
...
end
and the code:
t:f()
is equivalent to:
t.f(t)
For example:
local t = {x = 3}
function t:print_x()
print(self.x)
end
t:print_x() -- prints 3
t.x = 4
t:print_x() -- prints 4
If a function call has only a single string or table argument, the parentheses can be omitted:
local
function append_z(str)
return str.."z"
end
print(append_z"xy") -- prints xyz
local
function sum(values)
local total = 0
for _, value in ipairs(values) do
total = total + value
end
return total
end
print(sum{3, 1, 6}) -- prints 10
Functions may also return multiple values:
function f()
return 1, 2
end
local x, y = f()
print(x + y) -- prints 3

Quickstart tutorial
Installing Amulet
Windows:
Download the Windows installer and run it. Or, if you prefer, download the Windows zip archive, extract it to a directory of your choice and add that directory to your PATH.
Mac:
Download the OSX pkg file and run it. Or, if you prefer, download the OSX zip archive, extract it to a directory of your choice and add that directory to your PATH.
Linux:
Download and extract the Linux zip archive to a directory of your choice. Then add the directory to your PATH. The amulet
executable is for x86_64. If you're running a 32 bit system, rename the file amulet.i686
to amulet
.
Online editor: There is also an online editor which you can use without having to download or install anything. However be aware that there are some limitations when using the online editor: you can't load image or audio files and only one lua module is allowed per project. These restrictions do not apply when exporting to HTML from the desktop version.
Running a script
Create a text file called main.lua
containing the following:
log("It works!")
Open a terminal ("command prompt" on Windows) and change to the directory containing the file. Then type "amulet main.lua
":
> amulet main.lua
main.lua:1: It works!
If you see the text "main.lua:1: It works!
", Amulet is installed and working.
If you are using the online editor, just type this into the main text area and click "Run".
Creating a window
Type the following into main.lua
:
local win = am.window{
title = "Hi",
width = 400,
height = 300,
clear_color = vec4(1, 0, 0.5, 1)
}
and run it as before. This time a bright pink window should appear.
Rendering text
Add the following line to main.lua after the line that creates the window:
win.scene = am.text("Hello!")
This assigns a scene graph to the window. The scene has a single text node. The window will render its scene graph each frame.

Transformation nodes
Change the previous line to:
win.scene =
am.translate(150, 100)
^ am.scale(2)
^ am.rotate(math.rad(90))
^ am.text("Hello!")
This adds translate, scale and rotate nodes as parents of the text node. These nodes transform the position, size and rotation of all their children. The resulting scene graph looks like this:

The translate node moves its descendants to the right 150 and up 100 (by convention the y axis increases in upward direction). The scale node doubles its descendant's size and the rotate node rotates its descendants by 90 degrees (math.rad
converts degrees to radians). Finally the text node renders some text to the screen.
When you run the program you should see something like this:

Actions
Add the following to the end of main.lua:
win.scene:action(function(scene)
scene"rotate".angle = am.frame_time * 4
end)
When you run it the text will spin.
This code adds an action to the scene, which is a function that's run once per frame. Actions can be added to any scene node. In this case we've added it to the node win.scene
, which is the top translate
node of our scene graph. The node to which an action is attached is passed as an argument to the action function.
The line:
scene"rotate".angle = am.frame_time * 4
first finds a node with the tag "rotate"
in the scene graph. By default nodes have tags that correspond to their names, so this returns the rotate node. You can also add your own tags to nodes using the tag
method which we'll discuss in more detail in the next section.
Then we set the angle
property of the rotate node to the current frame time (the time at the beginning of the frame, in seconds) times 4.
Note that scene"rotate"
could also be written as scene("rotate")
. The first form takes advantage of some Lua syntactic sugar that allows function parenthesis to be omitted if the argument is a single literal string.
Since this code is run each frame, it causes the text to spin.
Here is the complete code listing:
local win = am.window{
title = "Hi",
width = 400,
height = 300,
clear_color = vec4(1, 0, 0.5, 1)
}
win.scene =
am.translate(150, 100)
^ am.scale(2)
^ am.rotate(math.rad(90))
^ am.text("Hello!")
win.scene:action(function(scene)
scene"rotate".angle = am.frame_time * 4
end)
Drawing shapes
Here is a simple program that draws 2 red circles on either side of a yellow square on a blue background.
local red = vec4(1, 0, 0, 1)
local blue = vec4(0, 0, 1, 1)
local yellow = vec4(1, 1, 0, 1)
local win = am.window{
title = "Hi",
width = 400,
height = 300,
clear_color = blue,
}
win.scene =
am.group()
^ {
am.translate(-150, 0)
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(150, 0)
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(0, -25)
^ am.rect(-50, -50, 50, 50, yellow)
}

This time, we've created variables for the different colours we'll need. In Amulet colours are 4-dimensional vectors. Each component of the vector represents the red, green, blue and alpha intensity of the colour and ranges from 0 to 1.
The scene graph has a group
node at the top. group
nodes don't have any effect on the rendering and are only used to group other nodes together. The group node has 3 children, each of which is a translate
node with a shape child. The scene graph looks like this:

Instead of building the scene graph using the ^
operator as we've done above, we can also do it step-by-step using the append
method, which adds a node to the child list of another node:
local circle1_node = am.translate(-150, 0)
circle1_node:append(am.circle(vec2(0, 0), 50, red))
local circle2_node = am.translate(150, 0)
circle2_node:append(am.circle(vec2(0, 0), 50, red))
local rect_node = am.translate(0, -25)
rect_node:append(am.rect(-50, -50, 50, 50, yellow))
local group_node = am.group()
group_node:append(circle1_node)
group_node:append(circle2_node)
group_node:append(rect_node)
win.scene = group_node
This results in the exact same scene graph.
Responding to key presses
Let's change the above program so that the left circle only appears while the A key is down and the right circle only appears while the B key is down.
In order to distinguish the two circles in the scene graph we'll give them different tags.
Change the scene setup code to look like this:
win.scene =
am.group()
^ {
am.translate(-150, 0):tag"left_eye"
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(150, 0):tag"right_eye"
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(0, -25)
^ am.rect(-50, -50, 50, 50, yellow)
}
The only difference is the addition of :tag"left_eye"
and :tag"right_eye"
. These add the tag "left_eye"
to the translate node which is the parent of the left circle node and "right_eye"
to the right translate node which is the parent of the right circle node.
Now add the following action:
win.scene:action(function(scene)
scene"left_eye".hidden = not win:key_down"a"
scene"right_eye".hidden = not win:key_down"b"
end)
The hidden
field of a node determines whether it is drawn or not. Each frame we set the hidden
field of the left and right translate nodes to whether the A or B keys are being pressed. win:key_down(X)
returns true if key X
was being held down at the start of the frame.
You may notice that the three shapes appear briefly when the window is first shown and then disappear immediately. This is because actions only start running in the next frame, so the shapes are only hidden on the second frame. To fix this we can add the following lines either before or after we add the action (it doesn't matter where):
win.scene"left_eye".hidden = true
win.scene"right_eye".hidden = true
Here is the complete code listing:
local red = vec4(1, 0, 0, 1)
local blue = vec4(0, 0, 1, 1)
local yellow = vec4(1, 1, 0, 1)
local win = am.window{
title = "Hi",
width = 400,
height = 300,
clear_color = blue,
}
win.scene =
am.group()
^ {
am.translate(-150, 0):tag"left_eye"
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(150, 0):tag"right_eye"
^ am.circle(vec2(0, 0), 50, red)
,
am.translate(0, -25)
^ am.rect(-50, -50, 50, 50, yellow)
}
win.scene:action(function(scene)
scene"left_eye".hidden = not win:key_down"a"
scene"right_eye".hidden = not win:key_down"b"
end)
win.scene"left_eye".hidden = true
win.scene"right_eye".hidden = true
Drawing sprites
Sprites can be drawn using sprite nodes. To create a sprite node, pass the name of a .png or .jpg file to the am.sprite()
function and add it to your scene graph.
Let's create a beach scene using the following two images:

beach.jpg

ball.png
Download these images and copy them to the same directory as your main.lua
file. Then copy the following into main.lua
:
local win = am.window{
title = "Beach",
width = 400,
height = 300,
}
win.scene =
am.group()
^ {
am.sprite"beach.jpg"
,
am.translate(0, -60)
^ am.sprite"ball.png"
}
Run the program and you should get something like this:

The children of any scene node are always drawn in order, so first the beach.jpg sprite node is drawn and then the ball.png sprite, with it's corresponding translation, is drawn. This ensures the ball is visible on the beach. If we draw the beach second it would obscure the ball.
If you are using the online editor then you won't be able to load image files. Instead you'll need to draw the sprites using text. See the "Beach ball" example in the online editor for an equivalent example that doesn't load any image files. See also the documentation for am.sprite for how to create sprites with text.
Responding to mouse clicks
Let's make the ball bounce when we click it. We'll add a rotate node so we can make the ball spin when it's in the air. We'll also tag the ball's translate and rotate nodes so we can easily access them:
win.scene =
am.group()
^ {
am.sprite"beach.jpg"
,
am.translate(0, -60):tag"ballt"
^ am.rotate(0):tag"ballr"
^ am.sprite"ball.png"
}
Now add an action to animate the ball when it's clicked:
-- ball state variables:
local ball_pos = vec2(0, -60)
local ball_angle = 0
local velocity = vec2(0)
local spin = 0
-- constants:
local min_pos = vec2(-180, -60)
local max_pos = vec2(180, 500)
-- min and max impulse velocity:
local min_v = vec2(-50, 150)
local max_v = vec2(50, 300)
local gravity = vec2(0, -500)
win.scene:action(function(scene)
-- check if the left mouse button was pressed
if win:mouse_pressed"left" then
local mouse_pos = win:mouse_position()
-- check if the mouse click is on the ball
if math.distance(mouse_pos, ball_pos) < 50 then
-- compute a velocity based on click position
local dir = math.normalize(ball_pos - mouse_pos)
velocity = dir * 300
velocity = math.clamp(velocity, min_v, max_v)
-- set a random spin
spin = math.random() * 4 - 2
end
end
-- update the ball position
ball_pos = ball_pos + velocity * am.delta_time
-- if the ball is on the ground, set the
-- velocity and spin to zero.
if ball_pos.y <= -60 then
velocity = vec2(0)
spin = 0
end
-- clamp the ball position so it doesn't disappear
-- off the edge of the screen
ball_pos = math.clamp(ball_pos, min_pos, max_pos)
-- update the ball angle
ball_angle = ball_angle + spin * am.delta_time
-- update the ball translate and rotate nodes
scene"ballt".position2d = ball_pos
scene"ballr".angle = ball_angle
-- apply gravity to the velocity
velocity = velocity + gravity * am.delta_time
end)
First we create some variables to keep track of the ball's state. We need to track its position, angle, velocity and spin (angular velocity). The ball_pos
and velocity
variables are 2 dimensional vectors, since we want to track position and velocity along both the x and y axes. We could have made separate variables for the x and y components, but using a vec2
is more concise. Note that if the values of the x and y components of the vector are the same, we only need to give the value once, so we just need to write vec2(0)
when initialising the velocity instead of vec2(0, 0)
.
We also create some constants that we'll need. We define the minimum and maximum positions of the ball (min_pos
, max_pos
). We also define the minimum and maximum impulse velocity to apply to the ball when it's clicked (min_v
, max_v
). And finally we create a constant for gravity.
Next comes the action itself. The comments in the body of the action should help you work out what's going on, but here are some things to note:
win:mouse_pressed(button)
can be used to check whether a mouse button was pressed in the last frame.button
can be"left"
,"right"
or"middle"
.win:mouse_position()
returns the current mouse position as avec2
value.math.distance
computes the distance between two vectors.math.clamp
clamps a value between two other values. It works with both numbers and vectors.math.random()
returns a random number between 0 and 1.am.delta_time
is the time since the last frame.- Vector values can be added, subtracted and multiplied just like numbers. You can also combine numbers and vectors, for example when we multiply
gravity
byam.delta_time
. The result ofvec2(x, y) * c
isvec2(x*c, y*c)
.
Playing audio
To play a sound, attach a play
action to a scene node in the current scene. For example to play a sound file bounce.ogg
we could do the following:
scene:action(am.play("bounce.ogg"))
The file bounce.ogg
must exist in the same directory as the script.
To have a sound loop, pass in a second argument of true
, like so:
win.scene:action(am.play("ocean.ogg", true))
Note that Amulet only reads Ogg Vorbis audio files.
Here are some audio files you can try out with the beach ball game we made previously:
And here is the complete code listing for the beach ball game with sounds included. Note that we need an extra variable on_ground
to keep track of whether the ball was on the ground, so we don't play the landing sound every frame.
local win = am.window{
title = "Beach",
width = 400,
height = 300,
}
win.scene =
am.group()
^ {
am.sprite"beach.jpg"
,
am.translate(0, -60):tag"ballt"
^ am.rotate(0):tag"ballr"
^ am.sprite"ball.png"
}
local ball_pos = vec2(0, -60)
local ball_angle = 0
local velocity = vec2(0)
local spin = 0
local min_pos = vec2(-180, -60)
local max_pos = vec2(180, 500)
local min_v = vec2(-50, 150)
local max_v = vec2(50, 300)
local gravity = vec2(0, -500)
local on_ground = true
win.scene:action(function(scene)
-- check if the left mouse button was pressed
if win:mouse_pressed"left" then
local mouse_pos = win:mouse_position()
-- check if the mouse click is on the ball
if math.distance(mouse_pos, ball_pos) < 50 then
-- compute a velocity based on click position
local dir = math.normalize(ball_pos - mouse_pos)
velocity = dir * 300
velocity = math.clamp(velocity, min_v, max_v)
-- set a random spin
spin = math.random() * 4 - 2
-- play bounce sound
scene:action(am.play("bounce.ogg"))
on_ground = false
end
end
-- update the ball position
ball_pos = ball_pos + velocity * am.delta_time
-- if the ball lands on the ground, set the
-- velocity and spin to zero.
if ball_pos.y <= -60 and not on_ground then
velocity = vec2(0)
spin = 0
-- play land sound
scene:action(am.play("land.ogg"))
on_ground = true
end
-- clamp the ball position so it doesn't disappear
-- off the edge of the screen
ball_pos = math.clamp(ball_pos, min_pos, max_pos)
-- update the ball angle
ball_angle = ball_angle + spin * am.delta_time
-- update the ball translate and rotate nodes
scene"ballt".position2d = ball_pos
scene"ballr".angle = ball_angle
-- apply gravity to the velocity
velocity = velocity + gravity * am.delta_time
end)
-- play ocean sound in a loop
win.scene:action(am.play("ocean.ogg", true))
Math

Vectors
Amulet has built-in support for 2, 3 or 4 dimensional vectors. Vectors are typically used to represent things like position, direction or velocity in 2 or 3 dimensional space. Representing RGBA colors is another common use of 4 dimensional vectors.
In Amulet vectors are immutable. This means that once you create a vector, its value cannot be changed. Instead you need to construct a new vector.
Constructing vectors
To construct a vector use one of the functions vec2
, vec3
or vec4
. A vector may be constructed by passing its components as separate arguments to one of these functions, for example:
local velocity = vec3(1, 2, 3)
Passing a single number to a vector constructor will set all components of the vector to that value. For example:
local origin = vec2(0)
sets origin
to the value vec2(0, 0)
.
It's also possible to construct a vector from a combination of other vectors and numbers. The new vector's components will be taken from the other vectors in the order they are passed in. For example:
local bottom_left = vec2(0)
local top_right = vec2(10, 100)
local rect = vec4(bottom_left, top_right)
sets rect
to vec4(0, 0, 10, 100)
Accessing vector components
There are multiple ways to access the components of a vector. The first component can be accessed using any of the fields x
, r
or s
; the second using any of the fields y
, g
or t
; the third using any of the fields z
, b
or p
; and the fourth using any of the fields w
, a
or q
. Here are some examples:
local color = vec4(0.1, 0.3, 0.7, 0.8)
print(color.r..", "..color.g..", "..color.b..", "..color.a)
local point = vec2(5, 2)
print("x="..point.x..", y="..point.y)
A vector's components can also be accessed with 1-based integer indices.
Vectors support the Lua length operator (#
), which returns the number of components of the vector (not its magnitude). This allows for iterating through the components of a vector of unknown size, for example:
local v = vec3(10, 20, 30)
for i = 1, #v do
print(v[i])
end
Swizzle fields
Another way to construct vectors is to recombine the components of an existing vector using swizzle fields, which are special fields whose names consist of a combination of any of the component field characters. A new vector containing the named components will be returned. For example:
local dir = vec3(1, 2, 3)
print(dir.xy)
print(dir.rggb)
print(dir.zzys)
Running the above code results in the following output:
vec2(1, 2)
vec4(1, 2, 2, 3)
vec4(3, 3, 2, 1)
Note: You can pass vectors, matrices and quaternions directly to print
or other functions that expect strings and they will be formatted appropriately.
Vector update syntax
Although you can't directly set the components of a vector, Amulet provides some syntactic sugar to make it easier to create a new vector from an existing vector that has only some fields modified. Say, for example, you had a 3 dimensional vector, v1
, and you wanted to create a new vector, v2
, that had the same components as v1
, except for the y component, which you'd like to be 10. One way to do this would be to write:
v2 = vec3(v1.x, 10, v1.z)
but Amulet also allows you to write:
v2 = v1{y = 10}
You can use this syntax to "update" multiple components and it also supports swizzle fields. For example:
local v = vec4(1, 2, 3, 4)
v = v{x = 5, ba = vec2(6)}
This would set v
to vec4(5, 2, 6, 6)
.
If the values of a swizzle field are going to be updated to the same value (as with ba
above), you can just set the field to the value instead of constructing a vector. So the above could also have been written as:
v = v{x = 5, ba = 6}
Vector arithmetic
You can do arithmetic with vectors using the standard operators +
, -
, *
and /
. If both operands are vectors then they should have the same size and the operation is applied in a component-wise fashion, yielding a new vector of the same size. If one operand is a number then the operation is applied to each component of the vector, yielding a new vector of the same size as the vector operand. For example:
print(vec2(3, 4) + 1)
print(vec3(30) / vec3(3, 10, 5))
print(2 * vec4(1, 2, 3, 4))
produces the following output:
vec2(4, 5)
vec3(10, 3, 6)
vec4(2, 4, 6, 8)
Vectors can be compared by value with ==
like this:
if vec2(x,y) == vec2(0,0) then ...
Also, you can concatenate vectors with strings for easy formatting:
local label = "position: "..vec2(x,y)

Matrices
Amulet has built-in support for 2x2, 3x3 and 4x4 matrices. Matrices are typically used to represent transformations in 2 or 3 dimensional space such as rotation, scaling, translation or perspective projection.
Matrices, like vectors, are immutable.
Constructing matrices
Use one of the functions mat2
, mat3
or mat4
to construct a 2x2, 3x3 or 4x4 matrix.
Passing a single number argument to one of the matrix constructors generates a matrix with all diagonal elements equal to the number and all other elements equal to zero. For example mat3(1)
constructs the 3x3 identity matrix:
You can also pass the individual elements of the matrix as arguments to one of the constructors. These can either be numbers or vectors or a mix of the two. As the constructor arguments are consumed from left to right, the matrix is filled in column by column. For example:
local m = mat3(1, 2, 3,
4, 5, 6,
7, 8, 9)
sets m
to the matrix:
Here's another example:
local m = mat4(vec3(1, 2, 3), 4,
vec4(5, 6, 7, 8),
vec2(9, 10), vec2(11, 12),
13, 14, 15, 16)
This sets m
to the matrix:
Note: Matrix constructors are admittedly somewhat confusing, because when you write the matrix constructor in code the columns are layed out horizontally. This is however the convention used in the OpenGL Shader Language (GLSL).
A matrix can also be constructed by passing an existing matrix to one of the matrix construction functions. If the existing matrix is larger than the new one, the new matrix's elements come from the top-left corner of the existing matrix. Otherwise the top-left corner of the new matrix is filled with the contents of the existing matrix and the rest from the identity matrix. For example:
local m = mat4(mat2(1, 2, 3, 4))
will set m
to the matrix:
Finally a 3x3 or 4x4 rotation matrix can be constructed from a quaternion by passing the quaternion as the single argument to mat3
or mat4
(see quaternions).
Accessing matrix components
The columns of a matrix can be accessed as vectors using 1-based integer indices. The Lua length operator can be used to determine the number of columns. For example:
local matrix = mat2(1, 0, 0, 2)
for i = 1, #matrix do
print(matrix[i])
end
This would produce the following output:
vec2(1, 0)
vec2(0, 2)
Matrix arithmetic
As with vectors the +
, -
, *
and /
operators work with matrices too. When one operand is a number, the result is a new matrix of the same size with the operator applied to each element of the matrix. For example:
local m1 = 2 * mat2(1, 2, 3, 4)
sets m1
to the matrix:
and:
local m2 = mat3(3) - 1
sets m2
to the matrix:
When both operands are matrices, the +
and -
operators work in a similar way to vectors, with the operations applied component-wise. For example:
local m3 = mat2(1, 2, 3, 4) + mat2(0.1, 0.2, 0.3, 0.4)
sets m3
to the matrix:
However, when both operands are matrices, the *
operator computes the matrix product.
If the first operand is a vector and the second is a matrix, then the first operand is taken to be a row vector (a matrix with one row) and should have the same number of columns as the matrix. The result is the matrix product of the row vector and the matrix, which is another row vector.
Similarly if the first argument is a matrix and the second a vector, the vector is taken to be a column vector (a matrix with one column) and the result is the matrix product of the matrix and column vector, which is another column vector.
The /
operator also works when both arguments are matrices and is equivalent to multiplying the first matrix by the inverse of the second.
Matricies can be compared by value with ==
like this:
if m1*m2 == mat4(1) then ...

Quaternions
Quaternions are useful for representing 3D rotations.
Like vectors and matrices they are immutable.
Constructing quaternions
The quat
function is used to construct quaternions. The simplest way to construct a quaternion is to pass an angle (in radians) and a unit 3D vector representing the axis about which the rotation should occur. For example:
local q = quat(math.rad(45), vec3(0, 0, 1))
constructs a quaternion that represents a 45 degree rotation around the z axis. (math.rad
converts degrees to radians).
If the axis argument is omitted then it is taken to be vec3(0, 0, 1)
, so the above is equivalent to:
local q = quat(math.rad(45))
This is a useful shortcut for 2D rotations in the xy plane.
A quaternion can also be constructed from Euler angles. Euler angles are rotations around the x, y and z axes, also known as pitch, yaw and roll. For example:
local q = quat(math.rad(30), math.rad(60), math.rad(20))
constructs a quaternion that represents the rotation you'd end up with if you first rotated 30 degrees around the x axis, then 60 degrees around the y axis and finally 20 degrees around the z axis.
If two unit vector arguments are given, then the quaternion represents the rotation that would be needed to rotate the one vector into into the other. For example:
local q = quat(vec3(1, 0, 0), vec3(0, 1, 0))
The above quaternion represents a rotation of 90 degrees in the xy plane, since it rotates a vector pointing along the x axis to one pointing along the y axis.
A quaternion can be constructed from a 3x3 or 4x4 matrix by passing the matrix as the single argument to quat
.
A quaternion can also be converted to a 3x3 or 4x4 matrix by passing it as the single argument to the mat3
or mat4
functions (see mat-cons).
Finally a quaternion can be constructed from the coefficients of its real and imaginary parts:
local q = quat(w, x, y, z)
w
is the real part and x
, y
and z
are the coeffients of the imaginary numbers \(i\), \(j\) and \(k\).
Quaternion fields
The angle
, axis
, pitch
, roll
, yaw
, w
, x
, y
and z
fields can be used to read the corresponding attributes of a quaternion.
Note: Quaternions use a normalized internal representation, so the value returned by a field might be different from the value used to construct the quaternion. Though the quaternion as a whole represents the equivalent rotation.
You may notice this when comparing quaternions with ==
. For example:
quat(vec3(1,0,0), vec3(0,1,0)) == quat(vec3(-1,0,0), vec3(0,-1,0))
evaluates to true
.
Quaternion operations
Quaternions can be multiplied together using the *
operator. The result of multiplying 2 quaternions is the rotation that results from applying the first quaternion's rotation followed by the second quaternion's rotation.
Multiplying a quaternion by a vector rotates the vector. For example:
local v1 = vec3(1, 0, 0)
local q = quat(math.rad(90), vec3(0, 0, 1))
local v2 = q * v1
would set v2
to the vector vec3(0, 1, 0)
, which is v1
rotated 90 degrees in the xy plain.
A vec2
can be rotated in a similar way (the z component is assumed to be zero and the z component of the result is dropped, yielding another vec2
).
Math functions
The following functions supplement the standard Lua math functions.
math.sign(n)
Returns +1 if n > 0, -1 if n < 0 and 0 if n == 0.
math.fract(v)
Returns the fractional part of v
. If v
is a vector it returns a vector of the same size with each component being the fractional part of the corresponding component in the original vector.
math.clamp(v, min, max)
Clamps a value v
between min
and max
. v
, min
and max
may be vectors. In this case each component is clamped based on the corresponding components in min
and max
.
math.randvec2()
Returns a vec2
with all components set to a random number between 0 and 1.
math.randvec3()
Returns a vec3
with all components set to a random number between 0 and 1.
math.randvec4()
Returns a vec4
with all components set to a random number between 0 and 1.
math.dot(vector1, vector2)
Returns the dot product of two vectors. The vectors must have the same size.
math.cross(vector1, vector2)
Returns the cross product of two 3 dimensional vectors.
math.normalize(vector)
Returns the normalized form of a vector (i.e. the vector that points in the same direction, but whose length is 1). If the given vector has zero length, then a vector of the same size is returned whose first component is 1 and whose remaining components are 0.
math.length(vector)
Returns the length of a vector.
math.distance(vector1, vector2)
Returns the distance between two vectors.
math.inverse(matrix)
Returns the inverse of a matrix.
math.lookat(eye, center, up)
Creates a 4x4 view matrix at eye
, looking in the direction of center
with the y axis of the camera pointing in the same direction as up
.
math.perspective(fovy, aspect, near, far)
Creates a 4x4 matrix for a symmetric perspective-view frustum.
fovy
is the field of view in the y plain, in radians.aspect
is typically the window width divided by its height.near
andfar
are the distances of the near and far clipping plains from the camera (these should be positive).
math.ortho(left, right, bottom, top [, near, far])
Creates a 4x4 orthographic projection matrix. near
and far
are the distance from the viewer of the near and far clipping plains (negative means behind the viewer). Their default values are -1
and 1
.
math.oblique(angle, zscale, left, right, bottom, top [, near, far])
Creates a 4x4 oblique projection matrix. near
and far
are the distance from the viewer of the near and far clipping plains (negative means behind the viewer). Their default values are -1
and 1
.
math.translate4(position)
Creates a 4x4 translation matrix.
position
may be either 2 or 3 numbers (the x, y and z components) or a vec2
or vec3
.
If the z component is omitted it is assumed to be 0.
math.scale4(scaling)
Creates a 4x4 scale matrix.
scaling
may be 1, 2 or 3 numbers or a vec2
or vec3
. If 1 number is provided it is assume to be the x and y components of the scaling and the z scaling is assumed to be 1. If 2 numbers or a vec2
is provided, they are the scaling for the x and y components and z is assumed to be 1.
math.rotate4(rotation)
Creates a 4x4 rotation matrix.
rotation
can be either a quaternion, or an angle (in radians) followed by an optional vec3
axis. If the axis is omitted it is assumed to be vec3(0, 0, 1)
so the rotation becomes a 2D rotation in the xy plane about the z axis.
math.perlin(pos [, period])
Generate perlin noise. pos
can be a 2, 3, or 4 dimensional vector, or a number. If the second argument is supplied then the noise will be periodic with the given period. period
should be of the same type as pos
and its components should be integers greater than 1.
The returned value is between -1 and 1.
math.simplex(pos)
Generate simplex noise. pos
can be a 2, 3, or 4 dimensional vector, or a number.
The returned value is between -1 and 1.
math.mix(from, to, t)
Returns the linear interpolation between from
and to
determined by t
. from
and to
can be numbers or vectors, and must be the same type. t
should be a number between 0 and 1. from
and to
can also be quaternions. In this case math.mix
returns the spherical linear interpolation of the two quaternions.
math.slerp(from, to, t)
Returns the spherical linear interpolation of the two quaternions from
and to
. t
should be a number between 0 and 1. Unlike (math.mix
)[#math.mix] this interpolation takes the shortest path.

Buffers and views
Buffers are contiguous blocks of memory. They are used for storing images, audio and vertex data, or anything else you like.
You can't access a buffer's memory directly. Instead you access a buffer through a view. Views provide a typed array-like interface to the buffer.
Buffers
am.buffer(size)
Returns a new buffer of the given size in bytes.
The buffer's memory will be zeroed.
The #
operator can be used to retrieve the size of a buffer in bytes.
Fields:
usage
: A hint to the graphics driver telling it how this buffer will be used when it's used for vertex attribute data or element indices. Can be one of"static"
(the data won't change often),"dynamic"
(the data will change frequenty), or"stream"
(the data will only be used a few times). The default is"static"
.dataptr
: Returns a pointer to the buffer as a Lualightuserdata
value. The intended use for this is to manipulate the buffer using the LuaJIT FFI library. Readonly.
Methods:
mark_dirty()
: Mark the buffer dirty. This should be called if you update the buffer using thedataptr
field. This will cause data to be copied to any textures or vbos that depend on the buffer when next they are drawn. Note that you don't need to call this method if you're not usingdataptr
to update the buffer, for example if you're updating it through a view - in that case the buffer will automatically be marked dirty.
am.load_buffer(filename)
Loads the given file and returns a buffer containing the file's data, or nil
if the file wasn't found.
am.base64_encode(buffer)
Returns a base64 encoding of a buffer as a string.
am.base64_decode(string)
Converts a base64 string to a buffer.
Views
buffer:view(type [, offset [, stride [, count]]])
Returns a view into buffer
.
type
can be one of the following:
type | size (bytes) | Lua value range | internal range | endianess |
---|---|---|---|---|
"float" |
4 | approx -3.4e38 to 3.4e38 | same | native |
"vec2" |
8 | any vec2 |
same | native |
"vec3" |
12 | any vec3 |
same | native |
"vec4" |
16 | any vec4 |
same | native |
"byte" |
1 | -128 to 127 | same | N/A |
"ubyte" |
1 | 0 to 255 | same | N/A |
"byte_norm" |
1 | -1.0 to 1.0 | -127 to 127 | N/A |
"ubyte_norm" |
1 | 0.0 to 1.0 | 0 to 255 | N/A |
"short" |
2 | -32768 to 32767 | same | native |
"ushort" |
2 | 0 to 65535 | same | native |
"short_norm" |
2 | -1.0 to 1.0 | -32767 to 32767 | native |
"ushort_norm" |
2 | 0.0 to 1.0 | 0 to 65535 | native |
"ushort_elem" |
2 | 1 to 65536 | 0 to 65535 | native |
"int" |
4 | -2147483648 to 2147483647 | same | native |
"uint" |
4 | 0 to 4294967295 | same | native |
"uint_elem" |
4 | 1 to 4294967296 | 0 to 4294967295 | native |
The _norm
types map Lua numbers in the range -1 to 1 (or 0 to 1 for unsigned types) to integer values in the buffer.
The _elem
types are specifically for element array buffers and offset the Lua numbers by 1 to conform to the Lua convention of array indices starting at 1.
All view types currently use the native platform endianess, which happens to be little-endian on all currently supported platforms.
The offset
argument is the byte offset of the first element of the view. The default is 0.
The stride
argument is the distance between consecutive values in the view, in bytes. The default is the size of the view type.
The count
argument determines the number of elements in the view. The underlying buffer must be large enough to accommodate the elements with the given stride. The default is the maximum supported by the buffer with the given stride.
You can read and write to views as if they were Lua arrays (as with Lua arrays, indices start at 1). For example:
local buf = am.buffer(12)
local view = buf:view("float")
view[1] = 1.5
view[2] = view[1] + 2
Attempting to read an index less than 1 or larger than the number of elements will return nil.
You can retrieve the number of elements in a view using the #
operator.
View fields
view.buffer
The buffer associated with the view.
Readonly.
View methods
view:slice(n [, count [, stride_multiplier]])
Returns a new view with the same type as view
that references the same buffer, but which starts at the n
th element of view
and continues for count
elements. If count
is omitted or nil it covers all the elements of view
after and including the n
th. stride_multiplier
can be used to increase the stride of the view and thereby skip elements. It must be a positive integer and defaults to 1 (no skipping).
view:set(val [, start [, count]])
Bulk sets values in a view. This is faster than setting them one at a time.
If val
is a number or vector then this sets all elements of view
to val
.
If val
is a table then the elements are set to their corresponding values from the table.
As a special case, if val
is a table of numbers and the view's type is a vector, then the elements of the table will be used to set the components of the vectors in the view. For example:
local verts = am.buffer(24):view("vec3")
verts:set{1, 2, 3, 4, 5, 6}
print(verts[1]) -- vec3(1, 2, 3)
print(verts[2]) -- vec3(4, 5, 6)
Finally if val
is another view then the elements are set to the corresponding values from that view. The views may be of different types as long as they are "compatible". The types are converted as if each element were set using the Lua code view1[i] = view2[i]
. This means you can't set a number view to a vector view or vice versa.
If start
is given then only elements at that index and beyond will be set. The default value for start
is 1
.
If count
is given then at most that many elements will be set.
am.float_array(table)
Returns a float
view to a newly created buffer and fills it with the values in the given table.
am.byte_array(table)
Returns a byte
view to a newly created buffer and fills it with the values in the given table.
am.ubyte_array(table)
Returns a ubyte
view to a newly created buffer and fills it with the values in the given table.
am.byte_norm_array(table)
Returns a byte_norm
view to a newly created buffer and fills it with the values in the given table.
am.ubyte_norm_array(table)
Returns a ubyte_norm
view to a newly created buffer and fills it with the values in the given table.
am.short_array(table)
Returns a short
view to a newly created buffer and fills it with the values in the given table.
am.ushort_array(table)
Returns a ushort
view to a newly created buffer and fills it with the values in the given table.
am.short_norm_array(table)
Returns a short_norm
view to a newly created buffer and fills it with the values in the given table.
am.ushort_norm_array(table)
Returns a ushort_norm
view to a newly created buffer and fills it with the values in the given table.
am.int_array(table)
Returns an int
view to a newly created buffer and fills it with the values in the given table.
am.uint_array(table)
Returns a uint
view to a newly created buffer and fills it with the values in the given table.
am.int_norm_array(table)
Returns an int_norm
view to a newly created buffer and fills it with the values in the given table.
am.uint_norm_array(table)
Returns a uint_norm
view to a newly created buffer and fills it with the values in the given table.
am.ushort_elem_array(table)
Returns a ushort_elem
view to a newly created buffer and fills it with the values in the given table.
am.uint_elem_array(table)
Returns a uint_elem
view to a newly created buffer and fills it with the values in the given table.
am.vec2_array(table)
Returns a vec2
view to a newly created buffer and fills it with the values in the given table.
The table may contain either vec2
s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have half the number of elements as there are numbers in the table.
am.vec3_array(table)
Returns a vec3
view to a newly created buffer and fills it with the values in the given table.
The table may contain either vec3
s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have a third the number of elements as there are numbers in the table.
am.vec4_array(table)
Returns a vec4
view to a newly created buffer containing the values in the given table.
The table may contain either vec4
s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have a quarter the number of elements as there are numbers in the table.
am.struct_array(size, spec)
Returns a table of views of the given size
as defined by spec
. spec
is a sequence of view name (a string) and view type (also a string) pairs. The returned table can be passed directly to the am.bind
function. The views all use the same underlying buffer.
For example:
local arr = am.struct_array(3, {"vert", "vec2", "color", "vec4"})
arr.vert:set{vec2(-1, 0), vec2(1, 0), vec2(0, 1)}
arr.color:set(vec4(1, 0, 0.5, 1))

Windows and input
Creating a window
am.window(settings)
Creates a new window and returns a handle to it. settings
is a table with any of the following fields:
mode
: Either"windowed"
or"fullscreen"
. A fullscreen window will have the same resolution as the user's desktop. The default is"windowed"
. Not all platforms support windowed mode (e.g. iOS). On these platforms this setting is ignored.width
andheight
: These define the window's default coordinate system. If letterboxing is enabled then this is (-width/2
,-height/2
) in the bottom-left corner and (width/2
,height/2
) in the top-right corner. If letterboxing is disabled, then the coordinate system will extend in the horizontal or vertical directions to ensure an area of at leastwidth
×height
is visible in the center of the window. In either case the centre coordinate will always be (0, 0). The default size is 640×480. Mouse and touch positions as well as rendering will be in this coordinate system unless a custom projection matrix is defined (see below). These also define the physical size of the window in windowed mode, unless thephysical_size
property is given (see below).physical_size
: The initial physical size of the window in windowed mode (avec2
). This is in "screen units" which usually correspond to pixels, but not always. For example on a retina Mac display wherehighdpi
is enabled there are 2 pixels per screen unit. If this property is omitted then thewidth
andheight
properties will be used for the physical window size. Note that this property has no effect on the coordinate system used by the window. Note also that this property only has an effect where the platform supports different sized windows. E.g. on iOS windows always fill the screen, so this property is ignored.title
: The window title.resizable
: Whether the window can be resized by the user (true
orfalse
, defaulttrue
).borderless
: Whether the window has a title bar and border (true
orfalse
, defaultfalse
).highdpi
: Whether to use high DPI resolution if available (true
orfalse
, defaultfalse
).depth_buffer
: Whether the window has a depth buffer (true
orfalse
, defaultfalse
).stencil_buffer
: Whether the window has a stencil buffer (true
orfalse
, defaultfalse
).stencil_clear_value
: The value to clear the stencil buffer with before drawing each frame (an integer between 0 and 255). The default is 0.lock_pointer
:true
orfalse
. When pointer lock is enabled the cursor will be hidden and mouse movement will be set to "relative" mode. In this mode the mouse is tracked infinitely in all directions, i.e. as if there is no edge of the screen to stop the mouse cursor. This is useful for implementing first-person style mouse-look. The default isfalse
.show_cursor
: Whether to show the mouse cursor (true
orfalse
, defaulttrue
).clear_color
: The color (avec4
) used to clear the window each frame before drawing. The default clear color is black (vec4(0, 0, 0, 1)
).letterbox
:true
orfalse
. Indicates whether the original aspect ratio (as determined by thewidth
andheight
settings of the window) should be maintained after a resize by adding black horizontal or vertical bars to the sides of the window. The default istrue
.msaa_samples
: The number of samples to use for multisample anti-aliasing. This must be a power of 2. Use zero (the default) for no anti-aliasing.projection
: A custom projection matrix (amat4
) to be used for the window's coordinate system. If supplied, this matrix is used when transforming mouse or touch event coordinates and is set as the projection matrix for rendering, but does not affect theleft
,right
,top
,bottom
,width
andheight
fields of the window.
Window fields
window.left
The x coordinate of the left edge of the window in the window's default coordinate system.
Readonly.
window.right
The x coordinate of the right edge of the window, in the window's default coordinate system.
Readonly.
window.bottom
The y coordinate of the bottom edge of the window, in the window's default coordinate system.
Readonly.
window.top
The y coordinate of the top edge of the window, in the window's default coordinate system.
Readonly.
window.width
The width of the window in the window's default coordinate system. This will always be equal to the width
setting supplied when the window was created if the letterbox
setting is enabled. Otherwise it may be larger, but it will never be smaller than the width
setting.
Readonly.
window.height
The height of the window in the window's default coordinate space. This will always be equal to the height
setting supplied when the window was created if the letterbox
setting is enabled. Otherwise it may be larger, but it will never be smaller than the height
setting.
Readonly.
window.pixel_width
The real width of the window in pixels.
Readonly.
window.pixel_height
The real height of the window in pixels
Readonly.
window.safe_left
The x coordinate of the left edge of the window's safe area in the window's default coordinate system.
This should be used to position elements that you don't want obscured by e.g. the iPhone X notch.
Readonly.
window.safe_right
The x coordinate of the right edge of the window's safe area, in the window's default coordinate system.
This should be used to position elements that you don't want obscured by e.g. the iPhone X notch.
Readonly.
window.safe_bottom
The y coordinate of the bottom edge of the window's safe area, in the window's default coordinate system.
This should be used to position elements that you don't want obscured by e.g. the iPhone X notch.
Readonly.
window.safe_top
The y coordinate of the top edge of the window's safe area, in the window's default coordinate system.
This should be used to position elements that you don't want obscured by e.g. the iPhone X notch.
Readonly.
window.mode
See window settings.
Updatable.
window.clear_color
See window settings.
Updatable.
window.stencil_clear_value
See window settings.
Updatable.
window.letterbox
See window settings.
Updatable.
window.lock_pointer
See window settings.
Updatable.
window.show_cursor
See window settings.
Updatable.
window.scene
The scene node currently attached to the window. This scene will be rendered to the window each frame.
Updatable.
window.projection
See window settings.
Updatable.
Closing a window
window:close()
Closes the window and quits the application if this was the only window.
Detecting when a window resizes
window:resized()
Returns true if the window's size changed since the last frame.
Detecting key presses
The following functions detect physical key states and are not affected by the keyboard layout preferences selected in the OS (except when targetting HTML).
Keys are represented by one of the following strings:
"a"
"b"
"c"
"d"
"e"
"f"
"g"
"h"
"i"
"j"
"k"
"l"
"m"
"n"
"o"
"p"
"q"
"r"
"s"
"t"
"u"
"v"
"w"
"x"
"y"
"z"
"1"
"2"
"3"
"4"
"5"
"6"
"7"
"8"
"9"
"0"
"enter"
"escape"
"backspace"
"tab"
"space"
"minus"
"equals"
"leftbracket"
"rightbracket"
"backslash"
"semicolon"
"quote"
"backquote"
"comma"
"period"
"slash"
"capslock"
"f1"
"f2"
"f3"
"f4"
"f5"
"f6"
"f7"
"f8"
"f9"
"f10"
"f11"
"f12"
"printscreen"
"scrolllock"
"pause"
"insert"
"home"
"pageup"
"delete"
"end"
"pagedown"
"right"
"left"
"down"
"up"
"numlock"
"kp_divide"
"kp_multiply"
"kp_minus"
"kp_plus"
"kp_enter"
"kp_1"
"kp_2"
"kp_3"
"kp_4"
"kp_5"
"kp_6"
"kp_7"
"kp_8"
"kp_9"
"kp_0"
"kp_period"
"lctrl"
"lshift"
"lalt"
"lgui"
"rctrl"
"rshift"
"ralt"
"rgui"
Keys not listed above are represented as a hash followed by the scancode, for example "#101"
.
window:key_down(key)
Returns true if the given key was down at the start of the current frame.
window:keys_down()
Returns an array of the keys that were down at the start of the current frame.
window:key_pressed(key)
Returns true if the given key's state changed from up to down since the last frame.
Note that if key_pressed
returns true for a particular key, then key_down
will also return true
. Also if key_pressed
returns true
for a particular key then key_released
will return false
for the same key. (If necessary, Amulet will postpone key release events to the next frame to ensure this.)
window:keys_pressed()
Returns an array of all the keys whose state changed from up to down since the last frame.
window:key_released(key)
Returns true if the given key's state changed from down to up since the last frame.
Note that if key_released
returns true for a particular key, then key_down
will return false
. Also if key_released
returns true
for a particular key then key_pressed
will return false
. (If necessary, Amulet will postpone key press events to the next frame to ensure this.)
window:keys_released()
Returns an array of all the keys whose state changed from down to up since the last frame.
Detecting mouse events
window:mouse_position()
Returns the position of the mouse cursor, as a vec2
, in the window's coordinate system.
window:mouse_norm_position()
Returns the position of the mouse cursor in normalized device coordinates, as a vec2
.
window:mouse_pixel_position()
Returns the position of the mouse cursor in pixels where the bottom left corner of the window has coordinate (0, 0), as a vec2
.
window:mouse_delta()
Returns the change in mouse position since the last frame, in the window's coordinate system (a vec2
).
window:mouse_norm_delta()
Returns the change in mouse position since the last frame, in normalized device coordinates (a vec2
).
window:mouse_pixel_delta()
Returns the change in mouse position since the last frame, in pixels (a vec2
).
window:mouse_down(button)
Returns true
if the given button was down at the start of the frame. button
may be "left"
, "right"
or "middle"
.
window:mouse_pressed(button)
Returns true
if the given mouse button's state changed from up to down since the last frame. button
may be "left"
, "right"
or "middle"
.
Note that if mouse_pressed
returns true
for a particular button then mouse_down
will also return true
. Also if mouse_pressed
returns true
for a particular button then mouse_released
will return false
. (If necessary, Amulet will postpone button release events to the next frame to ensure this.)
window:mouse_released(button)
Returns true if the given mouse button's state changed from down to up since the last frame. button
may be "left"
, "right"
or "middle"
.
Note that if mouse_released
returns true
for a particular button then mouse_down
will return false
. Also if mouse_released
returns true
for a particular button then mouse_pressed
will return false
. (If necessary, Amulet will postpone button press events to the next frame to ensure this.)
window:mouse_wheel()
Returns the mouse scroll wheel position (a vec2
).
window:mouse_wheel_delta()
Returns the change in mouse scroll wheel position since the last frame (a vec2
).
Detecting touch events
window:touches_began()
Returns an array of the touches that began since the last frame.
Each touch is an integer and each time a new touch occurs the lowest available integer greater than or equal to 1 is assigned to the touch.
If there are no other active touches then the next touch will always be 1, so if your interface only expects a single touch at a time, you can just use 1 for all touch functions that take a touch argument and any additional touches will be ignored.
window:touches_ended()
Returns an array of the touches that ended since the last frame.
See window:touches_began
for more info about the returned touches.
window:active_touches()
Returns an array of the currently active touches.
See window:touches_began
for more info about the returned touches.
window:touch_began([touch])
Returns true if the specific touch began since the last frame.
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_ended([touch])
Returns true if the specific touch ended since the last frame.
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_active([touch])
Returns true if the specific touch is active.
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_position([touch])
Returns the last touch position in the window's coordinate system (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_norm_position([touch])
Returns the last touch position in normalized device coordinates (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_pixel_position([touch])
Returns the last touch position in pixels, where the bottom left corner of the window is (0, 0) (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_delta([touch])
Returns the change in touch position since the last frame in the window's coordinate system (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_norm_delta([touch])
Returns the change in touch position since the last frame in normalized device coordinates (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_pixel_delta([touch])
Returns the change in touch position since the last frame in pixels (a vec2
).
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_force([touch])
Returns the force of the touch where 0 means no force and 1 is "average" force. Harder presses will result in values larger than 1.
The default value for touch
is 1
.
See window:touches_began
for additional notes on the touch
argument.
window:touch_force_available()
Returns true if touch force is supported on the device.

Scenes
Scene graphs are how you draw graphics in Amulet. Scene nodes are connected together to form a graph which is attached to a window (via the window's scene
field). The window will then render the scene graph each frame.
Scene nodes correspond to graphics commands. They either change the rendering state in some way, for example by applying a transformation or changing the blend mode, or execute a draw command, which renders something to the screen.
Each scene node has a list of children, which are rendered in depth-first, left-to-right order.
A scene node may be the child of multiple other scene nodes, so in general a scene node does not have a unique parent. Cycles are also allowed and are handled by imposing a limit to the number of times a node can be recursively rendered.
Scene graph construction syntax
Special syntax is provided for constructing scene graphs from nodes. The expression:
node1 ^ node2
adds node2
as a child of node1
and returns node1
. The resulting scene graph looks like this:

The expression:
node1 ^ { node2, node3 }
adds both node2
and node3
as children of node1
and returns node1
:

The expression:
node1 ^ { node2, node3 } ^ node4
does the same as the previous expression, except node4
is added as a child of both node2
and node3
:

If node2
or node3
were graphs with multiple nodes, then node4
would be added to the leaf nodes of those graphs.
Here is a more complex example:
node1
^ node2
^ {
node3
^ node4
,
node5
^ {node6, node7, node8}
}
^ node9
^ node10
The above expression results in the following graph:

Scene node common fields and methods
The following fields and methods are common to all scene nodes.
node.hidden
Determines whether the node and its children are rendered. The default is false
, meaning that the node is rendered.
Updatable.
node.paused
Determines whether the node and its children's actions are executed. The default is false
, meaning the actions are executed.
Note that a descendant node's actions might still be executed if it is has another, non-paused, parent.
Updatable.
node.num_children
Returns the node's child count.
Readonly.
node.recursion_limit
This determines the number of times the node will be rendered recursively when part of a cycle in the scene graph. The default is 8.
Updatable.
node:tag(tagname)
Adds a tag to a node and returns the node. tagname
should be a string.
Note that most scene nodes receive a default tag name when they are created. See the documentation of the different nodes below for what these default tags are.
No more than 65535 unique tag names may be created in a single application.
node:untag(tagname)
Removes a tag from a node and returns the node.
node:all(tagname [, recurse])
Searches node
and all its descendants for any nodes with the tag tagname
and returns them as a table.
The recurse
boolean argument determines if all
recursively searches the descendents of matched nodes. The default value is false
.
The returned table has a metatable that allows setting a field on all nodes by setting the corresponding field on the table. So for example to set the color of all sprite nodes that are descendents of a parent node, one might do the following:
parent_node:all"sprite".color = vec4(1, 0, 0, 1)
node(tagname)
Searches node
and its descendants for tagname
and returns the first matching node found, or nil
if no matching nodes were found. The search is depth-first left-to-right.
The found node's parent is also returned as a second value, unless the found node was the root of the given subgraph.
node:action([id,] action)
Attaches an action to a node and returns the node.
action
may be a function or a coroutine.
The action function will be called exactly once per frame as long as the node is part of a scene graph that is attached to a window. If a coroutine is used, it will be run until it yields or finishes. The action will be run for the first time on the frame after it was attached to the node.
The action function may accept a single argument which is the node to which it is attached. If a coroutine is used, then the node is returned by coroutine.yield()
.
If the action function returns true
then the action will be removed from the node and not run again. Similarly if a coroutine yields true
or finishes.
Each action has an ID. If the id
argument is omitted, then its ID is the action
argument itself. If present, id
may be a value of any type besides nil, a function or coroutine (typically it's a string).
Multiple actions may be attached to a scene node, but they must all have unique ids. If you attempt to attach an action with an ID that is already used by another action on the same node, then the other action will be removed before the new one is attached.
The order that actions are run is determined by the node's position in the scene graph. Each node is visited in depth-first, left-to-right order and the actions on each node are run in the order they were added to the node. Each action is never run more than once per frame, even if the node occurs multiple times in the graph or is part of a cycle. For example, given the following scene graph:

The nodes will be visited in this order:
node1
node2
node4
node5
node7
node6
node3
Note that the action execution order is determined before the first action runs each frame and is not affected by any modifications to the scene graph made by actions running during the frame. Any modifications to the scene graph will only affect the order of actions in subsequent frames.
node:late_action([id,] action)
Attach a late action to a scene node. Late actions are the same as normal actions, except they are run after all normal actions are finished.
See also node:action.
node:cancel(id)
Cancels an action.
node:update()
Executes actions on a node and its descendants. Actions are still only run once per frame, so if the give node's actions have already been run, they won't run again.
Use this method to execute actions on nodes that are not attached to the window, for example nodes that are being manually rendered into a framebuffer.
node:append(child)
Appends child
to the end of node
's child list and returns node
.
node:prepend(child)
Adds child
to the start of node
's child list and returns node
.
node:remove(child)
Removes the first occurrence of child
from node
's child list and returns node
.
node:remove(tagname)
Searches for a node with tag tagname
in the descendants of node
and removes the first one it finds. Then returns node
.
node:remove_all()
Removes all of node
's children and returns node
.
node:replace(child, replacement)
Replaces the first occurrence of child
with replacement
in node
's child list and returns node
.
node:replace(tagname, replacement)
Searches for a node with tag tagname
in the descendants of node
and replaces the first one it finds with replacement
. Then returns node
.
node:child(n)
Returns the nth child of node
(or nil
if there is no such child).
node:child_pairs()
Returns an iterator over all node
's children. For example:
for i, child in node:child_pairs() do
-- do something with child
end
Basic nodes
am.group(children)
Group nodes are only for grouping child nodes under a common parent. They have no other effect. The children can be passed in as a table.
Default tag: "group"
.
Example:
local group_node = am.group{node1, node2, node3}
am.text([font, ] string [, color] [, halign [, valign]])
Renders some text.
font
is an object generated using the sprite packing tool. If omitted, the default font will be used, which is a monospace font of size 16px.
color
should be a vec4
. The default color is white.
halign
and valign
specify horizontal and vertical alignment. The allowed values for halign
are "left"
, "right"
and "center"
. The allowed values for valign
are "bottom"
, "top"
and "center"
. The default in both cases is "center"
.
Fields:
text
: The text to display. Updatable.color
: The color of the text. Updatable.width
: The width of the displayed text in pixels. Readonly.height
: The height of the displayed text in pixels. Readonly.
Default tag: "text"
.
am.sprite(source [, color] [, halign [, valign]])
Renders a sprite (an image).
source
can be either a filename, an ASCII art string or a sprite spec.
When source
is a filename, that file is loaded and displayed as the sprite. Currently only .png
and .jpg
files are supported. Note that loaded files are cached, so each file will only be loaded once.
source
may also be an ASCII art string. This is a string with at least one newline character. Each row in the string represents a row of pixels. Here's an example:
local face = [[
..YYYYY..
.Y.....Y.
Y..B.B..Y
Y.......Y
Y.R...R.Y
Y..RRR..Y
.Y.....Y.
..YYYYY..
]]
am.window{}.scene = am.scale(20) ^ am.sprite(face)
The resulting image looks like this:

face
The mapping from characters to colors is determined by the am.ascii_color_map
table. By default this is defined as:
am.ascii_color_map = {
W = vec4(1, 1, 1, 1), -- full white
w = vec4(0.75, 0.75, 0.75, 1), -- silver
K = vec4(0, 0, 0, 1), -- full black
k = vec4(0.5, 0.5, 0.5, 1), -- dark grey
R = vec4(1, 0, 0, 1), -- full red
r = vec4(0.5, 0, 0, 1), -- half red (maroon)
Y = vec4(1, 1, 0, 1), -- full yellow
y = vec4(0.5, 0.5, 0, 1), -- half yellow (olive)
G = vec4(0, 1, 0, 1), -- full green
g = vec4(0, 0.5, 0, 1), -- half green
C = vec4(0, 1, 1, 1), -- full cyan
c = vec4(0, 0.5, 0.5, 1), -- half cyan (teal)
B = vec4(0, 0, 1, 1), -- full blue
b = vec4(0, 0, 0.5, 1), -- half blue (navy)
M = vec4(1, 0, 1, 1), -- full magenta
m = vec4(0.5, 0, 0.5, 1), -- half magenta
O = vec4(1, 0.5, 0, 1), -- full orange
o = vec4(0.5, 0.25, 0, 1), -- half orange (brown)
}
but you can modify it as you please (though this must be done before creating a sprite).
Any characters not in the color map will come out as transparent pixels, except for space characters which are ignored.
The third kind of source is a sprite spec. Sprite specs are usually generated using the sprite packing tool, though you can create them manually as well if you like.
You can define your own sprite spec by supplying a table with all of the following fields:
texture
: the texture containing the sprite.s1
: the left texture coordinate (0 to 1)t1
: the bottom texture coordinate (0 to 1)s2
: the right texture coordinate (0 to 1)t2
: the top texture coordinate (0 to 1)x1
: the left offset of the sprite (in pixels)y1
: the bottom offset of the sprite (in pixels)x2
: the right offset of the sprite (in pixels)y2
: the top offset of the sprite (in pixels)width
: the width of the sprite (in pixels)height
: the height of the sprite (in pixels)
Typically x1
and y1
would both be zero and x2
and y2
would be equal to width
and height
, though they may be different when transparents pixels are removed from the edges of sprites when packing them. The width
and height
fields are used for adjusting sprite position based on the requested alignment.
The color
argument is a vec4
that applies a tinting color to the sprite. The default is white (no tinting).
The halign
and valign
arguments determine the alignment of the sprite. The allowed values for halign
are "left"
, "right"
and "center"
. The allowed values for valign
are "bottom"
, "top"
and "center"
. The default in both cases is "center"
.
Fields:
source
: The sprite source (filename, ascii art string or sprite spec). Updatable.color
: The sprite tint color as avec4
. Updatable.width
: The width of the sprite in pixels.height
: The height of the sprite in pixels.spec
: The sprite spec table, from which you can retrieve the texture, texture coordinates and vertices. This is available even if the sprite wasn't created with a sprite spec. Readonly.
Default tag: "sprite"
.
am.rect(x1, y1, x2, y2 [, color])
Draws a rectangle from (x1
, y1
) to (x2
, y2
).
color
should be a vec4
and defaults to white.
Fields:
x1
: The left coordinate of the rectangle. Updatable.y1
: The bottom coordinate of the rectangle. Updatable.x2
: The right coordinate of the rectangle. Updatable.y2
: The top coordinate of the rectangle. Updatable.color
: The color of the rectangle as avec4
. Updatable.
Default tag: "rect"
.
am.circle(center, radius [, color [, sides]])
Draws a circle or regular polygon.
center
should be a vec2
.
color
should be a vec4
. The default is white.
sides
is the number of sides to use when rendering the circle. The default is 255. You can change this to make other regular polygons. For example change it to 6 to draw a hexagon.
Fields:
center
: The circle center as avec2
. Updatable.radius
: The circle radius. Updatable.color
: The circle color as avec4
. Updatable.
Default tag: "circle"
.
am.line(point1, point2 [, thickness [, color]])
Draws a line from point1
to point2
.
point1
and point1
should be vec2
s.
thickness
should be a number. The default is 1.
color
should be a vec4
. The default is white.
Fields:
point1
: Updatable.point2
: Updatable.thickness
: Updatable.color
: Updatable.
Default tag: "line"
.
am.particles2d(settings)
Renders a simple 2D particle system.
settings
should be a table with any of the following fields:
source_pos
: The position where the particles emit from (vec2
)source_pos_var
: The source position variation (vec2
)start_size
: The start size of the particles (number)start_size_var
: The start size variation (number)end_size
: The end size of the particles (number)end_size_var
: The end size variation (number)angle
: The angle the particles emit at (radians)angle_var
: The variation in the angle the particles emit at (radians)speed
: The speed of the particles (number)speed_var
: The variation in the speed of the particles (number)life
: The lifetime of the particles (seconds)life_var
: The variation in lifetime of the particles (seconds)start_color
: The start color of the particles (vec4
)start_color_var
: The variation in the start color of the particles (vec4
)end_color
: The end color of the particles (vec4
)end_color_var
: The variation in the end color of the particles (vec4
)emission_rate
: The number of particles to emit per secondstart_particles
: The initial number of particlesmax_particles
: The maximum number of particlesgravity
: Gravity to apply to the particles (vec2
)damping
: Slows down particles if greater than zerosprite_source
: The particle sprite source (see am.sprite). If this is omitted the particles will be colored squares.warmup_time
: Simulate running the particle system for this number of seconds before showing it for the first time.
In the table the _var
fields are an amount that is added to and subtracted from the corresponding field (the one without the _var
suffix) to get the range of values from which one is randomly chosen. For example if source_pos
is vec2(1, 0)
and source_pos_var
is vec2(3, 2)
, then source positions will be chosen in the range vec2(-2, -2)
to vec2(4, 2)
.
All of the sprite settings are exposed as updatable fields on the particles node.
Note that no blending is applied to the particles, so if you want alpha on your particles, then you need to add a am.blend
node. For example:
local node = am.blend("add_alpha")
^ am.particles2d{
source_pos = win:mouse_position(),
source_pos_var = vec2(20),
max_particles = 1000,
emission_rate = 500,
start_particles = 0,
life = 0.4,
life_var = 0.1,
angle = math.rad(90),
angle_var = math.rad(180),
speed = 200,
start_color = vec4(1, 0.3, 0.01, 0.5),
start_color_var = vec4(0.1, 0.05, 0.0, 0.1),
end_color = vec4(0.5, 0.8, 1, 1),
end_color_var = vec4(0.1),
start_size = 30,
start_size_var = 10,
end_size = 2,
end_size_var = 2,
gravity = vec2(0, 2000),
}
Methods:
reset()
: resets the particles as if they had just been created with their current settings.
Default tag: "particles2d"
.

Transformation nodes
The following nodes apply transformations to all their descendants.
Note: These nodes have an optional uniform
argument in the first position of their construction functions. This argument is only relevant if you're writing your own shader programs. Otherwise you can ignore it.
am.translate([uniform,] position)
Apply a translation to a 4x4 matrix uniform. uniform
is the uniform name as a string. It is "MV"
by default.
position
may be either 2 or 3 numbers (the x, y and z components) or a vec2
or vec3
.
If the z component is omitted it is assumed to be 0.
Fields:
position
: The translation position as avec3
. Updatable.position2d
: The translation position as avec2
. Updatable.x
,y
,z
: Thex
,y
andz
components of the position. Updatable.
Default tag: "translate"
.
Examples:
local node1 = am.translate(10, 20)
local node2 = am.translate(vec2(10, 20))
local node3 = am.translate("MyModelViewMatrix", 1, 2, -3.5)
local node4 = am.translate(vec3(1, 2, 3))
node1.position2d = vec2(30, 40)
node2.x = 40
node2.y = 50
node3.position = vec3(1, 2, -3)
am.scale([uniform,] scaling)
Apply a scale transform to a 4x4 matrix uniform. uniform
is the uniform name as a string. It is "MV"
by default.
scaling
may be 1, 2 or 3 numbers or a vec2
or vec3
. If 1 number is provided it is assume to be the x and y components of the scaling and the z scaling is assumed to be 1. If 2 numbers or a vec2
is provided, they are the scaling for the x and y components and z is assumed to be 1.
Fields:
scale
: The scale as avec3
. Updatable.scale2d
: The scale as avec2
. Updatable.x
,y
,z
: Thex
,y
andz
components of the scale. Updatable.
Default tag: "scale"
.
Examples:
local node1 = am.scale(2)
local node2 = am.scale(2, 1)
local node3 = am.scale(vec2(1, 2))
local node4 = am.scale("MyModelViewMatrix", vec3(0.5, 2, 3))
node1.scale2d = vec2(1)
node2.x = 3
node4.scale = vec3(1, 3, 2)
am.rotate([uniform,] rotation)
Apply a rotation to a 4x4 matrix uniform. uniform
is the uniform name as a string. It is "MV"
by default.
rotation
can be either a quaternion, or an angle (in radians) followed by an optional vec3
axis. If the axis is omitted it is assumed to be vec3(0, 0, 1)
so the rotation becomes a 2D rotation in the xy plane about the z axis.
Fields:
rotation
: The rotation as aquat
. Updatable.angle
: The rotation angle in radians. Updatable.axis
: The rotation axis as avec3
. Updatable.
Default tag: "rotate"
.
Examples:
local node1 = am.rotate(math.rad(45))
local node2 = am.rotate(math.pi/4, vec3(0, 1, 0))
local node3 = am.rotate("MyModelViewMatrix",
quat(math.pi/6, vec3(1, 0, 0)))
node1.angle = math.rad(60)
node2.axis = vec3(0, 0, 1)
node3.rotation = quat(math.rad(60), vec3(0, 0, 1))
am.transform([uniform,] matrix)
Pre-multiply a 4x4 matrix uniform by the given 4x4 matrix. uniform
is the uniform name as a string (default is "MV"
).
Fields:
mat
: The matrix to multiply the uniform by. Updatable.
Default tag: "transform"
.
Advanced nodes
am.use_program(program)
Sets the shader program to use when rendering descendants. A program
object can be created using the am.program
function.
Fields:
program
: The shader program to use. Updatable.
Default tag: "use_program"
.
am.bind(bindings)
Binds shader program parameters (uniforms and attributes) to values. bindings
is a table mapping shader parameter names to values.
The named parameters are matched with the uniforms and attributes in the shader program just before a am.draw
node is executed.
Program parameter types are mapped to the following Lua types:
Program parameter type | Lua type |
---|---|
float uniform |
number |
vec2 uniform |
vec2 |
vec3 uniform |
vec3 |
vec4 uniform |
vec4 |
mat2 uniform |
mat2 |
mat3 uniform |
mat3 |
mat4 uniform |
mat4 |
sampler2D uniform |
texture2d |
float attribute |
view("float") |
vec2 attribute |
view("vec2") |
vec3 attribute |
view("vec3") |
vec4 attribute |
view("vec4") |
Any bound parameters not in the program are ignored, but all program parameters must have been bound before a draw
node is executed.
Note: The parameter P
is initially bound to a 4x4 projection matrix defined by the window's coordinate system, while the parameter MV
(the default model view matrix) is initially bound to the 4x4 identity matrix.
The bound parameters are available as updatable fields on the bind node. The fields have the same names as their corresponding parameters.
Default tag: "bind"
.
Example:
local bind_node = am.bind{
P = mat4(1),
MV = mat4(1),
color = vec4(1, 0, 0, 1),
vert = am.vec2_array{
vec2(-1, -1),
vec2(0, 1),
vec2(1, -1)
}
}
-- update a parameter
bind_node.color = vec4(0, 1, 1, 1)
am.draw(primitive [, elements] [, first [, count]])
Draws the currently bound vertices using the current shader program with the currently bound parameter values.
primitive
can be one of the following:
"points"
"lines"
"line_strip"
"line_loop"
"triangles"
"triangle_strip"
"triangle_fan"
Note that "line_loop"
and "triangle_fan"
may be slow on some systems.
elements
, if supplied, should be a ushort_elem
or uint_elem
view containing 1-based attribute indices. If omitted the attributes are rendered in order as if elements
were 1, 2, 3, 4, 5, ... etc. See also buffers and views.
first
specifies where in the list of vertices to start drawing (starting from 1). The default is 1.
count
specifies how many vertices to draw. The default is as many as are supplied through bound vertex attributes and the elements
view if present.
Fields:
primitive
: The primitive to draw. Updatable.elements
: The elements view. Updatable.first
: The first vertex to draw. Updatable.count
: The number of vertices to draw. Updatable.
Default tag: "draw"
.
Here is a complete example that draws a triangle with red, green and blue corners using am.use_program
, am.bind
and am.draw
nodes:
local win = am.window{}
local prog = am.program([[
precision highp float;
attribute vec2 vert;
attribute vec4 color;
uniform mat4 MV;
uniform mat4 P;
varying vec4 v_color;
void main() {
v_color = color;
gl_Position = P * MV * vec4(vert, 0.0, 1.0);
}
]], [[
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
]])
win.scene =
am.use_program(prog)
^ am.bind{
P = mat4(1),
MV = mat4(1),
color = am.vec4_array{
vec4(1, 0, 0, 1),
vec4(0, 1, 0, 1),
vec4(0, 0, 1, 1)
},
vert = am.vec2_array{
vec2(-1, -1),
vec2(0, 1),
vec2(1, -1)
}
}
^ am.draw"triangles"
The resulting image looks like this:

am.blend(mode)
Set the blending mode.
The possible values for mode
are:
"off"
"alpha"
"premult"
"add"
"subtract"
"add_alpha"
"subtract_alpha"
"multiply"
"invert"
Fields:
mode
: Updatable.
am.color_mask(red, green, blue, alpha)
Apply a color mask. The four arguments can be true
or false
and determine whether the corresponding color channel is updated in the rendering target (either the current window or framebuffer being rendered to).
For example using a mask of am.color_mask(false, true, false, true)
will cause only the green and alpha channels to be updated.
Fields:
red
: Updatable.green
: Updatable.blue
: Updatable.alpha
: Updatable.
Default tag: "color_mask"
.
am.cull_face(face)
Culls triangles with a specific winding.
The possible values for face
are:
"back"
: Cull back-facing triangles (same as"cw"
below)"front"
: Cull front-facing triangles (same as"ccw"
below)"cw"
: Cull clockwise wound triangles."ccw"
: Cull counter-clockwise wound triangles."none"
: Do not cull any triangles.
Fields:
face
: Updatable.
Default tag: "cull_face"
.
am.depth_test(func [, mask])
Sets the depth test function and mask. The window or framebuffer being rendered to needs to have a depth buffer for this to have any effect.
func
is used to determine whether a fragment is rendered by comparing the depth value of the fragment to the value in the depth buffer. The possible values for func
are:
"never"
"always"
"equal"
"notequal"
"less"
"lequal"
"greater"
"gequal"
mask
determines whether the fragment depth is written to the depth buffer. The possible values are true
and false
. The default is true
.
Fields:
func
: Updatable.mask
: Updatable.
Default tag: "depth_test"
.
am.stencil_test(settings)
Sets the stencil test and mask. The window or framebuffer being rendered to needs to have a stencil buffer for this to have any effect.
settings
should be a table with any combination of the following fields:
enabled
: whether the stencil test is enabled (true
/false
, defaultfalse
)ref
: the reference value to use in the test function (must be an integer between 0 and 255, default 0).read_mask
: a bitmask to apply to the reference value and the stencil buffer value before doing the test (must be an integer between 0 and 255, default 255).write_mask
: a bitmask that controls which stencil buffer bits can be written to (must be an integer between 0 and 255, default 255).func_front
: the test function to use for front-facing (CCW) triangles ("never"
,"always"
,"equal"
,"notequal"
,"less"
,"lequal"
,"greater"
or"gequal"
, default"always"
). The function compares a supplied reference value with the value in the stencil buffer.op_fail_front
: The operation to perform if the stencil test fails (see below for possible values).op_zfail_front
: The operation to perform if the stencil test passes, but the depth test fails (see below for possible values).op_zpass_front
: The operation to perform if the stencil test passes and the depth test passes (see below for possible values).func_back
: same asfunc_front
, but for back-facing (CW) triangles.op_fail_back
: same asop_fail_front
, but for back-facing (CW) triangles.op_zfail_back
: same asop_zfail_front
, but for back-facing (CW) triangles.op_zpass_back
: same asop_zpass_front
, but for back-facing (CW) triangles.
The op_fail_front
, op_zfail_front
and op_zpass_front
fields (and the analogous fields for back-facing triangles) determine how the stencil buffer is modified. They can be one of the following values:
"keep"
: keeps the existing stencil buffer value"zero"
: sets the stencil buffer to zero"replace"
: replaces the stencil buffer value with the supplied reference value"invert"
: invert the bits of the stencil buffer"incr"
: increment the stencil buffer value"decr"
: decrement the stencil buffer value"incr_wrap"
: increment the stencil buffer value, wrapping back to zero on overflow (> 255)"decr_wrap"
: decrement the stencil buffer value, wrapping to 255 on underflow
All the settings can be updated after the depth test node has been created using fields on the node with the corresponding names.
Default tag: "stencil_test"
.
am.viewport(left, bottom, width, height)
Set the viewport, which is the rectangular area of the window into which rendering will occur.
left
and bottom
is the bottom-left corner of the viewport in pixels, where the bottom-left corner of the window is (0, 0). width
and height
are also in pixels.
Fields:
left
,bottom
,width
,height
: Updatable.
Default tag: "viewport"
.
am.lookat([uniform,] eye, center, up)
Sets uniform
to the "lookat matrix" which looks from eye
(a vec3
) to center
(a vec3
), with up
(a unit vec3
) as the up direction.
This node can be thought of a camera positioned at eye
and facing the point center
.
The default value for uniform
is "MV"
.
Fields:
eye
: The camera position (vec3
). Updatable.center
: A point the camera is facing (vec3
). Updatable.up
: The up direction of the camera (vec3
). Updatable.
Default tag: "lookat"
.
am.cull_sphere([uniforms...,] radius [, center])
This first takes the matrix product of the given uniforms (which should be mat4
s). Then it determines whether the sphere with the given center and radius would be visible using the previously computed matrix product as the model-view-projection matrix. If it wouldn't be visible then none of this node's children are rendered (i.e. they are culled).
The default value for uniforms
is "P" and "MV"
and the default value for center
is vec3(0)
.
Fields:
radius
: Updatable.center
: Updatable.
Default tag: "cull_sphere"
.
am.cull_box([uniforms...,] min, max)
This first takes the matrix product of the given uniforms (which should be mat4
s). Then it determines whether the box with the min and max coordinates (min
and max
are vec3
s) would be visible using the computed matrix product as the model-view-projection matrix. If it wouldn't be visible then none of this node's children are rendered.
The default value for uniforms
is "P" and "MV"
.
Fields:
min
: Updatable.max
: Updatable.
Default tag: "cull_box"
.
am.billboard([uniform,] [preserve_scaling])
Removes rotation from uniform
, which should be a mat4
. By default uniform
is "MV"
.
If preserve_scaling
is false
or omitted then any scaling will also be removed from the matrix. If it is true
, then scaling will be preserved, as long as it's the same across all three axes.
Default tag: "billboard"
am.read_uniform(uniform)
This node has no effect on rendering. Instead it records the value of the named uniform when rendering occurs.
This is useful for finding the value of the model-view matrix (MV
) at a specific node without having to keep track of all the ancestor transforms. This could then be used to, for example, determine the position of a mouse click in a node's coordinate space, by taking the inverse of the model-view matrix.
Fields:
value
: The value of the uniform, or nil if the node hasn't been rendered yet, or the named uniform wasn't set in an ancestor node.
Default tag: "read_uniform"
.
am.quads(n, spec [, usage])
Returns a node that renders a set of quads. The returned node is actually an am.bind
node with an am.draw
node child. i.e. no program or blending is defined -- these must be created separately as parent nodes.
n
is the initial capacity. Set this to the number of quads you think you'll want to render. It doesn't matter if it's too small as the capacity will be increased as required, though it's slightly faster if no capacity increases are required.
spec
is a table of attribute name and type pairs (the same as used for am.struct_array
).
usage
is an optional hint to the graphics driver about how the quads will be used. See the usage
property of am.buffer
for more details.
Fields:
num_quads
: The number of quads. This is zero initially.
Methods:
add_quad(data)
: adds a quad to be rendered and returns the quad number.data
is a table where the keys are attribute names and the values are the values of the 4 vertices of the quad. The values can be specified in several ways:- as a table where each each element is the value for the left-top, left-bottom, right-bottom and right-top corners of the quad.
- a single value for all corners.
- a view containing the values for the elements As with the
view:set
method, if the attribute is a vector, a table of numbers is also accepted. The quad number (starting at 1) is returned.
remove_quad(n [,count])
: Removescount
quads starting with then
th.count
is 1 if omitted.clear()
: Removes all quads.- Additionally methods are created for each attribute of the form
quad_<attribute name>
that can be used to update the value of a quad attribute. The signature of the method is:quad_attr(n, values)
wheren
is the quad number andvalues
has the same meaning as in theadd_quad
method.
Default tag: "quads"
.
Example:
local quads = am.quads(2, {"vert", "vec2", "color", "vec3"})
quads:add_quad{vert = {vec2(-100, 0), vec2(-100, -100),
vec2(0, -100), vec2(0, 0)},
color = {vec3(1, 0, 0), vec3(0, 1, 0),
vec3(0, 0, 1), vec3(1, 1, 1)}}
quads:add_quad{vert = {vec2(0, 100), vec2(0, 0),
vec2(100, 0), vec2(100, 100)},
color = {vec3(1, 0, 0), vec3(0, 1, 0),
vec3(0, 0, 1), vec3(1, 1, 1)}}
local win = am.window{}
local prog = am.program([[
precision highp float;
attribute vec2 vert;
attribute vec3 color;
uniform mat4 MV;
uniform mat4 P;
varying vec3 v_color;
void main() {
v_color = color;
gl_Position = P * MV * vec4(vert, 0.0, 1.0);
}
]], [[
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}
]])
win.scene = am.use_program(prog) ^ quads
The above program produces the following output:

am.postprocess(settings)
Allows for post-processing of a scene. First the children of the postprocess
node are rendered into a texture, then the texture is rendered to the entire window using a user-supplied shader program.
settings
is a table containing any number of the following fields:
width
: the width of the texture to render the children into. If omitted the window width is used.height
: the height of the texture to render the children into. If omitted the window height is used.minfilter
: the minfilter of the texture. The default is"nearest"
.magfilter
: the magfilter of the texture. The default is"nearest"
.depth_buffer
: whether there should be a depth buffer when rendering the scene. The default is false.stencil_buffer
: whether there should be a stencil buffer when rendering the scene. The default is false.clear_color
: The color to clear the texture to before rendering each frame (avec4
). The default is black.auto_clear
: Whether to automatically clear the texture before rendering each frame. The default is true.program
: The shader program to use to render the texture.
The shader program should expect the following uniforms and attributes:
uniform sampler2D tex;
attribute vec2 vert;
attribute vec2 uv;
Note that if either width
or height
are set then they must both be set.
Fields:
clear_color
: The color to clear the texture to before rendering each frame (avec4
). Updatable.auto_clear
: Whether to automatically clear the texture before rendering each frame. Updatable.program
: The shader program to use to render the texture. Updatable.
Methods:
clear()
: Clear the texture manually.
Default tag: "postprocess"
.
Creating custom scene nodes
Creating a custom leaf node is relatively simple: just create a function that constructs the graph you want and return it.
You can add custom methods by setting the appropriate fields on the root node of the returned graph (any node can have any number of custom fields set on it, as long as they don't clash with pre-defined fields).
If you define methods of the form:
function node:get_FIELD()
...
end
function node:set_FIELD(val)
...
end
Then you will be able to access FIELD
as if it's a regular field and the appropriate access method will be called.
For example:
node.FIELD = val
will be equivalent to:
node:set_FIELD(val)
If you want a readonly field, just define the get_FIELD
method and not the set_FIELD
method.
The am.text
, am.sprite
and am.particles2d
nodes are all implemented this way. Their source is here and here.
If you want to create a non-leaf node, then you need to use the am.wrap
function:
am.wrap(node)
This "wraps" node
inside a special type of node called a wrap node.
When a wrap node is rendered it renders the inner node. However any nodes added as children of a wrap node are also added to the leaf node(s) of the inner node.
For example suppose we want to create a transformation node called move_and_rotate
that does both a translation and a rotation:
function move_and_rotate(x, y, degrees)
return am.translate(x, y) ^ am.rotate(math.rad(degrees))
end
We would like to be able to create such a node and add children to it. Like so:
local mvrot = move_and_rotate(10, 20, 60)
mvrot:append(am.rect(-10, -10, 10, 10))
However what this will do is add the rect
node as a child of the translate
node returned by move_and_rotate
.
Instead we need to do:
mvrot"rotate":append(am.rect(-10, -10, 10, 10))
which is a bit clunky.
A wrap node solves this problem:
function move_and_rotate(x, y, degrees)
return am.wrap(am.translate(x, y) ^ am.rotate(math.rad(degrees)))
end
local mvrot = move_and_rotate(10, 20, 60)
mvrot:append(am.rect(-10, -10, 10, 10))
For completeness here we add some fields to set the x, y and degrees properties of our new node:
function move_and_rotate(x, y, degrees)
local inner = am.translate(x, y) ^ am.rotate(math.rad(degrees))
local wrapped = am.wrap(inner)
function wrapped:get_x()
return x
end
function wrapped:set_x(v)
x = v
inner.position2d = vec2(x, y)
end
function wrapped:get_y()
return y
end
function wrapped:set_y(v)
y = v
inner.position2d = vec2(x, y)
end
function wrapped:get_degrees()
return degrees
end
function wrapped:set_degrees(v)
degrees = v
inner"rotate".angle = math.rad(degrees)
end
return wrapped
end
local mvrot = move_and_rotate(-100, -100, 0)
mvrot:append(am.rect(-50, -50, 50, 50))
mvrot.x = 100
mvrot.y = 100
mvrot.degrees = 45
There are some caveats when using wrap nodes:
- The inner node is not considered part of the scene graph for the purpose of running actions. So any actions need to be attached to the wrap node, not the inner node.
- Tag search functions do not search the inner node.
The am.postprocess
node is implemented using a wrap node. You can view it's implementation here.
Shader programs
Compiling a shader program
am.program(vertex_shader, fragment_shader)
Compiles and returns a shader program for use with am.use_program
nodes.
vertex_shader
and fragment_shader
should be the sources for the vertex and fragment shaders in the OpenGL ES shader language version 1. This is the same shader language supported by WebGL 1.
Example:
local vert_shader = [[
precision highp float;
attribute vec3 vert;
uniform mat4 MV;
uniform mat4 P;
void main() {
gl_Position = P * MV * vec4(vert, 1);
}
]]
local frag_shader = [[
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0, 0.5, 1.0);
}
]]
local prog = am.program(vert_shader, frag_shader)
Image buffers
An image buffer represents a 2D image in memory. Each pixel occupies 4 bytes with 1 byte per channel (RGBA). The data for the image buffer is stored in an am.buffer
.
Creating image buffers
am.image_buffer([buffer, ] width [, height])
Creates an image buffer of the given width and height. If height
is omitted it is the same as width
(the image is square).
If a buffer
(created using am.buffer
) is given, it is used as the image buffer. It must have the correct size, which is 4 * width * height
. If buffer
is omitted a new one is created.
Fields:
width
: the image width.height
: the image height.buffer
: the raw data buffer.
am.load_image(filename)
Loads the given image file and returns a new image buffer. Only .png
and .jpg
files are supported. Returns nil
if the file was not found.
Saving images
image_buffer:save_png(filename)
Saves the given image as a png in filename
.
Pasting images
image_buffer:paste(src, x, y)
Pastes one image into another such that the bottom-left corner of the source image is at the given pixel coordinate in the target image. The bottom-left pixel of the target image has coordinate (1, 1).
Decoding/encoding PNGs
am.encode_png(image_buffer)
Returns a raw buffer containing the png encoding of the given image.
am.decode_png(buffer)
Converts the raw buffer, which should be a png encoding of an image, into an image buffer.
Textures
Textures are 2D images that can be used as input to shader programs when bound to a sampler2D
uniform.
They can also be rendered to via framebuffers.
A texture may optionally have a backing image buffer. If a texture has a backing image buffer, then any changes to the image buffer will be automatically transferred to the texture.
Textures always have 4 channels (RGBA) with 1 byte per channel.
Creating a texture
am.texture2d(width [, height])
Creates a texture of the given width and height without a backing image buffer.
am.texture2d(image_buffer)
Creates a texture using the given image buffer as the backing image buffer.
am.texture2d(filename)
This is shorthand for am.texture2d(am.load_image(filename))
.
Texture fields
texture.width
Readonly.
texture.height
Readonly.
texture.image_buffer
The backing image buffer or nil
if there isn't one.
Readonly.
texture.minfilter
Defines the minification filter which is applied when the texture's pixels are smaller that the screen's pixels.
The allowed values are:
"nearest"
(the default)"linear"
"nearest_mipmap_nearest"
"linear_mipmap_nearest"
"nearest_mipmap_linear"
"linear_mipmap_linear"
If one of the mipmap filters is used a mipmap will be automatically generated.
Updatable.
texture.magfilter
Defines the magnification filter which is applied when the texture's pixels are larger that the screen's pixels.
The allowed values are:
"nearest"
(the default)"linear"
Updatable.
texture.filter
This field can be used to set both the minification and magnification fields. The allowed values are:
"nearest"
"linear"
Updatable.
texture.swrap
Sets the wrapping mode in the x direction. The allowed values are:
"clamp_to_edge"
(the default)"repeat"
"mirrored_repeat"
Note that the texture width must be a power of 2 if either "repeat"
or "mirrored_repeat"
is used.
Updatable.
texture.twrap
Sets the wrapping mode in the y direction. The allowed values are:
"clamp_to_edge"
(the default)"repeat"
"mirrored_repeat"
Note that the texture height must be a power of 2 if either "repeat"
or "mirrored_repeat"
is used.
Updatable.
texture.wrap
This field can be used to set the wrap mode in both the x and y directions at the same time.
The allowed values are:
"clamp_to_edge"
"repeat"
"mirrored_repeat"
Updatable.
Framebuffers
A framebuffer is like an off-screen window you can draw to. It has a texture associated with it that gets updated when you draw to the framebuffer. The texture can then be used as input to further rendering.
Creating a framebuffer
am.framebuffer(texture [, depth_buf [, stencil_buf]])
Creates framebuffer with the given texture attached.
depth_buf
and stencil_buf
determine whether the framebuffer has a depth and/or stencil buffer. These should be true
or false
(default is false
).
Framebuffer fields
framebuffer.clear_color
The color to use when clearing the framebuffer (a vec4
).
Updatable.
framebuffer.stencil_clear_value
The value to use when clearing the framebuffer's stencil buffer (an integer between 0 and 255).
Updatable.
framebuffer.projection
A mat4
projection matrix to use when rendering nodes into this framebuffer. The "P"
uniform will be set to this. If this is not specified then the projection math.ortho(-width/2, width/2, -height/2, height/2)
is used.
Updatable.
framebuffer.pixel_width
The width of the framebuffer, in pixels.
Readonly.
framebuffer.pixel_height
The height of the framebuffer, in pixels.
Readonly.
Framebuffer methods
framebuffer:render(node)
Renders node
into the framebuffer.
framebuffer:render_children(node)
Renders node
's children into the framebuffer (but not node
itself).
framebuffer:clear([color [, depth [, stencil]]])
Clears the framebuffer, using the current clear_color
.
By default the color, depth and stencil buffers are cleared, but you can selectively clear only some buffers by setting the color
, depth
and stencil
argumnets to true
or false
.
framebuffer:read_back()
Reads the pixels from the framebuffer into the texture's backing image buffer. This is a relatively slow operation, so use it sparingly.
framebuffer:resize(width, height)
Resize the framebuffer. Only framebuffers with textures that have no backing image buffers can be resized. Also the texture must not use a filter that requires a mipmap.
Actions
This section covers useful action utility functions. For information on the action mechanism see the description of the action
method.
Delay action
am.delay(seconds)
Returns an action that does nothing for the given number of seconds.
Combining actions
am.series(actions)
Returns an action that runs the given array of actions one after another.
am.parallel(actions)
Returns an action that runs the given array of actions all at the same time. The returned action finishes once all the actions in the array are finished.
am.loop(func)
func
should be a function that returns an action. am.loop
returns an action that repeatedly runs the action returned by func
.
Tweens
am.tween([target,] time, values [, ease])
Returns an action that changes the values of one or more fields of target
to new values over a given time period.
time
is in seconds.
values
is a table mapping field names to their new values.
ease
is an easing function. This function takes a value between 0 and 1 and returns a value between 0 and 1. This determines the "shape" of the interpolation between the two values. If omitted a linear interpolation is used.
The following easing functions are pre-defined (though of course you can define your own):
am.ease.linear
am.ease.quadratic
am.ease.cubic
am.ease.hyperbola
am.ease.sine
am.ease.windup
am.ease.elastic
am.ease.bounce
am.ease.cubic_bezier(x1, y1, x2, y2)
: This returns a cubic bezier ease function with the given control points.am.ease.out(f)
: This takes an existing ease function and returns its reverse. E.g. if the existing ease function is slow and then fast, the new one will be fast and then slow.am.ease.inout(f, g)
. This returns an ease function that uses the ease functionf
for the first half of the transition andout(g)
for the second half.
Tweening works with fields that are numbers or vectors (vec2
, vec3
or vec4
). It also works with quaternions. In all cases the math.mix
function is used to interpolate between the initial value and the final value.
If target is omitted it is taken to be the node to which the tween action is attached.
Example:
am.window{}.scene =
am.rect(-100, -100, 100, 0, vec4(1, 0, 0, 1))
:action(
am.tween(1, {
color = vec4(0, 1, 0, 1),
y2 = 100
}))
Coroutine actions
An action can also be a coroutine. Inside a coroutine action it can sometimes be useful to wait for another action to finish, such as a tween. That is what the am.wait
function is for:
am.wait(action)
Waits for the given action to finish before continuing. This can only be called from within a coroutine.
Example:
am.window{}.scene =
am.rect(-100, -100, 100, 0, vec4(1, 0, 0, 1))
:action(coroutine.create(function(node)
while true do
am.wait(am.tween(node, 1, {
color = vec4(0, 1, 0, 1),
y2 = 100
}))
am.wait(am.tween(node, 1, {
color = vec4(1, 0, 0, 1),
y2 = 0
}))
end
end))
Time
Frame time
am.frame_time
This contains the time the current frame started, in seconds.
Delta time
am.delta_time
This contains the amount of time that lapsed between the start of the current frame and the start of the previous frame, in seconds.
Real time
am.current_time()
This returns the time since the program started, in seconds. This value can change over the course of a frame.
Saving and loading state
am.save_state(key, state [,format])
Save the table state
under key
.
format
can be json
or lua
. The default is lua
.
The location of the save file varies from platform to platform, but will generally be the recommended location to store app data on that system. The shortname
and author
fields from conf.lua
will be used to derive this location if provided.
On iOS this also saves to iCloud. Be aware that iCloud has a 1MB size limit.
Note: The lua
format allows users to execute arbitrary lua code by modifying the save file.
am.load_state(key, [,format])
Loads the table saved under key
and returns it. If no table has been previously saved under key
then nil
is returned.
format
can be json
or lua
. The default is lua
.
Note: The lua
format allows users to execute arbitrary lua code by modifying the save file.
Audio
Audio buffers
An audio buffer is a block of memory that stores an uncompressed audio sample.
An audio buffer may have any number of channels, although Amulet will currently only play up to two channels.
An audio buffer also has a sample rate. Amulet currently plays all audio at a sample rate 44.1kHz and will resample an audio buffer if it has a different sample rate (this requires extra processing).
The audio data itself is stored in a raw buffer as a series of single precision floats (4 bytes each). The samples for each channel are contiguous. The first channel's data comes first, then the second channel's data, etc (the channels are not interleaved).
If the sample data is stored in the buffer buf
, and there are two channels, then the following code will create views that can be used to update or read the samples for each channel:
local c = 2 -- channels
local s = #buf / 4 / c -- samples per channel
local left_channel = buf:view("float", 0, 4, s)
local right_channel = buf:view("float", s * 4, 4, s)
am.audio_buffer(buffer, channels, sample_rate)
Returns a new audio buffer using the given raw buffer (a buffer created with am.buffer
). The audio data should be layed out as described above.
channels
is the number of channels and sample_rate
is the sample rate in Hz.
am.load_audio(filename)
Loads the given audio file and returns a new audio buffer. The file must be a .ogg
audio file. Returns nil
if the file was not found.
Audio buffer fields
audio_buffer.channels
The number of channels. Readonly.
audio_buffer.sample_rate
The sample rate in Hz. Readonly.
audio_buffer.samples_per_channel
The number of samples per channel. Readonly.
audio_buffer.length
The length of the audio in seconds. Readonly.
audio_buffer.buffer
The underlying raw buffer where the audio data is stored. Readonly.
Audio tracks
An audio track contains the playback state of an audio buffer - that is the current position and speed of playback. The same audio buffer can be used with multiple audio tracks.
Note that you normally don't need to explicitly create tracks to play audio, unless you want to change the speed and position during playback.
am.track(buffer [, loop [, playback_speed [, volume]]])
Creates a new track with the given buffer, loop setting (true
/false
), playback_speed and volume. This doesn't cause the track to start playing, use am.play
for that.
Audio track fields
track.playback_speed
Playback speed as a multiplier of the normal speed (e.g. 0.5 means half speed and 2 means double speed). Updatable.
track.volume
Playback volume (1 is normal volume). Updatable.
Audio track methods
track:reset([position])
Sets playback to the given position, measured in seconds. If the argument is omitted, playback is reset to the start.
Playing audio
am.play(source, loop, pitch, volume)
Returns an action that plays audio. Like any other action it needs to be attached to a scene node that's connected to a window to run.
source
can either be an audio track, an audio buffer, the name of a ".ogg" file or a seed generated by the sfxr tool (there's an online version of that tool in the examples list of the online editor).
loop
can be true
or false
.
pitch
is a multiplier applied to the playback speed. 1.0 mean play at the original speed. 2.0 means play twice as fast and 0.5 means play at half the original speed.
volume
should be between 0 and 1.
Note: When the source is an sfxr seed or a filename, an audio buffer is generated behind the scenes and cached. Therefore there might be a short delay the first time this function is called with a given seed or filename (though for short audio clips this will probably not be noticeable). Also avoid calling this function many times with different seeds or filenames, because that will cause unbounded memory growth.
Note: When playing audio in a web browser via HTML export, most browsers will not play any sounds until the user interacts with the page in some way. This is an effort by browser creators to suppress annoying auto-play advertisements. Amulet attempts to detect this and begins playing sound after the first click or key press.
Generating sound effects
am.sfxr_synth(settings)
Returns an audio buffer containing a generated sound effect.
settings
can either be a table containing any number of the following fields:
Field | Default value | Notes |
---|---|---|
wave_type |
"square" |
Can also be "sawtooth" , "sine" or "noise" |
base_freq |
0.3 |
|
freq_limit |
0.0 |
|
freq_ramp |
0.0 |
|
freq_dramp |
0.0 |
|
duty |
0.0 |
|
duty_ramp |
0.0 |
|
vib_strength |
0.0 |
|
vib_speed |
0.0 |
|
vib_delay |
0.0 |
|
env_attack |
0.0 |
|
env_sustain |
0.3 |
|
env_decay |
0.4 |
|
env_punch |
0.0 |
|
filter_on |
false |
|
lpf_resonance |
0.0 |
|
lpf_freq |
1.0 |
|
lpf_ramp |
0.0 |
|
hpf_freq |
0.0 |
|
hpf_ramp |
0.0 |
|
pha_offset |
0.0 |
|
pha_ramp |
0.0 |
|
repeat_speed |
0.0 |
|
arp_speed |
0.0 |
|
arp_mod |
0.0 |
or a numeric seed. Use the sfxr example in the online editor to generate seeds.
Audio graphs
TODO
(Amulet has support for building graphs of audio effect nodes, however it is currently undocumented because the API is unstable).
Audio graph nodes
TODO
Analysing audio
TODO
Controllers
Each controller is assigned an index starting from 1. The first controller attached will always have index 1, so if your game only uses one controller you can just use index 1.
Controllers are supported on Windows, OSX and Linux and up to 8 controllers can be connected at once.
am.controller_present(index)
Returns true
if controller index
is currently connected.
am.controller_attached(index)
Returns true
if controller index
was attached since the last frame.
am.controller_detached(index)
Returns true
if controller index
was removed since the last frame.
am.controllers_present()
Returns a list of currently connected controller indexes.
am.controllers_attached()
Returns a list of controller indexes attached since the last frame.
am.controllers_detached()
Returns a list of controller indexes removed since the last frame.
am.controller_lt_val(index)
Returns the value of the left trigger axis of controller index
. The returned value is between 0 and 1.
am.controller_rt_val(index)
Returns the value of the right trigger axis of controller index
. The returned value is between 0 and 1.
am.controller_lstick_pos(index)
Returns the position of the left stick of controller index
as a vec2
. The position values range from -1 to 1 in both the x and y components. Negative x means left and negative y means down.
am.controller_rstick_pos(index)
Returns the position of the left stick of controller index
as a vec2
. The position values range from -1 to 1 in both the x and y components. Negative x means left and negative y means down.
am.controller_button_pressed(index, button)
Returns true
if the given button of controller index
was pressed since the last frame.
button
can be one of the following strings:
"a"
"b"
"x"
"y"
"back"
"guide"
"start"
"ls"
"rs"
"lb"
"rb"
"up"
"down"
"left"
"right"
am.controller_button_released(index, button)
Returns true
if the given button of controller index
was released since the last frame. See am.controller_button_pressed for a list of valid values for button
.
am.controller_button_down(index, button)
Returns true
if the given button of controller index
was down at the start of the current frame. See am.controller_button_pressed for a list of valid values for button
.
Packing sprites and generating fonts
Overview
Amulet includes a tool for packing images and font glyphs into a sprite sheet and generating a Lua module for conveniently accessing the images and glyphs therein.
Suppose you have an images
directory containing some .png
files and a fonts
directory containing myfont.ttf
and suppose these two directories are subdirectories of the main game directory (where your main.lua
file lives). To generate a sprite sheet, run the following command while in the main game directory:
> amulet pack -png mysprites.png -lua mysprites.lua
images/*.png fonts/myfont.ttf@32
This will generate mysprites.png
and mysprites.lua
in the current directory.
The @32
after the font file specifies the size of the glyphs to generate (in this case 32px).
To use the generated sprite sheet in your code, first required the Lua module and then pass the fields in that module to am.sprite
or am.text
. For example:
local mysprites = require "mysprites"
local run1_node = am.sprite(mysprites.run1)
local text_node = am.text(mysprites.myfont32, "BARF!")
The sprite field names are generated from the image filenames with the extension removed (so the file images/run1.png
would result in the field mysprites.run1
).
The font field name (myfont32
) is the concatenation of the font file name (without the extension) and the font size.
Pack options
In addition to the required -png
and -lua
options the pack command also supports the following options:
Option | Description |
---|---|
-mono |
Do not anti-alias fonts. |
-minfilter |
The minification filter to apply when loading the sprite sheet texture. linear (the default) or nearest . |
-magfilter |
The magnification filter to apply when loading the sprite sheet texture. linear (the default) or nearest . |
-no-premult |
Do not pre-multiply RGB channels by alpha. |
-keep-padding |
Do not strip transparent pixels around images. |
Padding
Unless you specify the -keep-padding
option, Amulet will remove excess rows and columns of completely transparent pixels from each image before packing it. It will always however leave a one pixel border of transparent pixels if the image was surrounded by transparent pixels to begin with. This is done to prevent pixels from one image "bleeding" into another image when it's drawn.
When excess rows and columns are removed the vertices of the sprite will be adjusted so the images is still drawn in the correct position, as if the excess pixels were still there.
If an image is not surrounded by a border of transparent pixels, then the border pixels will be duplicated around the image. This helps prevent "cracks" when tiling the image.
In summary, if you don't intend to use an image for tiling, surround it with a border of completely transparent pixels.
Specifying which font glyphs to generate
Optionally each font item may also be followed by a colon and a comma separated list of character ranges to include in the sprite sheet.
The characters in the character range can be written directly if they are ASCII, or given as hexadecimal Unicode codepoints.
Here are some examples of valid font items:
VeraMono.ttf@32:A-Z,0-9
ComicSans.ttf@122:0x20-0xFF
gbsn00lp.ttf@42:a-z,A-Z,0-9,0x4E00-0x9FFF
If the character range list is omitted, it defaults to 0x20-0x7E
, i.e:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
Loading bitmap font images
am.load_bitmap_font(filename, key)
Loads the image filename
and returns a font using the glyph layout described by key
where key
is a string containing all the glyphs in the file as they are layed out. All glyphs must have the same width and height which is determined from the width and height of the image and the number of rows and columns in key
. For example if the key is:
[[
ABC
DEF
GHI
]]
then the image contains 9 glyphs in 3 rows and 3 columns. If the image has height 30 and width 36 then each glyph has width 10 and height 12.
Exporting
To generate distribution packages, use the amulet export command like so:
> amulet export [<dir>]
<dir>
is the directory containing your main.lua
file and optional conf.lua
file (see below). It defaults to the current directory.
This will generate zip package files for Windows, Mac and Linux in the current directory. Alternatively you can pass one of the options -windows
, -mac
, -linux
, -html
, -ios-xcode-proj
or -android-studio-proj
, to generate packages for a specific platform.
If the -r
option is given then all subdirectories will also be included (recursively), otherwise only the files in the given directory are included.
By default all files in the directory with the following extensions will be included in the export: .lua
, .json
, .png
, .jpg
, .ogg
, .obj
, .vert
, .frag
. You can include all files with the -a
option.
All .txt files will also be copied to the generated zip and be visible to the user when they unzip it. This is intended for README.txt
or similar files.
By default packages are exported to the current directory. The -d option can be used to specify a different directory (the directory must already exist).
The -o option allows you to specify the complete path (dir + filename) of the generated package. In this case the -d option is ignored. The -o option doesn't work if you're exporting multiple platforms at once.
For example the following will export a windows build to builds/mygame.zip
. It will look in src
for the game files, recursively including all files.
amulet export -windows -r -a -o builds/mygame.zip src
The following will generate mac and linux builds in the builds
directory, this time looking for game files in game
recursively, but only including recognised files (since no -a
option is given):
amulet export -mac -linux -r -d builds game
As a courtesy to the user, the generate zip packages will contain the game files in a sub-folder. If you instead want the game files to appear in the root of the zip, use -nozipdir. You might want this if the game will run from a launcher such as Steam.
If you want to generate only the data.pak
file for your project you can specify the -datapak
option. It behaves in a similar way to the platform options, but generates a platform agnostic data.pak
file containing your projects Lua files and other assets. You might want to do this to update a previously generated xcode project without regenerating all the project files.
The generated zip will also contain an amulet_license.txt
file containing the Amulet license as well as the licenses of all third party libraries used by Amulet. Some of these licenses require that they be distributed with copies of their libraries, so to comply you should include amulet_license.txt when you distribute your work. Note that these licenses do not apply to your work itself.
IMPORTANT: Avoid unzipping and re-zipping the generated packages as you may inadvertently strip the executable marker from some files, which will cause them not to work on some platforms.
Project Configuration
Project metadata can be specified in a conf.lua
file in the same directory as main.lua
. Information like the icon to use for Mac and iOS exports, your game title, author and version, as well as app signing information for iOS is all specified here.
Basic metadata
A minimal conf.lua
might look like this:
title = "My Game Title"
author = "Your Name"
shortname = "mygame"
version = "1.0.0"
support_email = "support@example.com"
copyright_message = "Copyright © 2019 Your Name."
shortname
is used for the name of the executable and in a few other places. support_email
is displayed to the user in error messages if provided. author
is used to determine where save files go as well as in package metadata. The other metadata may or may not be included in the generated package metadata, depending on the target platform.
Language configuration
dev_region = "en"
supported_languages = "en,fr,de,ja,zh-Hans,zh-Hant,ko"
These options specify the main language and supported languages of the app respectively. am.language
will always return one of the supported languages. Note that not all platforms currently support this feature.
Lua version
luavm = "lua52"
Specify which version of Lua to use for exported builds. This does not currently affect which version of Lua is used to run the game from the command line. Valid values are "lua51"
, "lua52"
and "luajit"
.
Windows settings
d3dangle = true
Use Direct3D on Windows instead of OpenGL. Your GLSL shaders will be translated to HLSL using the Angle library. Note that MSAA is currently not supported when using Direct3D.
Mac settings
appid_mac = "com.example.myappid"
icon_mac = "assets/icon_mac.png"
Specify the icon and appid for Mac exports. The icon path is relative to where you run amulet from.
iOS settings
display_name = "My Game"
appid_ios = "com.example.mygameid"
icon_ios_ = "assets/icon.png"
launch_image = "assets/launch_image.png"
orientation = "portrait"
ios_dev_cert_identity = "XXXX123456"
ios_appstore_cert_identity = "XXXX123456"
ios_code_sign_identity = "Apple Distribution"
ios_dev_prov_profile_name = "MyGame Dev Profile"
ios_dist_prov_profile_name = "MyGame App Store Profile"
game_center_enabled = true
icloud_enabled = true
The above metadata is used when the -ios-xcode-proj
export option is given to generate an Xcode project for iOS. All the data is required.
orientation
can be "portrait"
, "landscape"
, "any"
or "hybrid"
(portrait on iPhone, but landscape on iPad).
ios_dev_cert_identity
and ios_appstore_cert_identity
is the code that appears in parenthesis in your certificate name (e.g. if you certificate name in Key Access is "iPhone Distribution: Your Name (XXXX123456)", then this value should be "XXXX123456"
.
ios_code_sign_identity
is the part that comes before the colon in your distribution certificate name. Typically this is either "iPhone Distribution"
or "Apple Distribution"
. (Use the Keychain Access application in Utilities to view your certificates.)
ios_dev_prov_profile_name
and ios_dist_prov_profile_name
are the names of your installed development and distribution provisioning profiles respectively. You can install a provisioning profile by downloading and double clicking it.
Android settings
display_name = "My Game"
appid_android = "com.example.mygameid"
icon_android = "assets/icon.png"
orientation = "portrait"
android_app_version_code = "4"
google_play_services_id = "58675281521"
The above metadata is used when the -android-studio-proj
export option is given to generate an Android Studio project. All the data, except google_play_services_id
is required (google_play_services_id
is only required if you want to use Google Play leaderboards or achievements).
orientation
can be "portrait"
, "landscape"
or "any"
.
appid_android
is used for the Java package name of the app. It should not contain dashes.
3D models
Amulet has some basic support for loading 3D models in Wavefront .obj
format.
am.load_obj(filename)
This loads the given .obj
file and returns 4 things:
- A buffer containing the vertex, normal and texture coordinate data.
- The stride in bytes.
- The offset of the normals in bytes.
- The offset of the texture coordinates in bytes.
The vertex data is always at offset 0. If the normal or texture coordinate data is not present, the corresponding return value will be nil.
The faces in the .obj
file must all be triangles (quads aren't supported).
Here's an example of how to load a model and display it. The example loads an model from model.obj
and assumes it contains normal and texture coordinate data and the triangles have a counter-clockwise winding. It loads a texture from the file texture.png
.
local win = am.window{depth_buffer = true}
local buf, stride, norm_offset, tex_offset = am.load_obj("model.obj")
local verts = buf:view("vec3", 0, stride)
local normals = buf:view("vec3", norm_offset, stride)
local uvs = buf:view("vec2", tex_offset, stride)
local shader = am.program([[
precision mediump float;
attribute vec3 vert;
attribute vec2 uv;
attribute vec3 normal;
uniform mat4 MV;
uniform mat4 P;
varying vec3 v_shadow;
varying vec2 v_uv;
void main() {
vec3 light = normalize(vec3(1, 0, 2));
vec3 nm = normalize((MV * vec4(normal, 0.0)).xyz);
v_shadow = vec3(max(0.1, dot(light, nm)));
v_uv = uv;
gl_Position = P * MV * vec4(vert, 1.0);
}
]], [[
precision mediump float;
uniform sampler2D tex;
varying vec3 v_shadow;
varying vec2 v_uv;
void main() {
gl_FragColor = texture2D(tex, v_uv) * vec4(v_shadow, 1.0);
}
]])
win.scene =
am.cull_face"ccw"
^ am.translate(0, 0, -5)
^ am.use_program(shader)
^ am.bind{
P = math.perspective(math.rad(60), win.width/win.height, 1, 1000),
vert = verts,
normal = normals,
uv = uvs,
tex = am.texture2d("texture.png"),
}
^am.draw"triangles"
Extra table functions
The following functions suplement the standard Lua table functions.
table.shallow_copy(t)
Returns a shallow copy of t
(i.e. a new table with the same keys and values as t
).
table.deep_copy(t)
Returns a deep copy of t
(all t
's keys and values are copied recursively). Cycles are detected and reproduced in the new table.
table.search(arr, elem)
Return the index of elem
in arr
or nil if it's not found.
table.shuffle(t [,rand])
Randomly rearranges the values of t
. The optional rand
argument should be a function where rand(n)
returns an integer between 1 and n (like math.random
). By default math.random
is used.
table.clear(t)
Remove all t
's pairs.
table.remove_all(arr, elem)
Remove all values equal to elem
from arr
.
table.append(arr1, arr2)
Inserts all of arr2
's values at the end of arr1
.
table.merge(t1, t2)
Sets all the key-value pairs from t2
in t1
.
table.keys(t)
Returns an array of t
's keys.
table.values(t)
Returns an array of t
's values.
table.filter(arr, f)
Returns a new array which contains only the values from arr
for which f(elem)
returns true (or any value besides nil
or false
).
table.tostring(t)
Converts a table to a string. The returned string is a valid Lua table literal.
table.count(t)
Returns the total number of pairs in the table.
Amulet require function
The require
function in Amulet is slightly different from the default one. The default Lua package loaders have been removed and replaced with a custom loader. The loader passes a new empty table into each module it loads. All exported functions can be added to this table, instead of creating a new table. If no other value is returned by the module, the passed in table will be used as the return value for require
.
The passed in export table can be accessed via the ...
expression. Here's a short example:
local mymodule = ...
mymodule.message = "hello"
function mymodule.print_message()
print(mymodule.message)
end
If this module is in the file mymodule.lua
, then it can be imported like so:
local mymodule = require "mymodule"
mymodule.print_message() -- prints hello
This scheme allows cyclic module imports, e.g. module A requires module B which in turn requires module A. Amulet will detect the recursion and return A's (incomplete) export table in B. Then when A has finished initialising, all its functions will be available in B. This does mean that B can't call any of A's functions while its initialising, but after initialisation all of A's functions will be available.
Of course you can still return your own values from modules and they will be returned by require
as with the default Lua require
function.
Logging
log(msg, ...)
Log a message to the console. The message will also appear in an overlay on the main window.
msg
may contain format specifiers like the standard Lua string.format
function.
The logged messages are prefixed with the file name and line number where log
was called.
Example:
log("here")
log("num = %g, string = %s", 1, "two")
Preventing accidental global variable usage
noglobals()
Prevents the creation of new global variables. An error will be raised if a new global is created after this call, or if an attempt is made to read a nil global.
Globbing
am.glob(patterns)
Returns an array (table) of file names matching the given glob pattern(s). patterns
should be a table of glob pattern strings. A glob pattern is a file path with zero or more wildcard (*
) characters.
Any matching files that are directories will have a slash (/
) appended to their names, even on Windows.
The slash character (/
) can be used as a directory separator on Windows (you don't need to use \
). Furthermore returned paths will always have '/' as the directory separator, even on Windows.
Note: This function only searches for files on the file system. It won't search the resource archive in a exported game. Its intended use is for writing file processing utilities and not for use directly in games you wish to distribute.
Example:
local image_files = am.glob{"images/*.png", "images/*.jpg"}
Running JavaScript
am.eval_js(js)
Runs the given JavaScript string and returns the result as a Lua value. JavaScript objects and arrays are converted to Lua tables and other JavaScript types are converted to the corresponding Lua types. undefined
is converted to nil
.
This function only works when running in a browser. On other platforms it has no effect and always returns nil
.
Converting to/from JSON
am.to_json(value)
Converts the given Lua value to a JSON string and returns it.
Tables with string keys are converted to JSON objects and tables with consecutive integer keys starting at 1 are converted to JSON arrays. Empty tables are converted to empty JSON arrays. Other types of tables are not supported anc cycles are not detected.
am.parse_json(json)
Converts the given JSON string to a Lua value and returns it. If there was an error parsing the JSON then nil
is returned and the error message is returned as a second return value.
Loading other resources
am.load_script(filename)
Loads the Lua script in filename
and returns a function that, when called, will run the script. If the file doesn't exist nil
is returned.
am.load_string(filename)
Loads filename
and returns its contents as a string or nil
if the file wasn't found.
Performance stats
am.perf_stats()
Returns a table with the following fields:
avg_fps
: frames per second averaged over the last 60 framesmin_fps
: the minimum frames per second over the last 60 framesframe_draw_calls
: the number ofdraw
calls in the last frameframe_use_program_calls
: the number ofuse_program
calls in the last frame
Amulet version
am.version
The current Amulet version, as a string. E.g. "1.0.3"
.
Platform
am.platform
The platform Amulet is running on. It will be one of the strings "linux"
"windows"
"osx"
"ios"
"android"
or "html"
.
Language
am.language()
Returns the user's preferred ISO 639-1 language code in lower case(e.g. "en"
), possibly followed by a dash and an ISO 3166-1 coutry code in upper case (e.g. "fr-CA"
). The returned value will be one of the languages listed in the conf.lua
file (see here). This currently only returns a meaningful value on Mac, iOS and Android (on other platforms it always returns "en"
).
Game Center (iOS only)
The following functions are only available on iOS.
am.init_gamecenter()
Initialize Game Center. This must be called before any other Game Center functions.
am.gamecenter_available()
Returns true if Game Center was successfully initialized.
am.submit_gamecenter_score(leaderboard_id, score)
Submit a score to a leaderboard. Note that Game Center accepts only integer scores.
am.submit_gamecenter_achievement(achievment_id)
Submit an achievement.
am.show_gamecenter_leaderboard(leaderboard_id)
Display a leaderboard.