Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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. One SceneRenderer type that owns the backend choice (CPU or GPU), presentation, sprites, picking, and the post pipeline. Your game calls this.
  • roxlap-scene — the world. Scene holds many independently-placed chunked voxel Grids in one f64 world, with edits, streaming, and snapshots.
  • roxlap-core — the Camera and 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 neutral 0x80.
  • 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:

  1. Build a Camera. Use the constructors (orbit, from_yaw_pitch, look_at) — they produce the right-handed basis the engine’s frustum culling expects.
  2. Build FrameParams from OpticastSettings for the current surface size, then override what you need (sky and fog colour here).
  3. render draws the scene into the backend’s target; present puts 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