Introduction & quickstart
roxlap is a voxel-scene engine in pure Rust. You describe a world as
voxel grids — carve, fill, stream, and animate them at runtime — and
one renderer facade draws it either on the CPU (a per-pixel 3D-DDA
raycaster that runs anywhere, no GPU required) or on the GPU (a
WGPU compute-shader marcher, same retro look at much higher frame
rates), falling back from one to the other automatically. The engine
reads Ken Silverman’s Voxlap asset formats (.vxl worlds, .kv6
sprites, .kfa animation rigs), so two decades of existing voxel
assets load directly.
This chapter gets a window on screen with a voxel world in it. By the end you will have seen the whole per-frame contract — everything after this is elaboration.
The three crates you talk to
roxlap is a Cargo workspace of small crates, but a game depends on three:
roxlap-render— the facade. OneSceneRenderertype that owns the backend choice (CPU or GPU), presentation, sprites, picking, and the post pipeline. Your game calls this.roxlap-scene— the world.Sceneholds many independently-placed chunked voxelGrids in one f64 world, with edits, streaming, and snapshots.roxlap-core— theCameraand the per-frame render settings.
Everything else (roxlap-formats, roxlap-gpu, …) arrives
transitively. Add to your Cargo.toml:
[dependencies]
roxlap-render = "0.22" # SceneRenderer — one renderer over CPU + GPU
roxlap-scene = "0.22" # Scene / Grid / edits / streaming
roxlap-core = "0.22" # Camera + per-frame render settings
glam = "0.30"
A minimal application
The complete program below ships as a compile-tested example —
crates/roxlap-render/examples/quickstart.rs
(~160 lines: a winit window, an event loop, and a slow orbit camera).
Run it first, then read the walkthrough:
cargo run --release -p roxlap-render --example quickstart
ROXLAP_GPU=0 cargo run --release -p roxlap-render --example quickstart # force CPU
Every snippet in this section is included verbatim from that file, so it cannot drift from what actually compiles.
Colours
/// [`VoxColor`] packs a voxel colour: RGB + a brightness byte (NOT
/// alpha; `rgb()` uses the neutral 0x80 — lighting bakes rewrite it).
const GRASS: VoxColor = VoxColor::rgb(0x4d, 0x8a, 0x3a);
const DOME: VoxColor = VoxColor::rgb(0x40, 0x60, 0xc0);
/// Sky/fog/tints are plain [`Rgb`] — no packing surprises.
const SKY: Rgb = Rgb::new(0x8f, 0xbc, 0xd4);
Two conventions to absorb immediately (both inherited from Voxlap, both covered properly in Concepts & conventions):
- Voxel colours are
VoxColors — RGB plus a brightness byte (not alpha!).VoxColor::rgb(r, g, b)packs at the neutral0x80. - Sky/fog/tint colours are plain
Rgb— a different type, so mixing the packings is a compile error.
Building a scene
/// A one-grid scene. Grid-local voxel coordinates; **+z is down**, so
/// the ground surface at `z = 210` fills downward toward the bedrock
/// placeholder at `z = 255`, and "up" is toward smaller z.
fn build_scene() -> Scene {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
let grid = scene.grid_mut(id).expect("grid just added");
grid.set_rect(
IVec3::new(-128, -128, 210),
IVec3::new(127, 127, 254),
Some(GRASS),
);
grid.set_sphere(IVec3::new(0, 0, 205), 30, Some(DOME));
scene
}
A Scene is a set of grids; each Grid is an unbounded chunked voxel
volume placed in the world by a GridTransform (f64 position +
quaternion rotation). Here one grid at the origin gets a solid ground
slab (set_rect) and a dome (set_sphere).
The third convention, and the one that trips everyone: +z points
DOWN. The ground surface sits at z = 210 and fills downward to
z = 254; the dome centre at z = 205 is above the ground. Voxlap
kept screen-y and world-z aligned, and roxlap keeps Voxlap’s
convention so assets and math port unchanged.
Creating the renderer
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("roxlap quickstart"))
.expect("create window"),
);
let size = window.inner_size();
// QE.7b - BackendPreference replaces want_gpu; the demos prefer
// GPU with automatic CPU fallback.
let backend = if std::env::var_os("ROXLAP_GPU").is_none_or(|v| v != "0") {
BackendPreference::PreferGpu
} else {
BackendPreference::Cpu
};
let opts = RenderOptions {
backend,
clear_sky: SKY,
..RenderOptions::default()
};
self.renderer = Some(SceneRenderer::new(
window.clone(),
(size.width, size.height),
&opts,
));
self.window = Some(window);
self.scene = Some(build_scene());
self.started = Some(Instant::now());
}
SceneRenderer::new takes anything implementing raw-window-handle —
winit here, but SDL or your own windowing works the same — plus the
surface size and RenderOptions. BackendPreference::PreferGpu asks
for the GPU compute backend and falls back to the CPU renderer
automatically when WGPU init fails, so the same binary runs on a
machine with no usable GPU. (The renderer field is declared before the
window in the struct so it drops first — the surface must release its
window handles while the window is still alive.)
Rendering a frame
/// One frame: orbit the camera, render the scene, present.
fn render_frame(renderer: &mut SceneRenderer, scene: &mut Scene, window: &Window, elapsed: f64) {
// `Camera::orbit` / `from_yaw_pitch` / `look_at` produce the
// canonical right-handed basis the sprite frustum cull needs —
// don't hand-roll one.
let camera = Camera::orbit(elapsed * 0.3, 0.35, 220.0, [0.0, 0.0, 195.0]);
let size = window.inner_size();
let settings = OpticastSettings::for_oracle_framebuffer(size.width.max(1), size.height.max(1));
// Defaults for everything but the sky; the GPU projection is
// derived from `settings`, so both backends show the same field
// of view.
let mut frame = FrameParams::new(&settings);
frame.sky_color = SKY;
frame.fog_color = SKY;
renderer.render(scene, &camera, &frame);
renderer.present(); // render() composites; present() finishes
}
The per-frame contract:
- Build a
Camera. Use the constructors (orbit,from_yaw_pitch,look_at) — they produce the right-handed basis the engine’s frustum culling expects. - Build
FrameParamsfromOpticastSettingsfor the current surface size, then override what you need (sky and fog colour here). renderdraws the scene into the backend’s target;presentputs it on screen. They are separate calls so you can draw overlays or an egui HUD between them — see Rendering & backends.
Teardown
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
// Drain in-flight GPU work before surface/window teardown so
// quitting never yanks the swapchain mid-submission.
if let Some(renderer) = self.renderer.as_mut() {
renderer.wait_idle();
}
}
Call wait_idle before the window is torn down so quitting never
yanks the swapchain out from under in-flight GPU work.
Explore the demos
The engine’s feature gallery is roxlap-scene-demo — eleven scenes
behind a menu (World, Sprites, Animation, Transparency, Lighting,
Spotlight, Particles, Doom, Picking, Primitives, Empty):
cargo run --release -p roxlap-scene-demo # CPU
ROXLAP_GPU=1 cargo run --release -p roxlap-scene-demo # GPU backend
ROXLAP_SCENE=Lighting cargo run --release -p roxlap-scene-demo # jump to a tab
The demo tour chapter maps each scene to the features it demonstrates; each topic chapter uses its scene as the worked example.
Where next
- Concepts & conventions — coordinate system, colour packing, units, camera basis: the five facts that make everything else make sense. Read this before writing real code.
- The scene graph — grids, chunks, edits, streaming.
- Rendering & backends — the facade in full.