Journal
Voxels6 min read

Why I Built My Own Engine

My voxel engine is my own, written from scratch in C# on Vulkan. Here is why, and what a frame looks like from the inside.

By James

The same meadow frame split into three strips: the colour-coded normals view, the image after the temporal blend alone, and the finished frame.

I built my voxel engine myself, from scratch, in C# on Vulkan. If you are wondering whether Unity or Unreal sits underneath, neither does: there is no game engine below it and no plugin for someone else's editor. Every voxel you see in this journal was stored, traced, lit and put on screen by code I wrote.

That is not a statement about other engines. Unity and Unreal are extraordinary pieces of software, and for a game built from triangle meshes they are the natural choice. That is not what I am building. I am building a world out of voxels five centimetres across, every one of which can be dug out, placed or painted, lit by path tracing. That particular combination pushes on exactly the parts of an engine that a general-purpose one keeps to itself.

This entry explains what I mean, and walks through what happens inside a single frame.

The short answer: voxels this small are a data problem

A general engine is built around meshes: triangles prepared ahead of time, streamed in, drawn by a rasteriser, lit with a mix of baked and real-time tricks. It does that superbly.

This world has no meshes. My test world alone is more than a million bricks of 512 voxels each, stored as one byte per voxel, and any of them can be changed at any moment. For that to work I need to decide:

  • how the voxels are laid out in memory, because at this scale a single wasted byte per voxel is hundreds of megabytes;
  • how rays find them, because the GPU's ray tracing hardware is built for triangles and has to be taught about voxels;
  • what happens in the frame an edit lands, because it has to be visible immediately, with no rebuild and no hitch;
  • how to test all of it, because a renderer can be quietly wrong in ways nobody spots.

In a general engine those decisions are made for you, deep inside code you do not own. Owning them is the whole point of this project, so I own the engine.

The pieces

At the level of a block diagram, a frame looks like this.

CPU (C# / .NET)GPU (VULKAN)OUTPUTWorldbricks of 8x8x8 voxelsgeneration, edits, undoFrame setupcamera, sun, windchanged bricks onlyUploadvoxel data to GPU memoryrebuild ray structuresRay tracing passhardware finds the bricksa shader walks voxelsshade: sun, sky, lampsone bounce, glass, waterDenoise (compute)reproject last frameblend with historyedge-aware waveletfilterComposite (compute)tone map (AgX)encode to sRGBWindowswapchain presentHeadlessframe_NNNN.pngmetrics.json
One frame, from the world on the CPU to pixels. The CPU owns the voxels; the GPU traces them; compute passes clean up the image; the result goes to a window or, just as often, to a PNG file with its measurements.

The CPU owns the world

The authoritative copy of the world lives on the CPU, in C#. It is divided into bricks of 8 x 8 x 8 voxels, and only bricks with something in them exist. Generation, editing and undo all happen here, and each frame only the bricks that changed are sent to the GPU.

An early version of the voxel glade seen from inside the forest: broadleaf and conifer trees over a meadow of grass and small flowers.
An early frame from inside the forest of my test world. Every trunk, leaf and blade of grass is voxels, generated from a seed on the CPU.

The GPU traces it, with hardware help

Modern graphics cards have dedicated ray tracing hardware. It is very good at one question: given a ray, what does it hit first? Out of the box it understands triangles. For anything else, such as my voxel bricks, you describe your objects as boxes and supply a small program, an intersection shader, that the hardware calls whenever a ray enters one of your boxes. Mine walks the voxels inside the brick and reports the first solid one.

Setting that up in Vulkan looks roughly like this:

// A ray tracing pipeline for custom (non-triangle) geometry.
var stages = new[]
{
    Stage(ShaderStage.RayGen,       "raygen.spv"),       // one ray per pixel starts here
    Stage(ShaderStage.Miss,         "miss.spv"),         // the ray hit nothing: sample the sky
    Stage(ShaderStage.Intersection, "voxels.rint.spv"),  // walk the voxels inside a box
    Stage(ShaderStage.ClosestHit,   "shade.rchit.spv"),  // the nearest hit: record the surface
};

var groups = new[]
{
    General(0),                                  // raygen
    General(1),                                  // miss
    ProceduralHit(intersection: 2, closest: 3),  // "boxes" instead of triangles
};

var pipeline = vk.CreateRayTracingPipelines(device, new RayTracingPipelineCreateInfo
{
    Stages = stages,
    Groups = groups,
    MaxRecursionDepth = 1,   // shadow and bounce rays are traced in a loop, not recursively
    Layout = pipelineLayout,
});

The scene the rays search is described to the hardware as an acceleration structure: a tree of boxes that lets it skip empty space quickly. When bricks change, the parts of that structure that describe them are rebuilt.

Light, then compute passes

The ray tracing pass does the path tracing: for each pixel it finds the surface, then traces shadow rays towards the sun and nearby lamps, a bounce ray for indirect light, and rays through water and glass. That gives one noisy sample per pixel.

Everything after that is compute shaders: small general-purpose GPU programs. A denoiser in the SVGF family blends each pixel with its own history from previous frames and then filters it with an edge-aware wavelet, so noise disappears without blurring the edges of voxels. A final pass tone maps the result with AgX and encodes it for the screen.

The same meadow frame after the full denoiser, smooth with crisp voxel edges.
A meadow by the pond after the temporal blend alone, with visible grain on the grass and flowers.
History onlyFinished frame
The same frame after the temporal blend alone and after the full denoiser. The grain on the grass disappears; the edges of the voxels stay sharp. Drag the divider.

The frame loop

Stripped of detail, the loop that ties it together is short:

while (running)
{
    input.Poll();
    camera.Update(input, dt);

    world.ApplyPendingEdits();              // CPU: carve, place, paint, undo
    var changed = world.TakeChangedBricks();
    gpu.Upload(changed);                    // only what changed this frame
    gpu.RebuildAccelerationFor(changed);

    using var frame = gpu.BeginFrame();
    frame.TraceRays(camera, sun, wind);     // one sample per pixel
    frame.Denoise();                        // compute: history + wavelet filter
    frame.Composite(look);                  // compute: tone map, encode

    if (headless) capture.Save(frame);      // PNG + metrics.json
    else          swapchain.Present(frame);
}

The last two lines matter more than they look.

Headless capture is a first-class citizen

The same engine that draws to a window can run with no window at all: render a scene from a given camera, save the frames as PNGs and write a metrics file with the GPU time of every stage. It was the first feature I built, before the engine could draw anything worth looking at.

It is how the numbers in this journal were measured. It lets me compare frames byte for byte after a change, time two builds against each other fairly, and replay hundreds of seeded random edits to look for bugs. There is a whole entry about that: how I test a real-time renderer. In a general engine you can bolt something like this on. In my own, it is part of the frame loop.

The same rolling voxel terrain in the finished frame.
Rolling voxel terrain in the normals debug view, every surface coloured by the direction it faces.
Normals viewFinished frame
One of the engine's first test scenes, a kilometre of rolling terrain at 5 cm, in a debug view and as the final image. Debug views like this one are deterministic, which makes them ideal for exact comparisons.

What I gave up

Building your own engine means building everything: the window, the input, the camera, the capture tools, the editing tools. There is no asset store and no forum thread where someone has already solved your exact problem. The engine also requires ray tracing hardware, with no fallback renderer, because a second renderer is a second engine to maintain.

In return I get something I could not get any other way: complete control over how a very large amount of very small data is stored, traced, changed and verified. For a world made entirely of editable five-centimetre voxels, that control is the product.

The next entries go deeper: what the engine can do, how I test it, and what I built with it.