# 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.
```rust
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)
```rust
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
```rust
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)
```rust
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
```rust
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
```rust
// 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
```rust
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
```rust
let mut orbit = OrbitCamera::new()
.target(Vec3::ZERO)
.distance(10.0)
.fov(75.0)
.mode(OrbitMode::Interactive); // or AutoRotate { speed: 0.5 }
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)
```rust
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](https://crates.io/crates/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
```rust
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
```toml
[dependencies]
hoplite = { git = "https://github.com/xandwr/hoplite" }
```
```rust
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:
```bash
cargo run --example black_hole
```
## API Reference
### Setup Context (`SetupContext`)
| `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`)
| `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)`:
| `.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)`:
| `.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
| `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:
```wgsl
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:
```wgsl
@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