Getting Started with Tiramisu
Tiramisu is a type-safe 3D game engine for Gleam, built on Three.js. It follows the Model-View-Update (MVU) architecture, making game state management predictable and testable.
Quick Start with Mascarpone ๐ง
The easiest way to start is using Mascarpone, an interactive CLI tool that scaffolds complete Tiramisu projects.
Create a New Project
gleam new my_game
cd my_game
Run Mascarpone
gleam add --dev mascarpone
gleam run -m mascarpone
The interactive TUI will guide you through:
- Lustre Integration - Choose whether to include Lustre for UI overlays (menus, HUDs)
- Project Template - Select from:
- 2D Game - Orthographic camera and sprite setup
- 3D Game - Perspective camera with lighting
- Physics Demo - Physics-enabled objects
Mascarpone automatically:
- โ
Configures
gleam.toml
with all dependencies - โ Sets up Three.js and Rapier3D CDN imports
- โ Creates a working game example
- โ Adds necessary stylesheets for fullscreen games
- โ
Generates
.gitignore
for Gleam projects
Run Your Game
gleam run -m lustre/dev start
Open your browser and navigate to http://localhost:1234 - you should see your game running!
Manual Installation (Alternative)
If you prefer to set up your project manually or add Tiramisu to an existing project:
Add Tiramisu Dependency
gleam add tiramisu
Configure your gleam.toml
Add Three.js, Rapier, and styling configuration:
[tools.lustre.html]
scripts = [
{ type = "importmap", content = "{ \"imports\": { \"three\": \"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js\", \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/\", \"@dimforge/rapier3d-compat\": \"https://cdn.jsdelivr.net/npm/@dimforge/rapier3d-compat@0.11.2/+esm\" } }" }
]
stylesheets = [
{ content = "body { margin: 0; padding: 0; overflow: hidden; } canvas { display: block; }" }
]
Your First Game: Hello Cube (Manual Setup)
If you used Mascarpone, you already have a working game! This section shows how to create a spinning 3D cube manually if you chose the manual installation path.
Create the Game File
Replace the contents of src/my_game.gleam
with:
import gleam/option
import tiramisu
import tiramisu/background
import tiramisu/camera
import tiramisu/effect
import tiramisu/geometry
import tiramisu/light
import tiramisu/material
import tiramisu/scene
import tiramisu/transform
import vec/vec3
// Model: Game state
pub type Model {
Model(rotation: Float)
}
// Msg: Events that can happen
pub type Msg {
Tick
}
pub fn main() {
tiramisu.run(
dimensions: option.Some(tiramisu.Dimensions(width: 800.0, height: 600.0)),
background: background.Color(0x1a1a2e), // Dark blue background
init: init,
update: update,
view: view,
)
}
// Initialize the game state
fn init(_ctx: tiramisu.Context) -> #(Model, effect.Effect(Msg)) {
#(Model(rotation: 0.0), effect.tick(Tick))
}
// Update game state based on events
fn update(
model: Model,
msg: Msg,
ctx: tiramisu.Context,
) -> #(Model, effect.Effect(Msg)) {
case msg {
Tick -> {
let new_rotation = model.rotation +. ctx.delta_time
#(Model(rotation: new_rotation), effect.tick(Tick))
}
}
}
// Render the scene
fn view(model: Model) -> List(scene.SceneNode) {
// Create camera
let assert Ok(cam) = camera.perspective(
field_of_view: 75.0,
near: 0.1,
far: 1000.0,
)
let camera_node = scene.Camera(
id: "main",
camera: cam,
transform: transform.at(position: vec3.Vec3(0.0, 0.0, 5.0)),
look_at: option.Some(vec3.Vec3(0.0, 0.0, 0.0)),
active: True,
viewport: option.None,
)
// Create rotating cube
let assert Ok(cube_geometry) = geometry.box(
width: 1.0,
height: 1.0,
depth: 1.0,
)
let assert Ok(cube_material) =
material.new()
|> material.with_color(0x4ecdc4)
|> material.with_metalness(0.5)
|> material.with_roughness(0.5)
|> material.build()
let cube = scene.Mesh(
id: "cube",
geometry: cube_geometry,
material: cube_material,
transform: transform.Transform(
position: vec3.Vec3(0.0, 0.0, 0.0),
rotation: vec3.Vec3(model.rotation, model.rotation, 0.0),
scale: vec3.Vec3(1.0, 1.0, 1.0),
),
physics: option.None,
)
// Add lights
let assert Ok(ambient_light) = light.ambient(
intensity: 0.5,
color: 0xffffff,
)
let ambient = scene.Light(
id: "ambient",
light: ambient_light,
transform: transform.identity,
)
let assert Ok(directional_light) = light.directional(
intensity: 0.8,
color: 0xffffff,
)
let directional = scene.Light(
id: "sun",
light: directional_light,
transform: transform.Transform(
position: vec3.Vec3(5.0, 5.0, 5.0),
rotation: vec3.Vec3(0.0, 0.0, 0.0),
scale: vec3.Vec3(1.0, 1.0, 1.0),
),
)
[camera_node, cube, ambient, directional]
}
Run Your Game
gleam run -m lustre/dev start
Open your browser and navigate to http://localhost:1234. You should see a spinning teal cube!
Understanding the Code
Model-View-Update Architecture
Tiramisu follows the MVU pattern (also called The Elm Architecture):
- init: Create initial game state and effects
- view: Render the current state as a scene
- update: Handle events and produce new state
- Model: Immutable game state
- Msg: Events that trigger updates
Key Concepts
1. Context
The Context
type provides timing, input, and canvas dimension information:
pub type Context {
Context(
delta_time: Float, // Time since last frame (seconds)
input: InputState, // Keyboard, mouse, touch state
canvas_width: Float, // Current canvas width in pixels
canvas_height: Float, // Current canvas height in pixels
)
}
2. Effects
Effects are side effects that return messages:
effect.tick(Tick) // Run on every frame
effect.from(fn(dispatch) { ... }) // Custom effect
effect.batch([effect1, effect2]) // Run multiple effects
effect.none() // No effects
3. Scene Nodes
Your view
function returns a list of SceneNode
:
pub type SceneNode {
Camera(...) // Camera (one must be active: True)
Mesh(...) // 3D object with geometry and material
Light(...) // Light source
Group(...) // Container for child nodes
Sprite(...) // 2D sprite (billboarded quad)
InstancedMesh(...) // Many identical objects (efficient)
LOD(...) // Level-of-detail system
}
4. Transforms
Every scene node has a transform (position, rotation, scale):
transform.Transform(
position: vec3.Vec3(x, y, z),
rotation: vec3.Vec3(rx, ry, rz), // Radians
scale: vec3.Vec3(sx, sy, sz),
)
// Or use the identity transform
transform.identity // Position (0,0,0), no rotation, scale (1,1,1)