A voxel engine in C++20 and OpenGL 4.1, written solo. The world has four spatial dimensions. What you walk around in is a three-dimensional cross-section of it, and you can turn the cut.
The camera is locked in that clip. Nothing moves except the hyperplane the slice is cut on, and the landscape rebuilds itself because you are looking at a different cross-section of the same fixed world.
cmake -B build -G Ninja && cmake --build build -j && ./build/voxel_engine
Scroll the wheel to turn the cut. Full build notes, controls, what it is checked against.
Same world, same cut, same camera. Only the block shape changes, and P
toggles it live. The right angles in the first frame are square because a
cube says so; the obtuse corners in the second are where the hyperplane
actually cut the block.
A boulder is a 4-ball, so the slice takes a sphere out of it with radius
sqrt(r² - d²). Travel along the fourth axis and it swells, peaks, and
vanishes. A monolith is a 4-box, so turning the cut takes its footprint
from a rectangle to a hexagon.
Turning the cut in one plane leaves every cell four-sided however far it goes, so drawing those blocks as cubes is exact rather than an approximation - the frame halfway through that loop is a 0.45 rad rotation where cubes are still right. It takes a second plane to open the pentagons and hexagons. Nothing is reseeded on the way: follow one face through the sweep and it keeps its material and only changes shape.
CLIP_TIME_OF_DAY=0.76 ./scripts/capture_clip.sh w 60
The other clips turn the hyperplane. This one moves it, and the cut
stays flat the whole way - theta and phi are zero in every frame, so
nothing you see changing is changing because of an angle. A lake with a
treeline becomes a snowfield with a monolith, nine units along the fourth
axis, in the same world at the same camera.
It is a one-way ramp played forward and then backward rather than a
sweep that returns, and that is a measurement rather than a stylistic
choice. A fresh process at w=0 is byte-identical run to run and the
clip's first frame matches it exactly, but a frame that arrives at w=0
by travelling out to 9 and back differs by 9.77/255: travel does not
round-trip the way rotation does, and rotation is the one with a
byte-identical round-trip invariant. Rather than ship a loop whose two
ends quietly disagree, every frame here is a fresh point on the outward
journey and the return half is those same frames reversed.
The mesher's own design notes said this geometry could not be greedy-meshed. Neighbouring cells present their polygons at different angles, so no two faces line up to be merged.
That is true of the polygons and false of the conclusion.
The preimage of a convex set under a linear map is convex. to_4d is
linear and a lattice box is convex, so a box of cells presents one
convex polygon at any angle. Greedy meshing works on a tilted cut. It
just has to sweep in lattice space rather than slice space.
| cut | before merging | after |
|---|---|---|
| flat | 2.47x greedy | 1.41x |
| ZW only | 3.45x | 1.96x |
| both planes | 4.97x | 3.05x |
| hard tilt | 4.98x | 3.58x |
Left to right: no tilt, one rotation plane, both. A cut turned in a single plane presents four-sided cells at every angle, so a cube is exact there and not an approximation. Only a compound turn makes pentagons and hexagons.
The gap that is left is not slack. Re-merging in a third dimension recovers about 1% of the quads at any cut, measured in docs/bench/merge-headroom.md after the design notes claimed otherwise. A turned hyperplane simply meets more cells, and each one brings its own edges.
These are ratios and byte counts, so they reproduce anywhere. CI runs
--bench three times and requires byte-identical output; the x86-64
Ubuntu runner emits the same bytes as the arm64 M4 this was built on.
| Greedy meshing vs naive per-face | 5.3x fewer triangles, CI-gated at 4.5x |
| Vertex format, packed vs float | 40 B to 12 B |
| Whole-world GPU mesh, all three wins | 126.7 MB to 10.9 MB, 11.6x |
| Index data per chunk | zero, one shared quad buffer |
| Chunk serialization, RLE vs raw | 39.06 MB to 0.67 MB, 58x |
| Sub-chunks drawn vs loaded, underground | up to 70x fewer |
Frame times are a different kind of number. They belong to one laptop, so they are quoted here as the mean of four runs on an Apple M4:
| radius | voxels | triangles | frame | inside 60 Hz | |
|---|---|---|---|---|---|
| cubes | 32 | 277 M | 988,436 | 8.2 ms | 2.0x |
| cubes | 16 | 71 M | 260,018 | 5.1 ms | 3.3x |
| 4D cross-sections | 16 | 71 M | 470,810 | 6.4 ms | 2.6x |
276,889,600 voxels across 4,225 chunks is the largest world tested.
--validate --radius 32 reads every mesh back off the GPU and reports
bad_triangles=0, so that is correct geometry and not just a fast
number. Terrain generation and meshing run on nine workers; every GL
call stays on the main thread.
The millisecond column moves with machine load. The triangle and byte columns beside it never move, which is why the CI gates are on those and not on the clock.
Full sweeps, pass breakdowns, and the reasoning behind each gate: docs/performance.md.
The same terrain meshed both ways, in wireframe. Left is one quad per block face; right is the same surface after coplanar faces merge.
./build/voxel_engine --pose-at 300,30,-300,-108,3 --time-of-day 0.728 \
--godrays 1.4 --radius 12 --screenshot-after 150
The water reflects the sky it is under rather than a colour authored to
look like one, so a low sun is the best hour for a lake rather than the
worst. --godrays marches shafts from whatever is brighter than the sky
itself; it is off by default because it costs +2.30 ms of a 5.0 ms frame,
and every number in this README comes off a benchmark that must not
quietly acquire it. Swimming under the surface closes sight to 34 m and
takes the red out of the light. The details.
./build/voxel_engine --3d --pose-at 30,46,30,180,12 \
--time-of-day 0.84 --radius 8 --screenshot-after 100
Aurora curtains, a hashed starfield, a cloud deck gone slate, and
fireflies over the trees - all in one frame and none of it geometry. The
aurora is a handful of sines on the view direction inside the sky shader;
the fireflies derive every position from gl_VertexID and the clock, so
nine thousand of them are one draw call with no CPU work and nothing to
keep in sync.
./build/voxel_engine --3d --pose-at 18,52,18,-118,10 --time-of-day 0.30 \
--radius 8 --weather 0.35 --screenshot-after 100
Nothing places that bow. A rainbow is a ring 42 degrees off the point opposite the sun, so it is drawn against the antisolar direction and it rises as the sun sets, on its own. Each colour channel gets its own angle
- red leaves a droplet at 42.4 degrees and blue at 40.1 - which is why the secondary bow above it comes out with its colours reversed without anything asking for that.
./build/voxel_engine --3d --pose-at 300,95,-360,112,18 \
--time-of-day 0.79 --radius 8 --screenshot-after 90
Nine effects share that idea, and each has a scale with 0 to turn it off: swaying foliage (the shadow pass applies the same offset, or a canopy's shadow stays where the canopy no longer is), fireflies thinning to dust by day, leaves off the canopy, butterflies through the daylight hours, rain and snow, valley mist, flocks overhead, the aurora, and clouds dappling the ground. Meteors and the bow have no scale because they are conditions rather than features: one needs night, the other needs sun and rain at once. Rain also dimples the lake it falls on.
Together the whole atmosphere is 1.08 ms of a frame, and the leaves and butterflies are 0.17 ms of that - measured interleaved against the same build with them off, three runs each, not assumed. Creatures wander the terrain too, reading the ground height under themselves so they walk over hills rather than through them.
Swimming, weather, wildlife, and the rest of the stills, each with the command that regenerates it: docs/atmosphere.md.
Cascaded shadow maps, a bloom pyramid over an HDR target, per-block light that floods out from a placed Glow block, and a sky drawn entirely in a fragment shader - hashed starfield, a moon riding the sun's own arc, and a cloud deck that goes slate after dark.
Every still here regenerates from a command. Pose, seed, and hour are all arguments, which is the only reason the captions can carry them.
cmake -B build -G Ninja
cmake --build build -j
./build/voxel_engine
Needs CMake 3.20+, Ninja, and a C++20 compiler (Clang 15+, GCC 12+, MSVC 19.3+). The first configure takes about two minutes while FetchContent clones GLFW, GLM, and Dear ImGui. macOS is the primary target; Linux and Windows build clean on CI.
./build/voxel_engine --bench # mesher and cull benchmark, no window
./build/voxel_engine --validate # read meshes back off the GPU and check them
./build/voxel_engine --slice-prisms # blocks as 4D cross-sections
./build/voxel_engine --3d # the ordinary three-dimensional world
./build/voxel_engine --wind 0 # still air; 1 is the default breeze
./build/voxel_engine --motes 0 # no fireflies or dust
./build/voxel_engine --leaves 0 # no leaves off the canopy
./build/voxel_engine --butterflies 0 # no butterflies
./build/voxel_engine --weather 0.9 # pin a downpour; omit for the natural cycle
./build/voxel_engine --creatures 0 # empty the world of wildlife
./build/voxel_engine --aurora 0 # ... and every other effect has a scale, 0 off
| Key | Action |
|---|---|
| WASD, Space, Left Ctrl | Move, up, down |
| Left Shift | Sprint |
| F | Toggle walk / fly |
| Left / right click | Break / place block |
| 1-8 | Pick block (Glow is a light source) |
| Tab, F2, F12 | Mouse capture, HUD, screenshot |
| F5 / F6 | Save / load ./saves/world1/ |
T, [, ] |
Pause and step time of day |
| O, V, Esc | Occlusion culling, vsync, quit |
Four-dimensional controls:
| Key | Action |
|---|---|
| Mouse wheel | Turn the cut in the ZW plane. This is the one that reshapes the world. |
| E / Q | Travel along w, the fourth axis |
| Hold M or middle mouse | The mouse turns the cut: vertical is ZW, horizontal is XW |
| P | Draw blocks as their 4D cross-section instead of as cubes |
Walking does not morph the world, at any tilt, and neither does it in 4D Miner. Your slice is a fixed hyperplane and WASD moves you within it. The wheel is what turns the cut.
The engine is the thing being measured, so most of the work went into making the measurements hard to fake.
- Nine test binaries run by ctest, including a differential fuzz that decomposes every quad from the greedy and naive meshers back into unit faces and compares them as sets, over 10,020 fill x neighbour x seed cases
- 20 four-dimensional invariants in
--verify-4d, among them a byte-identical rotation round-trip: turn the cut 0.25 rad and back, and every byte of the world has to return - Byte-identity gates.
check_invariance.pyruns--benchthree times and requires identical output, on both CI platforms - A registration guard.
check_test_targets.pycompares the tests that actually registered against a written-down list, after an unclosedif(APPLE)once left Linux running six tests of eight and reporting 100% passed - Conventional Commits, checked on every pull request by
check_commit_messages.py. Its rules were derived from this repo's own 311 commits rather than copied off the spec, so the two that would have rejected a quarter of that history are deliberately not enforced - TSan and ASan/UBSan over the full logic suite in CI
--validatereads meshes back off the GPU and checks every triangle against the voxel data that produced it
| docs/how-it-works.md | How the engine works, from scratch - start here if graphics is new to you |
| docs/cross-section.md | How the cross-section works, with pictures |
| docs/atmosphere.md | Wind, fireflies, weather, birds, mist - one idea used five times |
| docs/4d.md | The engineering log: what broke, and what it cost |
| docs/performance.md | Every measurement, with its command |
| docs/design.md | Architecture and layering |
| docs/bench/ | Benchmark artifacts and inputs |
A frame crosses a thread boundary exactly once, and that is the rule the whole design is arranged around:
nine workers main thread (owns every GL call)
------------ --------------------------------
4D terrain -> greedy mesh --> finished queue -> GPU upload
(Perlin in (sweeps in (owned buffers, |
lattice lattice never pointers v
space) space) into the map) frustum + section cull
|
v
shadow -> sky -> terrain ->
water -> post (HDR, bloom)
Workers never touch GL, and the main thread never generates terrain. A worker gets a 16 KB copy of its neighbours' boundary layers rather than a pointer into the chunk map, so a chunk can be meshed against neighbours that are themselves still being rebuilt.
src/core/ Window, input, timing, thread pool, CLI, frame stats
src/gfx/ Shader, texture, mesh, camera, shadows, post-process, water
src/world/ Blocks, chunks, meshers, 4D terrain, the cross-section kernel
src/render/ Lighting and the shadow / sky / terrain / water passes
src/game/ Player, physics, block interaction
src/ui/ Debug HUD
src/bench/ Headless benchmarks
gfx/ knows nothing about voxels. world/ owns voxel data and meshing
and never reaches into gameplay. game/ is the only layer that
coordinates world, player, and input. Chunk generation and meshing run on
a worker pool; GL calls run on the main thread only.
Block textures are AI-generated and disclosed in TEXTURES.md.

















