Expresso ☕️

Lightweight 3D physics engine for Gleam game development.

Package Version Hex Docs

Overview

Expresso provides deterministic 3D rigid body physics using position-based dynamics with quaternion rotations and spatial acceleration (BVH/Grid). Designed for games, simulations, and particle systems.

Key Features:

Installation

gleam add expresso

Quick Start

import expresso/body
import expresso/world
import vec/vec3

pub fn main() {
  // Create world
  let world = world.new(gravity: vec3.Vec3(0.0, -9.8, 0.0))
  
  // Add bodies
  let world =
    world
    |> world.add_body(body.new_sphere("ball", vec3.Vec3(0.0, 5.0, 0.0), radius: 0.5))
    |> world.add_body(body.new_box("platform", vec3.Vec3(0.0, 0.0, 0.0), half_extents: vec3.Vec3(5.0, 0.5, 5.0)))
  
  // Step physics
  let #(world, events) = world.step(world, delta_time: 1.0 /. 60.0)
  
  // Get updated positions
  let assert Ok(ball) = world.get_body(world, "ball")
}

Core Concepts

Creating Bodies

// Sphere - best for projectiles and particles
let bullet = body.new_sphere("bullet", vec3.Vec3(0.0, 0.0, 0.0), radius: 0.25)

// Box - best for walls and platforms
let wall = body.new_box("wall", vec3.Vec3(0.0, 0.0, 0.0), 
                        half_extents: vec3.Vec3(10.0, 1.0, 5.0))

// Capsule - best for character controllers
let player = body.new_capsule("player", vec3.Vec3(0.0, 2.0, 0.0), 
                              height: 2.0, radius: 0.5)

// Cylinder - best for pillars and towers
import spatial/collider
let tower = body.new("tower", vec3.Vec3(5.0, 0.0, 0.0),
                     collider.cylinder(center: vec3.Vec3(0.0, 0.0, 0.0), 
                                      radius: 1.0, height: 3.0))

Body Types

// Dynamic - affected by physics
let dynamic = body.new_sphere("ball", vec3.Vec3(0.0, 5.0, 0.0), radius: 0.5)

// Kinematic - player-controlled, not affected by forces
let kinematic = 
  body.new_capsule("player", vec3.Vec3(0.0, 0.0, 0.0), height: 2.0, radius: 0.5)
  |> body.kinematic()

// Static - doesn't move
let static = 
  body.new_box("ground", vec3.Vec3(0.0, 0.0, 0.0), half_extents: vec3.Vec3(10.0, 0.5, 10.0))
  |> body.with_mass(0.0)

Rotation

import quaternion

// Set rotation
let rotation = quaternion.from_axis_angle(vec3.Vec3(0.0, 1.0, 0.0), 1.57)
let box = 
  body.new_box("box", vec3.Vec3(0.0, 0.0, 0.0), half_extents: vec3.Vec3(1.0, 1.0, 1.0))
  |> body.with_rotation(rotation)

// Add angular velocity
let spinner = 
  body.new_sphere("spinner", vec3.Vec3(0.0, 0.0, 0.0), radius: 1.0)
  |> body.with_angular_velocity(vec3.Vec3(0.0, 0.0, 2.0))

World Management

// Create and configure world
let world = 
  world.new(gravity: vec3.Vec3(0.0, -9.8, 0.0))
  |> world.with_iterations(10)
  |> world.with_restitution(0.5)

// Update body each frame
let world = 
  world.update_body(world, "player", fn(body) {
    body.with_velocity(player_velocity)
  })

// Step physics
let #(world, events) = world.step(world, delta_time: 0.016)

Spatial Queries

// Find bodies near a point
let nearby = world.query_radius(world, center: position, radius: 5.0)

// Find bodies in a region
let visible = world.query_region(world, 
                                 min: vec3.Vec3(-10.0, -10.0, -10.0),
                                 max: vec3.Vec3(10.0, 10.0, 10.0))

// Find nearest body
case world.query_nearest(world, point: player_pos) {
  Some(#(id, body, distance)) -> // Use nearest
  None -> // No bodies found
}

Collision Layers

// Set layer and collision mask
let player_bullet =
  body.new_sphere("bullet", vec3.Vec3(0.0, 0.0, 0.0), 0.2)
  |> body.with_layer(body.layer_projectile)
  |> body.with_collision_mask(body.combine_layers([
       body.layer_enemy,
       body.layer_environment,
     ]))

Predefined layers: layer_default, layer_player, layer_enemy, layer_projectile, layer_environment, layer_trigger, layer_all

Raycasting

case world.raycast(world, 
                   origin: player_pos,
                   direction: vec3.Vec3(1.0, 0.0, 0.0),
                   max_distance: 100.0,
                   layer_mask: option.None) {
  option.Some(hit) -> {
    // hit.body_id, hit.distance, hit.point, hit.normal
  }
  option.None -> {}
}

Collision Events

let #(world, events) = world.step(world, delta_time: 1.0 /. 60.0)

list.each(events, fn(event) {
  case event {
    world.CollisionStarted(a, b) -> // Bodies started colliding
    world.CollisionEnded(a, b) -> // Bodies separated
    world.TriggerEntered(body: b, trigger: t) -> // Body entered trigger
    world.TriggerExited(body: b, trigger: t) -> // Body left trigger
  }
})

Trigger Zones

// Non-physical sensor
let checkpoint =
  body.new_sphere("checkpoint", vec3.Vec3(10.0, 0.0, 0.0), 2.0)
  |> body.trigger()

Advanced Features

// Friction
let surface = 
  body.new_box("surface", vec3.Vec3(0.0, 0.0, 0.0), half_extents: vec3.Vec3(10.0, 0.5, 10.0))
  |> body.with_static_friction(0.6)
  |> body.with_dynamic_friction(0.4)

// Continuous collision detection (for fast objects)
let fast_bullet = 
  body.new_sphere("bullet", vec3.Vec3(0.0, 0.0, 0.0), radius: 0.1)
  |> body.with_velocity(vec3.Vec3(100.0, 0.0, 0.0))
  |> body.with_ccd(True)

// Spatial strategy selection
let world = world.with_spatial_strategy(world, world.UseBVH)  // Dynamic scenes
let world = world.with_spatial_strategy(world, world.UseGrid(cell_size: 2.0))  // Particles

How It Works

Expresso uses position-based dynamics with spatial acceleration:

  1. Apply forces (gravity, external)
  2. Integrate velocities
  3. Broad-phase collision detection (BVH/Grid)
  4. Narrow-phase collision (shape intersection)
  5. Resolve constraints (iterative separation)
  6. Apply impulses (velocity update)

Spatial Acceleration:

Performance

Body CountStrategyPerformance
<10Brute ForceExcellent (automatic)
10-100BVHExcellent
100-1000BVH/GridGood
>1000GridGood

Limitations

Expresso does not support:

For advanced features, consider full physics engines like Rapier or PhysX.

Documentation

Full API documentation: HexDocs

Key modules:

Development

gleam test           # Run tests
gleam build          # Build project
gleam docs build     # Generate docs

License

MIT

Search Document