hoplite

Crates.iohoplite
lib.rshoplite
version0.1.9
created_at2025-12-16 14:50:29.332916+00
updated_at2025-12-17 11:08:39.575151+00
descriptionA creative coding framework for Rust that gets out of your way
homepage
repositoryhttps://github.com/xandwr/hoplite
max_upload_size
id1987989
size1,062,191
Xander (xandwr)

documentation

https://docs.rs/hoplite

README

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

Background Color (No Shader Required)

run(|ctx| {
    ctx.background_color(Color::rgb(0.1, 0.1, 0.15));  // Dark blue-gray
    ctx.enable_mesh_rendering();
    let cube = ctx.mesh_cube();

    move |frame| {
        frame.mesh(cube).at(0.0, 0.0, -5.0).draw();
    }
});

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 (Fluent Builder API)

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

    move |frame| {
        // Fluent builder style (recommended)
        frame.mesh(cube)
            .at(0.0, 2.0, -5.0)
            .color(Color::rgb(0.9, 0.3, 0.2))
            .draw();

        // With full transform control
        frame.mesh(sphere)
            .transform(Transform::new()
                .position(Vec3::new(0.0, 0.0, -3.0))
                .rotation(Quat::from_rotation_y(frame.time)))
            .color(Color::BLUE)
            .draw();
    }
});

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_blocky_stone(16, 42);  // Returns TextureId

    move |frame| {
        // Builder style
        frame.mesh(cube).texture(tex).draw();

        // Or classic style
        frame.draw_mesh_textured(cube, Transform::new(), Color::WHITE, tex);
    }
});

Loading 3D Models

run(|ctx| {
    ctx.enable_mesh_rendering();

    // Fluent API for loading and transforming geometry
    let model = ctx.load("model.stl")
        .centered()      // Center at origin
        .upright()       // Convert Z-up to Y-up
        .normalized()    // Fit in unit cube
        .scaled(2.0)     // Scale to desired size
        .unwrap();

    move |frame| {
        frame.mesh(model).draw();
    }
});

Load STL files with automatic transformations. The fluent API chains centering, orientation fixes, and scaling before GPU upload.

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.set_camera(orbit.camera());  // Clean camera setting
}

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

Entity Component System (ECS)

run(|ctx| {
    ctx.enable_mesh_rendering();
    let cube = ctx.mesh_cube();  // Returns MeshId (type-safe handle)

    // Spawn entities during setup
    ctx.world.spawn((
        Transform::new().position(Vec3::new(0.0, 0.0, -5.0)),
        RenderMesh::new(cube, Color::RED),  // MeshId directly, no wrapper needed
    ));

    move |frame| {
        // Query and update entities
        for (_, transform) in frame.world.query::<&mut Transform>().iter() {
            transform.rotation *= Quat::from_rotation_y(frame.dt);
        }

        // Render all entities with mesh components
        frame.render_world();
    }
});

Built on hecs — a fast, minimal ECS. Use it for game objects, particles, or any dynamic entity management. The immediate-mode API still works alongside ECS.

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/xandwr/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
background_color(color) Set solid background color (no shader needed)
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, returns MeshId
mesh_sphere(segments, rings) Create a UV sphere mesh, returns MeshId
mesh_plane(size) Create a flat plane mesh, returns MeshId
load(path) Load geometry from file, returns MeshLoader
load_stl_bytes(bytes) Load STL from bytes, returns MeshLoader
add_texture(texture) Add a texture, returns TextureId
texture_from_file(path) Load texture from file, returns TextureId
texture_from_bytes(bytes, label) Load texture from memory
texture_blocky_noise(size, seed) Procedural dirt/stone texture
texture_blocky_grass(size, seed) Procedural grass texture
texture_blocky_stone(size, seed) Procedural stone 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
set_camera(camera) Set the camera (cleaner than *frame.camera = ...)
mesh(id) Start a mesh builder chain (fluent API)
draw_mesh(id, transform, color) Draw a 3D mesh (classic API)
draw_mesh_textured(id, transform, color, tex) Draw a textured 3D mesh (classic API)
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
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
render_world() Render all ECS entities with Transform + RenderMesh

Mesh Builder (MeshBuilder)

The fluent API for drawing meshes, created via frame.mesh(id):

Method Description
.at(x, y, z) Set position
.position(Vec3) Set position from Vec3
.transform(Transform) Set full transform (position, rotation, scale)
.color(Color) Set color/tint
.texture(TextureId) Apply texture
.draw() Queue the mesh for rendering

Mesh Loader (MeshLoader)

The fluent API for loading geometry, created via ctx.load(path):

Method Description
.centered() Center geometry at origin
.upright() Convert Z-up to Y-up orientation
.normalized() Scale to fit in unit cube
.scaled(factor) Apply uniform scale
.translated(Vec3) Move geometry by offset
.rotated_by(Quat) Apply custom rotation
.smooth_normals() Recalculate vertex normals
.unwrap() Finalize and return MeshId
.build() Finalize and return Result<MeshId>

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 (or use set_camera())
world &mut World ECS world for entity management
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)
  • hecs — Fast, minimal Entity Component System
  • fontdue — Font rasterization
  • bytemuck — Safe casting for GPU buffers
  • image — Image loading and handling

License

MIT

Commit count: 0

cargo fmt