Rendering
The Vulkan 1.4 renderer: tessellated, displaced voxel quads, GPU-driven cluster culling with two-phase Hi-Z, far terrain to the horizon, water, glass and grass, volumetric fog, cloud and light shafts, fire and spell volumes, shadows, physical light and the post chain, plus the 2D sprite renderer.
render/Renderer.hpp, rv::gfx::Renderer. Vulkan 1.4 with push descriptors, dynamic rendering and synchronization2, on SDL3, with volk for function loading and VMA for memory. Depth is reverse-Z throughout.
The renderer draws voxel chunks, actors (skinned and rigid meshes with LODs), far terrain, water, glass, grass, weather, fire and spell volumes, volumetric fog and clouds, cascaded shadows, bloom and tone mapping, then sprites and UI. Every pass works for perspective and orthographic views, with finite or infinite far planes and lens shift.
renderer.render(camera.view(), environment, settings, {}, dt);
The Application calls this for you with what onRender filled in.
Voxel surfaces
Greedy quads (see Meshing) are vertex-pulled as four-point patches and tessellated. The tessellation evaluation shader displaces each face by its world-space heightfield (granite, bark, thatch and the rest), faded to zero at the quad’s edges so neighbours never crack. Tessellation is set by pixels on screen (tessPixels, tessMax, tessDistance), so near faces get relief and far ones stay cheap.
GPU-driven visibility
Quads are grouped into clusters of up to 256 same-facing quads. Each frame a compute pass tests every cluster against:
- the view frustum;
- its facing (a cluster of faces all turned away is skipped);
- a two-phase Hi-Z occlusion pyramid: last frame’s visible set draws first, then clusters newly revealed are tested against the depth it left.
The survivors are written as indirect draws (vkCmdDrawIndirectCount). Clusters whose relief cannot be resolved at their distance skip tessellation and draw as plain triangles. Shadow casters are culled per cascade the same way.
Far terrain
Beyond the voxel radius, up to ten nested clipmap levels (4 m to 2 km spacing in LostVale) draw the land to the horizon from the game’s own height data, with geomorphing between levels, a sea shelf and lake beds seen through the same water absorption as the voxel sea, per-pixel field patchwork and woodland canopy, and aerial perspective. RendererConfig::farLevels turns it on; the game feeds each level with setFarLevel.
Water, glass and grass
- Water is its own pass with absorption by depth, so shallows show their bed and deep water darkens.
- Glass voxels are solid to physics but drawn by the glass pass: light and sight pass through, coloured by the pane, and the seam colour draws the lead cames. Light through coloured glass colours what it falls on.
- Grass is a dedicated pass that grows dense blades over every material that asks for it, thinning with distance (
grassDistance,grassDensity), bent by travelling gusts along the wind, parted by whatever moves through it, tinted by the season, and buried by the snow map (setSnowMap).
Air and light
The atmosphere is simulated on the GPU in voxel volumes every frame:
- a fog density volume with semi-Lagrangian advection, valley and forest mist, and burn-off;
- a cloud field;
- a sun-visibility volume ray-marched through the world’s occupancy voxels.
A half-resolution volumetric integration gives light shafts through the canopy, followed by a bilateral upsample. Fireflies and dust are microvoxel motes simulated in compute (moteCount).
Light is physical. One unit of illuminance is about 20,000 lux, so the noon sun is about 5.5 and a high full moon about 1.5 × 10−5. When the sun is down the moon casts the shadows, and its disc is shaded by where the sun lies (its phase is geometry). Firelight and flames are physical too; things that are not (spells, glowing crystals, wisps, seams) keep the look they were made with, scaled by artScale against the eye’s adapted exposure.
Local lights (fires, torches, braziers, glowing crystals) are submitted each frame. Fire is drawn by a flame pass as voxels of burning gas at 1/128 m near the eye, with a light that breathes with the flames on the GPU from the same seed.
Shadows are cascaded; orthographic views centre their cascades on the camera’s focus distance.
Post
Bloom (bloomStrength), an AgX grade, film grain and vignette. LostVale’s “kintsugi seams”, emissive lines round every sizeable merged tile, and the mosaic value jitter per quad are renderer settings (kintsugiSeams, seamGlow, mosaicJitter), so a game can use the meshing hierarchy as its visual language or switch it off.
The environment and settings
gfx::Environment is the frame’s world state: sun and moon directions, colours and intensities, sky colours, star visibility, fog density and falloff, valley mist, wind direction, strength and gust phase, cloud coverage and altitude, exposure and time.
gfx::RenderSettings holds the knobs: the debug view (shaded, mesh levels, quads, AO), wireframe, tessellation, seams and mosaic, bloom, volumetric intensity, steps and distance, motes, grain, vignette, and grass distance and density.
Budgets
gfx::RendererConfig sizes what is resident: chunk slots (65,536 by default), quad and cluster capacity, the occupancy and fog volumes and their cell sizes, far terrain levels, and the staging bytes uploaded a frame (48 MiB). The volumes cover camera-centred regions, so the world itself can be unbounded. app::compactRenderer() gives small budgets for little scenes and 2D games; RendererConfig::interfaceOnly() builds only the sprite and UI pipelines, for launchers and tools that must start instantly on a cold pipeline cache.
Actors
Skinned and rigid meshes are created with createActorMesh(vertices, indices) and drawn each frame with submitActors(draws, joints). Each draw can carry a coat (a spell’s sheen over the body) and a fighter’s tell (a pulsing rim of light). Meshes are destroyed with destroyActorMesh, deferred until in-flight frames retire.
Floating origin
Render space is world space minus the renderer’s origin (setOrigin, in metres, double precision). Streamed worlds rebase the origin as the focus moves (LostVale every 512 m), so precision near the camera never degrades however far the world runs.
Textures, screenshots and capture
createTexture, updateTexture, updateTextureRegion and destroyTexture manage sRGB textures for sprites and UI. saveScreenshot(path) writes the frame; captureFrame reads back colour and depth for tests.
The 2D renderer
render/Sprites.hpp. Fill a gfx::SpriteFrame and hand it over each frame with renderer.submitSprites(frame):
Sprite: the position of its pivot, size, pivot, rotation, UV rectangle, linear colour, texture, layer, a sort key (for example-yfor top-down depth) and flags (kSpriteFlipX,kSpriteFlipY,kSpriteAdditive).Tilemap: a grid of tile indices into an atlas (atlasColumnsbyatlasRows, with a cell inset against bleeding). Index 0 is empty; only tiles in view are drawn.viewProjfromCamera2D::viewProjection(),yDownto match the camera,sampling(Nearestfor pixel art) andclearColor.
Sprites are culled, sorted by layer, key, blend and texture, and drawn by instancing in batches (renderer.spriteStats()). With the 3D scene off they are the frame, over the clear colour; with it on they lie over the tone-mapped scene, under the UI. Colours are linear and alpha is straight (not premultiplied).
Written from the engine's headers and source as of commit 02e81b2 (2026-11-01).