Ragnavoxel docs

Getting started

What Ragnavoxel is, how a game is laid out on it, the Application host and its hooks, AppConfig, the fixed-step loop, and building the engine and its samples.

Ragnavoxel is a C++20 game engine on Vulkan 1.4 (with volk and VMA), SDL3, Jolt Physics, TileDB, miniaudio and an AoSoA archetype ECS. It began as LostVale’s microvoxel engine and is now general: any voxel size, any camera (perspective, orthographic, off-centre), 3D voxel scenes, skinned and rigid actors, and a 2D sprite and tile-map renderer, all behind a small application host.

Everything is in namespace rv, and the public headers live under include/ragnavoxel/:

Folder What
app/ Application, the host that owns everything and runs the loop
core/ types, units and the voxel size, the job system, noise and hashing, AoSoA, logging, resource paths
ecs/ entities, components, queries, resources, the staged scheduler
voxel/ chunks, palettes, materials, primitives, the mesher, the asset library
world/ VoxelScene (bounded worlds), ChunkStreamer (unbounded worlds), chunk components, occupancy
render/ the GPU device, pipelines, the 3D renderer, sprites, view maths
scene/ cameras and camera rigs
model/ prop models and microvoxel model grids
physics/ the Jolt wrapper
audio/ the audio engine
platform/ window, keyboard, mouse and gamepads
storage/ TileDB world storage

Building

The engine builds with CMake presets and takes its dependencies from vcpkg:

cmake --preset release
cmake --build build/release

That builds the library, its tests, the samples in build/release/ragnavoxel/samples/ and the editor in build/release/ragnavoxel/editor/. Vulkan pipelines are cached per GPU and driver in ~/.cache/ragnavoxel/, so only the first launch after a driver or shader change compiles them.

The Application

app/Application.hpp owns the window, the GPU device, the renderer, input, the ECS world and (if asked for) physics and audio, and runs a fixed-step loop. Subclass it and override its hooks:

#include "ragnavoxel/app/Application.hpp"
#include "ragnavoxel/scene/Camera.hpp"
#include "ragnavoxel/scene/CameraControllers.hpp"

struct MyGame : rv::app::Application {
  using Application::Application;
  rv::scene::Camera camera;
  rv::scene::OrbitController orbit;

  void onStart() override { /* load content, build the world */ }
  void onFixedUpdate(double dt) override { /* simulation at a fixed rate */ }
  void onUpdate(float dt) override {
    orbit.update(camera, rv::scene::cameraInputFromDevices(input(), dt), glm::vec2(framebufferSize()), dt);
  }
  void onRender(rv::app::FrameContext& f) override { f.camera = camera.view(); }
};

int main(int argc, char** argv) {
  rv::app::AppConfig cfg;
  cfg.title = "My game";
  cfg.parseArgs(argc, argv);
  return MyGame(cfg).run();
}

The hooks

Hook When
onStart() once, before the first frame
onFixedUpdate(dt) every fixed step (AppConfig::fixedStep, 1/60 s by default), at most maxStepsPerFrame times a frame; physics steps after it
onUpdate(dt) once a frame after the fixed steps; clock().alpha says how far the frame lies between steps, for interpolation
onRender(frame) fill the camera, environment and render settings; submit sprites, actors and UI to renderer()
onResize(size) the drawable changed size (pixels)
onShutdown() as the loop ends

ECS systems registered on fixedSystems() run every fixed step and those on frameSystems() once a frame, both in stage order (see The ECS). The world holds the app::Clock and a pointer to the platform::InputState as resources.

The host also gives you window(), device(), renderer(), input(), world(), physics() and audio() (null when switched off), clock(), framebufferSize(), aspect(), quit(code), and loadTexture(path) for PNG, JPG, TGA and BMP images.

AppConfig

Field Default Meaning
title, width, height, fullscreen, vsync “Ragnavoxel”, 1600 × 900, windowed, on the window
validation off Vulkan validation layers (slow; for engine work)
voxelSize 1/64 m the world’s voxel edge, set before anything is created (see Voxels and units)
scene3D on the 3D scene (voxels, actors, sky, post), or a 2D game of sprites and UI; switchable later with renderer().setSceneEnabled
renderer sized for a streamed world the renderer’s budgets; app::compactRenderer() suits small scenes and 2D, gfx::RendererConfig::interfaceOnly() launchers and tools
physics, audio on whether they start
shaderDir the engine’s compiled shaders RV_SHADER_DIR overrides it
fixedStep, maxStepsPerFrame 1/60 s, 8 the fixed step, and how many a frame may take before a long hitch slows the game instead of spiralling
screenshotPath, screenshotFrames test aid: render that many frames at a fixed 1/60 s step, save a screenshot and quit

parseArgs reads --screenshot PATH, --frames N, --size WxH, --fullscreen, --validation, --no-vsync and --mute. A --screenshot run steps exactly one fixed step a frame, so its pictures are reproducible.

The clock

app::Clock holds seconds (simulation time: fixed steps taken), frameDelta (real seconds since the last frame, clamped to 0.25), fixedStep, alpha, frame and steps.

Resources beside the executable

core/Resources.hpp: a shipped build keeps its assets and shaders beside its executable, and a development build reads them from the source tree. Ask for each by name with the development path as the fallback:

const char* assets = rv::resourceDir("assets", "/home/me/game/assets");

That gives, in order: the environment variable RV_ASSETS_DIR when set, the folder assets beside the running executable when it exists, else the fallback.

Where to next

Written from the engine's headers and source as of commit 02e81b2 (2026-11-01).