tiramisu/scene
Scene graph module - declarative 3D scene construction.
This module provides types and functions for building 3D scenes declaratively.
Scenes are composed of SceneNode values that describe meshes, lights, cameras, and groups.
Core Concepts
- Immutability: Scene nodes are immutable values. Updates create new nodes.
- Hierarchy: Use
Groupnodes to create parent-child relationships. - Validation: Geometry and material constructors return
Resultto catch invalid parameters. - Performance: Use
InstancedMeshfor many identical objects (1 draw call instead of thousands).
Quick Example
import tiramisu/scene
import tiramisu/transform
import gleam/option
import vec/vec3
pub fn view(model: Model) {
let assert Ok(geometry) = scene.box(width: 1.0, height: 1.0, depth: 1.0)
let assert Ok(material) = scene.basic_material(color: 0xff0000, transparent: False, opacity: 1.0)
[
scene.Mesh(
id: "player",
geometry: geometry,
material: material,
transform: transform.at(vec3.Vec3(0.0, 1.0, 0.0)),
physics: option.None,
),
scene.Light(
id: "sun",
light: scene.DirectionalLight(color: 0xffffff, intensity: 1.0),
transform: transform.identity,
),
]
}
Types
Level of Detail (LOD) configuration.
Defines which mesh to display based on camera distance. Use with LOD scene node
for automatic detail switching to improve performance.
Example
scene.LOD(
id: "tree",
levels: [
scene.lod_level(distance: 0.0, node: high_detail_mesh), // 0-50 units
scene.lod_level(distance: 50.0, node: medium_detail_mesh), // 50-100 units
scene.lod_level(distance: 100.0, node: low_detail_mesh), // 100+ units
],
transform: transform.identity,
)
pub type LODLevel(id) {
LODLevel(distance: Float, node: Node(id))
}
Constructors
-
LODLevel(distance: Float, node: Node(id))
Viewport configuration for split-screen or picture-in-picture rendering.
Coordinates are in pixels from the top-left of the canvas.
pub type ViewPort {
ViewPort(x: Int, y: Int, width: Int, height: Int)
}
Constructors
-
ViewPort(x: Int, y: Int, width: Int, height: Int)Arguments
- x
-
X position from left edge in pixels
- y
-
Y position from top edge in pixels
- width
-
Width in pixels
- height
-
Height in pixels
Values
pub fn audio(id id: id, audio audio: audio.Audio) -> Node(id)
Create an audio scene node.
Audio nodes play sounds in the scene. See the audio module for creating audio sources.
Example
import tiramisu/scene
import tiramisu/audio
import gleam/option
// Background music
let background_music = audio.new_audio("background")
|> audio.with_source(audio.Stream("music/theme.mp3"))
|> audio.with_loop(True)
|> audio.with_volume(0.5)
|> audio.with_autoplay(True)
scene.Audio(id: "bgm", audio: background_music)
// Sound effect (from pre-loaded buffer)
let assert Ok(jump_buffer) = asset.get_audio(cache, "sounds/jump.mp3")
let jump_sound = audio.new_audio("jump")
|> audio.with_source(audio.Buffer(jump_buffer))
|> audio.with_volume(0.8)
scene.Audio(id: "jump-sfx", audio: jump_sound)
pub fn camera(
id id: id,
camera camera: camera.Camera,
transform transform: transform.Transform,
look_at look_at: option.Option(vec3.Vec3(Float)),
active active: Bool,
viewport viewport: option.Option(ViewPort),
) -> Node(id)
Create a camera scene node.
Cameras define the viewpoint for rendering. At least one active camera is required. Multiple cameras can be used for split-screen, minimaps, or picture-in-picture.
Active: Only active cameras render. Set to True for at least one camera.
Look At: Optional target point the camera faces (camera auto-rotates to face it).
Viewport: Optional screen region for this camera (for split-screen).
Example
import tiramisu/scene
import tiramisu/camera
import tiramisu/transform
import vec/vec3
import gleam/option
// Main perspective camera
let assert Ok(cam) = camera.perspective(field_of_view: 75.0, near: 0.1, far: 1000.0)
scene.Camera(
id: "main-camera",
camera: cam,
transform: transform.at(position: vec3.Vec3(0.0, 5.0, 10.0)),
look_at: option.Some(vec3.Vec3(0.0, 0.0, 0.0)), // Look at origin
active: True,
viewport: option.None, // Fullscreen
)
// Minimap camera (top-down view in corner)
let assert Ok(minimap_cam) = camera.orthographic(
left: -20.0, right: 20.0, top: 20.0, bottom: -20.0, near: 0.1, far: 100.0
)
scene.Camera(
id: "minimap",
camera: minimap_cam,
transform: transform.at(position: vec3.Vec3(0.0, 50.0, 0.0))
|> transform.with_euler_rotation(vec3.Vec3(-1.57, 0.0, 0.0)),
look_at: option.None,
active: True,
viewport: option.Some(scene.ViewPort(x: 10, y: 10, width: 200, height: 200)),
)
pub fn debug_axes(
id id: id,
origin origin: vec3.Vec3(Float),
size size: Float,
) -> Node(id)
Create a debug coordinate axes visualization.
Displays X (red), Y (green), and Z (blue) axes from the origin point. Useful for visualizing object orientation, camera position, or world origin.
Origin: Center point in world space. Size: Length of each axis line in units.
Example
// Show world origin
scene.debug_axes(
id: "world_axes",
origin: vec3.Vec3(0.0, 0.0, 0.0),
size: 5.0, // 5 unit length axes
)
// Show object local axes
scene.debug_axes(
id: "player_axes",
origin: player_position,
size: 2.0,
)
pub fn debug_box(
id id: id,
min min: vec3.Vec3(Float),
max max: vec3.Vec3(Float),
color color: Int,
) -> Node(id)
Create a debug wireframe box visualization.
Useful for visualizing collision bounds, trigger zones, or spatial regions.
Min/Max: Define the axis-aligned bounding box corners in world space. Color: Hex color for the wireframe lines.
Example
// Visualize a collision box
scene.debug_box(
id: "trigger_zone",
min: vec3.Vec3(-5.0, 0.0, -5.0),
max: vec3.Vec3(5.0, 3.0, 5.0),
color: 0x00ff00, // Green wireframe
)
pub fn debug_grid(
id id: id,
size size: Float,
divisions divisions: Int,
color color: Int,
) -> Node(id)
Create a debug ground grid visualization.
Displays a grid on the XZ plane (horizontal ground plane) centered at origin. Useful for spatial reference, scale indication, or level design.
Size: Total width/depth of the grid in units. Divisions: Number of grid cells (higher = finer grid). Color: Hex color for the grid lines.
Example
// Create a 20x20 unit grid with 10 divisions
scene.debug_grid(
id: "ground_grid",
size: 20.0, // 20 units wide
divisions: 10, // 10x10 cells (2 units per cell)
color: 0x444444, // Dark gray
)
pub fn debug_line(
id id: id,
from from: vec3.Vec3(Float),
to to: vec3.Vec3(Float),
color color: Int,
) -> Node(id)
Create a debug line segment visualization.
Useful for visualizing raycasts, trajectories, connections, or directions.
From/To: Start and end points in world space. Color: Hex color for the line.
Example
// Visualize raycast from player to target
scene.debug_line(
id: "raycast",
from: player_position,
to: target_position,
color: 0xffff00, // Yellow line
)
pub fn debug_point(
id id: id,
position position: vec3.Vec3(Float),
size size: Float,
color color: Int,
) -> Node(id)
Create a debug point visualization.
Displays a small sphere at the specified position. Useful for marking locations, waypoints, spawn points, or intersection points.
Position: Point location in world space. Size: Radius of the debug sphere in units (typically small like 0.1-0.5). Color: Hex color for the sphere.
Example
// Mark spawn points
scene.debug_point(
id: "spawn_1",
position: vec3.Vec3(10.0, 0.0, 5.0),
size: 0.3, // Small sphere
color: 0x00ff00, // Green
)
// Mark raycast hit point
scene.debug_point(
id: "hit_point",
position: raycast_result.point,
size: 0.2,
color: 0xff0000, // Red
)
pub fn debug_sphere(
id id: id,
center center: vec3.Vec3(Float),
radius radius: Float,
color color: Int,
) -> Node(id)
Create a debug wireframe sphere visualization.
Useful for visualizing sphere colliders, range indicators, or explosion radii.
Center: Center position in world space. Radius: Sphere radius (should match your collider if visualizing physics). Color: Hex color for the wireframe lines.
Example
// Visualize attack range
scene.debug_sphere(
id: "attack_range",
center: player_position,
radius: 5.0, // 5 unit attack radius
color: 0xff0000, // Red wireframe
)
pub fn group(
id id: id,
transform transform: transform.Transform,
children children: List(Node(id)),
) -> Node(id)
Create a group node for scene hierarchy.
Groups allow you to organize nodes in a parent-child hierarchy. The group’s transform is applied to all children, making it easy to move/rotate/scale multiple objects together.
Example
import tiramisu/scene
import tiramisu/transform
import vec/vec3
// Solar system: sun with orbiting planets
scene.Group(
id: "solar-system",
transform: transform.identity,
children: [
scene.Mesh(...), // Sun at center
scene.Group(
id: "earth-orbit",
transform: transform.at(position: vec3.Vec3(10.0, 0.0, 0.0))
|> transform.rotate_y(model.earth_angle),
children: [
scene.Mesh(...), // Earth
scene.Group(
id: "moon-orbit",
transform: transform.at(position: vec3.Vec3(2.0, 0.0, 0.0))
|> transform.rotate_y(model.moon_angle),
children: [scene.Mesh(...)], // Moon
),
],
),
],
)
pub fn instanced_mesh(
id id: id,
geometry geometry: geometry.Geometry,
material material: material.Material,
instances instances: List(transform.Transform),
) -> Node(id)
Create an instanced mesh for rendering many identical objects efficiently.
Instead of creating N separate meshes (N draw calls), instanced meshes render all instances in a single draw call. Perfect for forests, crowds, particles, or any scene with many repeated objects.
Performance: Use this when you have 10+ identical objects for significant speedup.
Example
import tiramisu/scene
import tiramisu/geometry
import tiramisu/material
import tiramisu/transform
import vec/vec3
import gleam/list
// Create 100 trees efficiently
let assert Ok(tree_geo) = geometry.cylinder(radius: 0.2, height: 3.0)
let assert Ok(tree_mat) = material.lambert(
color: 0x8b4513,
map: option.None,
normal_map: option.None,
ambient_oclusion_map: option.None,
)
let tree_positions = list.range(0, 99)
|> list.map(fn(i) {
let x = int.to_float(i % 10) *. 5.0
let z = int.to_float(i / 10) *. 5.0
transform.at(position: vec3.Vec3(x, 0.0, z))
})
scene.InstancedMesh(
id: "forest",
geometry: tree_geo,
material: tree_mat,
instances: tree_positions, // All rendered in 1 draw call!
)
pub fn instanced_model(
id id: id,
object object: asset.Object3D,
instances instances: List(transform.Transform),
physics physics: option.Option(physics.RigidBody),
) -> Node(id)
Create instanced 3D models for rendering many copies of a loaded asset.
Like InstancedMesh, but for loaded models (GLTF/FBX/OBJ). Renders all instances
in one draw call for maximum performance.
Example
import tiramisu/scene
import tiramisu/asset
import tiramisu/transform
import vec/vec3
import gleam/option
import gleam/list
// Load rock model
let assert Ok(rock_data) = asset.get_model(cache, "rock.glb")
// Place 50 rocks around the scene
let rock_positions = list.range(0, 49)
|> list.map(fn(i) {
let angle = int.to_float(i) *. 0.125
let radius = 20.0
let x = radius *. gleam_community.maths.cos(angle)
let z = radius *. gleam_community.maths.sin(angle)
transform.at(position: vec3.Vec3(x, 0.0, z))
})
scene.InstancedModel(
id: "rock-field",
object: rock_data.scene,
instances: rock_positions,
physics: option.None,
)
pub fn light(
id id: id,
light light: light.Light,
transform transform: transform.Transform,
) -> Node(id)
Create a light scene node.
Lights illuminate the scene. See the light module for different light types
(ambient, directional, point, spot, hemisphere).
Example
import tiramisu/scene
import tiramisu/light
import tiramisu/transform
import vec/vec3
// Directional sun light
let assert Ok(sun) = light.directional(intensity: 1.2, color: 0xffffff)
|> light.with_shadows(True)
scene.Light(
id: "sun",
light: sun,
transform: transform.identity
|> transform.with_euler_rotation(vec3.Vec3(-0.8, 0.3, 0.0)),
)
pub fn lod(
id id: id,
levels levels: List(LODLevel(id)),
transform transform: transform.Transform,
) -> Node(id)
Create a Level of Detail (LOD) node.
LOD nodes automatically switch between different detail levels based on camera distance, improving performance by showing simpler models when far away.
Levels: Ordered list from closest (distance: 0.0) to farthest. Use lod_level() to create.
Example
import tiramisu/scene
import tiramisu/geometry
import tiramisu/material
import tiramisu/transform
import gleam/option
// High detail mesh (shown up close)
let assert Ok(high_geo) = geometry.sphere(radius: 1.0, width_segments: 32, height_segments: 32)
let assert Ok(mat) = material.new() |> material.with_color(0x00ff00) |> material.build()
let high_detail = scene.Mesh(
id: "tree-high",
geometry: high_geo,
material: mat,
transform: transform.identity,
physics: option.None,
)
// Low detail mesh (shown far away)
let assert Ok(low_geo) = geometry.sphere(radius: 1.0, width_segments: 8, height_segments: 8)
let low_detail = scene.Mesh(
id: "tree-low",
geometry: low_geo,
material: mat,
transform: transform.identity,
physics: option.None,
)
scene.LOD(
id: "optimized-tree",
levels: [
scene.lod_level(distance: 0.0, node: high_detail), // 0-50 units away
scene.lod_level(distance: 50.0, node: low_detail), // 50+ units away
],
transform: transform.identity,
)
pub fn lod_level(
distance distance: Float,
node node: Node(id),
) -> LODLevel(id)
Create an LOD level with a distance threshold and scene node.
Levels should be ordered from closest (distance: 0.0) to farthest.
Example
let high_detail = scene.lod_level(distance: 0.0, node: detailed_mesh)
let low_detail = scene.lod_level(distance: 100.0, node: simple_mesh)
pub fn mesh(
id id: id,
geometry geometry: geometry.Geometry,
material material: material.Material,
transform transform: transform.Transform,
physics physics: option.Option(physics.RigidBody),
) -> Node(id)
Create a mesh scene node.
Meshes are the basic building blocks for 3D objects. They combine geometry (shape), material (appearance), and transform (position/rotation/scale).
Physics: Optional rigid body for physics simulation.
Example
import tiramisu/scene
import tiramisu/geometry
import tiramisu/material
import tiramisu/transform
import gleam/option
import vec/vec3
// Create a red cube
let assert Ok(cube_geo) = geometry.box(width: 1.0, height: 1.0, depth: 1.0)
let assert Ok(red_mat) = material.new()
|> material.with_color(0xff0000)
|> material.build()
scene.Mesh(
id: "player",
geometry: cube_geo,
material: red_mat,
transform: transform.at(position: vec3.Vec3(0.0, 1.0, 0.0)),
physics: option.None,
)
pub fn model_3d(
id id: id,
object object: asset.Object3D,
transform transform: transform.Transform,
animation animation: option.Option(animation.AnimationPlayback),
physics physics: option.Option(physics.RigidBody),
) -> Node(id)
Create a 3D model node from a loaded asset (GLTF, FBX, OBJ).
Use this for models loaded via the asset module. Supports animations and physics.
Animation: Optional animation playback (single or blended). See animation module.
Physics: Optional rigid body for physics simulation.
Example
import tiramisu/scene
import tiramisu/asset
import tiramisu/animation
import tiramisu/transform
import vec/vec3
import gleam/option
import gleam/list
// Load model from cache
let assert Ok(gltf_data) = asset.get_model(cache, "character.glb")
// Find walk animation
let walk_clip = list.find(gltf_data.animations, fn(clip) {
animation.clip_name(clip) == "Walk"
})
let walk_anim = animation.new_animation(walk_clip)
|> animation.set_speed(1.2)
|> animation.set_loop(animation.LoopRepeat)
scene.Model3D(
id: "player",
object: gltf_data.scene,
transform: transform.at(position: vec3.Vec3(0.0, 0.0, 0.0)),
animation: option.Some(animation.SingleAnimation(walk_anim)),
physics: option.None,
)
pub fn particles(
id id: id,
emitter emitter: particle_emitter.ParticleEmitter,
transform transform: transform.Transform,
active active: Bool,
) -> Node(id)
Create a particle emitter scene node.
Particle systems create effects like fire, smoke, sparks, or magic spells.
See the particle_emitter module for configuring emitters.
Active: Set to True to emit particles, False to pause emission.
Example
import tiramisu/scene
import tiramisu/particle_emitter as particles
import tiramisu/transform
import vec/vec3
import gleam/option
// Fire effect
let fire_emitter = particles.new()
|> particles.with_rate(50.0) // 50 particles per second
|> particles.with_lifetime(1.0, 2.0) // Live 1-2 seconds
|> particles.with_initial_velocity(vec3.Vec3(0.0, 3.0, 0.0))
|> particles.with_velocity_randomness(vec3.Vec3(0.5, 0.5, 0.5))
|> particles.with_size(0.5, 0.1) // Start large, end small
|> particles.with_color(0xff4500, 0xff0000) // Orange to red
|> particles.with_opacity(1.0, 0.0) // Fade out
scene.Particles(
id: "campfire",
emitter: fire_emitter,
transform: transform.at(position: vec3.Vec3(0.0, 0.0, 0.0)),
active: True,
)