hoplite 0.1.0

A creative coding framework for Rust that gets out of your way
Documentation

Hoplite

A creative coding framework for Rust that gets out of your way.

Write shaders, render 3D scenes, and build visualizations with a single closure. No boilerplate, no ceremony—just code for the screen.

use hoplite::*;

fn main() {
    run(|ctx| {
        ctx.default_font(16.0);
        ctx.hot_effect_world("shaders/nebula.wgsl");

        move |frame| {
            frame.text(10.0, 10.0, &format!("FPS: {:.0}", frame.fps()));
        }
    });
}

Philosophy

Hoplite is built on three principles:

  1. One closure, one call — Your setup and frame logic live in closures. No trait implementations, no engine lifecycle to memorize.

  2. Hot reload everything — Edit your WGSL shaders and watch them update instantly. No restart required.

  3. Escape hatches everywhere — Start simple, access the full wgpu API when you need it.

Features

Shader-First Rendering

run(|ctx| {
    // Background effect rendered every frame
    ctx.hot_effect_world("shaders/starfield.wgsl");

    // Post-processing that reads the previous pass
    ctx.hot_post_process("shaders/bloom.wgsl");

    move |frame| { /* your frame logic */ }
});

Effects and post-process passes chain automatically. The render graph handles ping-pong buffers, texture binding, and presentation.

3D Mesh Rendering

run(|ctx| {
    ctx.enable_mesh_rendering();
    let cube = ctx.mesh_cube();
    let sphere = ctx.mesh_sphere(32, 16);

    move |frame| {
        frame.draw_mesh(cube, Transform::new()
            .position(Vec3::new(0.0, 2.0, 0.0))
            .rotation(Quat::from_rotation_y(frame.time))
            .uniform_scale(1.5),
            Color::rgb(0.9, 0.3, 0.2)
        );
    }
});

Meshes render with depth testing, respecting effect passes and post-processing in the pipeline.

Textured Meshes

run(|ctx| {
    ctx.enable_mesh_rendering();
    let cube = ctx.mesh_cube();
    let tex = ctx.texture_minecraft_cobblestone(16, 42);

    move |frame| {
        frame.draw_mesh_textured(cube, Transform::new(), Color::WHITE, tex);
    }
});

2D Sprites

run(|ctx| {
    let sprite = ctx.sprite_from_file("assets/icon.png").unwrap();

    move |frame| {
        // Draw at position
        frame.sprite(sprite, 10.0, 10.0);

        // Draw scaled with tint
        frame.sprite_scaled_tinted(sprite, 100.0, 100.0, 64.0, 64.0, Color::rgb(1.0, 0.5, 0.5));

        // Draw a region (for sprite sheets)
        frame.sprite_region(sprite, 200.0, 100.0, 32.0, 32.0, 0.0, 0.0, 16.0, 16.0);
    }
});

Sprites render in the 2D overlay layer on top of all 3D content and effects.

Orbit Camera

let mut orbit = OrbitCamera::new()
    .target(Vec3::ZERO)
    .distance(10.0)
    .fov(75.0)
    .mode(OrbitMode::Interactive);  // or AutoRotate { speed: 0.5 }

move |frame| {
    orbit.update(frame.input, frame.dt);
    *frame.camera = orbit.camera();
}

Interactive mode: drag to rotate, scroll to zoom. Auto-rotate mode for demos and visualizations.

Immediate-Mode 2D

move |frame| {
    // Simple primitives
    frame.rect(10.0, 10.0, 100.0, 50.0, Color::rgba(0.2, 0.2, 0.2, 0.8));
    frame.text(20.0, 20.0, "Hello, Hoplite!");

    // Debug panels with title bars
    let y = frame.panel_titled(10.0, 100.0, 200.0, 150.0, "Debug");
    frame.text(18.0, y + 8.0, &format!("Time: {:.1}s", frame.time));
}

All 2D draws are batched and rendered as an overlay after your render pipeline completes.

Runtime Hot Reload

Edit any .wgsl file passed to hot_effect* or hot_post_process* methods. Hoplite watches the filesystem and recompiles shaders on change. If compilation fails, the previous working shader stays active.

[hot-reload] Reloading shader: "shaders/nebula.wgsl"
[hot-reload] Shader compiled successfully

Quick Start

[dependencies]
hoplite = { git = "https://github.com/yourname/hoplite" }
use hoplite::*;

fn main() {
    run_with_config(
        AppConfig::new().title("My App").size(1280, 720),
        |ctx| {
            ctx.default_font(16.0);

            // Your setup here

            move |frame| {
                // Your frame logic here
            }
        }
    );
}

Examples

Run the black hole demo with gravitational lensing:

cargo run --example black_hole

API Reference

Setup Context (SetupContext)

Method Description
default_font(size) Load the default font at given pixel size
effect(shader) Add a screen-space effect pass
effect_world(shader) Add a world-space effect with camera uniforms
post_process(shader) Add screen-space post-processing
post_process_world(shader) Add world-space post-processing
hot_effect(path) Hot-reloadable screen-space effect
hot_effect_world(path) Hot-reloadable world-space effect
hot_post_process(path) Hot-reloadable screen-space post-process
hot_post_process_world(path) Hot-reloadable world-space post-process
enable_mesh_rendering() Enable 3D mesh pipeline
mesh_cube() Create a unit cube mesh
mesh_sphere(segments, rings) Create a UV sphere mesh
mesh_plane(size) Create a flat plane mesh
add_texture(texture) Add a texture, returns index
texture_from_file(path) Load texture from file
texture_from_bytes(bytes, label) Load texture from memory
texture_minecraft_noise(size, seed) Procedural dirt/stone texture
texture_minecraft_grass(size, seed) Procedural grass texture
texture_minecraft_cobblestone(size, seed) Procedural cobblestone texture
add_sprite(sprite) Add a sprite, returns SpriteId
sprite_from_file(path) Load sprite from file (linear filtering)
sprite_from_file_nearest(path) Load sprite from file (pixel art)
sprite_from_bytes(bytes, label) Load sprite from memory

Frame Context (Frame)

Method Description
fps() Current frames per second
width() / height() Screen dimensions in pixels
text(x, y, str) Draw text at position
text_color(x, y, str, color) Draw colored text
rect(x, y, w, h, color) Draw filled rectangle
panel(x, y, w, h) Draw a bordered panel
panel_titled(x, y, w, h, title) Panel with title bar
draw_mesh(index, transform, color) Draw a 3D mesh
draw_mesh_textured(index, transform, color, tex) Draw a textured 3D mesh
sprite(id, x, y) Draw sprite at position
sprite_tinted(id, x, y, tint) Draw sprite with color tint
sprite_scaled(id, x, y, w, h) Draw sprite at custom size
sprite_scaled_tinted(id, x, y, w, h, tint) Draw scaled sprite with tint
sprite_region(id, x, y, w, h, sx, sy, sw, sh) Draw sprite sub-region

Frame Fields

Field Type Description
time f32 Total elapsed time in seconds
dt f32 Delta time since last frame
input &Input Keyboard and mouse state
camera &mut Camera Current camera (modify to change view)
gpu &GpuContext Low-level GPU access
draw &mut Draw2d Low-level 2D API

Shader Uniforms

World-space shaders receive these uniforms:

struct Uniforms {
    resolution: vec2f,
    time: f32,
    fov: f32,
    camera_pos: vec3f,
    _pad1: f32,
    camera_forward: vec3f,
    _pad2: f32,
    camera_right: vec3f,
    _pad3: f32,
    camera_up: vec3f,
    aspect: f32,
}

@group(0) @binding(0) var<uniform> u: Uniforms;

Post-process shaders also get the input texture:

@group(0) @binding(1) var input_texture: texture_2d<f32>;
@group(0) @binding(2) var input_sampler: sampler;

Dependencies

Hoplite builds on solid foundations:

  • wgpu — Cross-platform GPU abstraction
  • winit — Window creation and input handling
  • glam — Fast math types (Vec3, Mat4, Quat)
  • fontdue — Font rasterization
  • bytemuck — Safe casting for GPU buffers

License

MIT