Ragnavoxel docs

Worlds

VoxelScene for worlds that fit in memory, ChunkStreamer for unbounded worlds generated around a focus with level-of-detail rings and look-ahead, stamping primitives, editing streamed chunks, the floating origin, and saving to TileDB.

Voxel content reaches the renderer two ways: a bounded VoxelScene that keeps itself drawn, or a ChunkStreamer for unbounded worlds generated around a focus.

VoxelScene: worlds that fit in memory

world/VoxelScene.hpp. For levels, dioramas, editor previews and any game whose world fits in memory. Coordinates are world voxels from the scene’s minimum corner, which sits at render-space origin.

rv::world::VoxelScene scene(materials, glm::ivec3(8, 2, 8));  // size in chunks
auto rock = scene.voxelOf(rockMaterial);
auto turf = scene.voxelOf(turfMaterial);

// Terrain: whole bricks under each column's top are set at once.
scene.fillColumns({0, 0, 0}, scene.sizeVoxels(), rock, [&](int x, int z) { return heightAt(x, z); });
scene.fillSphere({4096, 900, 4096}, 300.0f, rv::voxel::kAir);  // carve
scene.fillBox({100, 0, 100}, {164, 64, 164}, turf);
scene.fillWhere(lo, hi, rock, [](const glm::ivec3& v) { return (v.x ^ v.z) & 64; });  // any shape
scene.setVoxel({10, 20, 30}, turf);
scene.stamp(trees);  // a PrimitiveSet: trees, rocks, models

scene.attach(renderer());     // once
scene.syncAll(renderer());    // at start-up: mesh and upload everything
// each frame:
scene.sync(renderer());       // remesh changed chunks on the job system, upload within budget

rendererConfig(base) gives a renderer configuration whose voxel and atmosphere volumes cover the scene. sync returns how many chunks still wait for staging room; the rest go on the following frames. raycast(origin, direction, maxDistance) returns the first solid voxel hit, the face entered through and the distance. stats() counts chunks meshed and uploaded and quads drawn. Air carves, and edits are clipped to the scene.

ChunkStreamer: unbounded worlds

world/Streaming.hpp. Around a focus point, chunks are wanted at a level of detail chosen by horizontal distance:

Level Detail LostVale’s ring
kLodMicro full voxel detail: editable, with colliders and herbs 40 m
kLodBrick everything at brick resolution, flora at its own cells 64 m
kLodCoarse brick ground, flora in 50 cm cells, small plants dropped 96 m
kLodBlock whole blocks: the edge of the streamed world 288 m

Past about 40 m a voxel is under a quarter of a pixel at 1600 × 900, so the brick ring keeps everything that can still be seen at a fraction of the quads. Beyond the streamed world, far terrain carries on to the horizon.

A game supplies a TerrainSource: generate(chunk, lod, out) fills a chunk at the requested level of detail, thread-safely; classify(chunk, uniform) says whether a chunk is empty, uniform (and of what) or mixed, so whole chunks of air and rock cost nothing; and columnBand gives the vertical range of chunks worth asking for in a column.

rv::world::ChunkStreamer streamer(palette, source, rv::world::StreamingConfig{});

void onUpdate(float dt) override {
  streamer.update(world(), renderer(), *physics(), focusBlock, view);
}

Ordering. Work goes by what the camera needs soonest: a chunk with nothing drawn comes before any chunk being remade at new detail; within each, nearness along the focus’s path ahead (look-ahead by seconds and metres of the path), then whether it is in view; chunks only made coarser, behind the focus, come last. Jobs in flight are bounded by their expected worker time, so cheap whole-block chunks far out flow freely beside costly near ones. Generation and meshing run on workers; finished meshes are staged to the GPU without stalling, within a main-thread budget a frame.

Chunks as entities. Each resident chunk is an ECS entity carrying ChunkCoord, ChunkVoxelsRef, ChunkMeshRef, ChunkRender and ChunkCollider (world/ChunkComponents.hpp). Colliders are built asynchronously as the focus comes near, and a rebuilt collider replaces the old body only once it is ready, so the ground never drops out from under a player.

Querying. voxelAt(world, voxel) reads resident data (falling back to classification); settled(radius) is true once every wanted chunk within that many metres is meshed and uploaded, and unsettled(radius) lists what each holdout waits on; stats() counts resident, generating, meshing and lagging chunks with per-phase timings.

Editing. edit(world, lo, hi, fn) changes voxels in a box of chunks resident at full detail: fn(voxel, current) returns the new id. Changed chunks are copied on write, remeshed with their neighbours, and returned for you to persist. Edits prepared off the main thread from a snapshot are swapped in with replace, which refuses if the chunk changed meanwhile. invalidate(chunk) forces a chunk to regenerate from the source after its stored edits change.

Tuning. StreamingConfig sets the four ring radii, job limits, the per-frame upload and residency budgets, look-ahead, how much chunks out of view are put off, and the hysteresis that keeps a chunk from flickering between levels at a ring’s edge.

The floating origin

Render and physics space are world space minus an origin kept in whole blocks. When the focus has moved far enough, rebase: set the renderer’s origin, shift the regions, and call streamer.applyOrigin(world, renderer) to re-apply every resident chunk’s render-space origin. LostVale rebases every 512 m, so a world a thousand kilometres across draws with full precision at the camera.

Primitives

voxel/Primitives.hpp. A Primitive is a world-placed stamp: a capsule, ellipsoid, box or a grid of voxel art, with a grain (per voxel, or once per clump of bricks filled uniformly) and an override priority for where it meets voxels already there:

Priority Replaces
Foliage only air and water
Wood, Rock also foliage
Embed anything, ground included (stones bedded in the soil they sit in)
Overlay only what is already solid (a patch of different ground under the surface, never above it)

A PrimitiveSet bins stamps by chunk and rasterises them into chunk storage, at any level of detail. Models from the asset library instantiate into primitive sets.

Occupancy

world/Occupancy.hpp computes the coverage (0 to 255) of opaque voxels over a region of cells, in parallel over chunks, straight into an upload buffer if you like. The renderer’s lighting volumes (sun visibility, light shafts) are built from it.

Saving worlds: TileDB

storage/WorldStore.hpp. Chunks live in a 3D sparse TileDB array indexed by chunk coordinate, with one variable-length, zstd-compressed byte attribute. TileDB writes are fragment-based and lock-free, so batches of chunks are written from many threads at once (writeChunks(chunks, writers), one fragment a writer) instead of funnelling through a single writer. World metadata (seed, palette, dimensions) is array metadata (writeMeta, readMeta). readAllChunks(fn) streams every stored chunk back; consolidate() merges fragments. LostVale keeps every chunk the player has changed whole in such an array, overriding generation (coarsened for distant levels of detail).

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