| Crates.io | hoplite |
| lib.rs | hoplite |
| version | 0.1.9 |
| created_at | 2025-12-16 14:50:29.332916+00 |
| updated_at | 2025-12-17 11:08:39.575151+00 |
| description | A creative coding framework for Rust that gets out of your way |
| homepage | |
| repository | https://github.com/xandwr/hoplite |
| max_upload_size | |
| id | 1987989 |
| size | 1,062,191 |
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()));
}
});
}
Hoplite is built on three principles:
One closure, one call — Your setup and frame logic live in closures. No trait implementations, no engine lifecycle to memorize.
Hot reload everything — Edit your WGSL shaders and watch them update instantly. No restart required.
Escape hatches everywhere — Start simple, access the full wgpu API when you need it.
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();
}
});
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.
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.
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);
}
});
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.
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.
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.
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.
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.
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
[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
}
}
);
}
Run the black hole demo with gravitational lensing:
cargo run --example black_hole
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)| 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 |
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 |
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> |
| 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 |
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;
Hoplite builds on solid foundations:
MIT