W A S D: fly (forward is where you are looking) E / space / Q: up / down ← → ↑ ↓: look click: dig the block under the crosshair B: place a block against the targeted face T: cycle the held block type C: hidden-face culling on/off G: generate a new world The readout shows how many faces survive the cull out of the total the chunk would have, how many were actually drawn this frame, and the frame rate.
A blocky world you can fly through, dig into and build on, rendered face by face with the pen — with the two optimisations that make voxel rendering possible at all, and a key that turns one of them off so you can see what it was doing. HIDDEN-FACE CULLING — THE MEASUREMENT A 16 × 12 × 16 chunk is 3072 cells and the terrain fills about 1,800 of them. That is 10,700 cube faces, and almost every one of them is pressed flat against another solid block where nobody can ever see it. Keeping only the faces where a solid block touches air leaves 1,330 — 87.6% eliminated. The running project reports the same thing from its own counters: 1,194 of 11,190 on one seed, 1,246 of 10,320 on another, so 88–89% either way. The number that matters more is faces actually drawn, because back-face and frustum culling also apply. Measured headless on the default view: **461 faces per frame with the cull on, 4,129 with it off** — a 9× difference in real work, for the same picture. That is what C is for. Which leaves a question worth asking: if the picture is identical, how do you know the cull is not silently dropping something? Because those two counts are produced by completely different code paths — build_faces with the cull emits per-neighbour, without it emits all six — and both were reproduced independently in tools/mkbanner.py, which is a numpy-free reimplementation of the generator, the cull and the rasteriser, and is what draws the thumbnail. NO DEPTH SORT Painter's algorithm wants faces back to front, and the obvious way is to sort them every frame. For a regular grid of axis-aligned cubes you do not need to. Traverse each axis independently from the slab furthest from the eye to the slab nearest, nest the three loops in any order, and the result is already a correct back-to-front ordering — Frieder, Gordon and Reynolds proved this in 1985, and it is why voxel splatters do not sort. The per-axis orders are rebuilt each frame by a two-pointer merge inward from both ends, which handles the awkward case of the eye being inside the chunk that a naive count-up or count-down gets wrong. Total cost: 44 comparisons, against the several thousand a sort of the face list would need. Two more things carry their weight: One transform per cell, not four per face. The camera-space image of a one-block step along each world axis is computed once per frame, so a face's four corners are its cell's base corner plus a couple of vector adds. Nine multiplies per cell replaces thirty-six per face. Back-face tests hoist out of the loops. Whether a +X face is visible depends only on the eye's x against the slab's x, so the test lives in the x loop, not next to the face — one comparison per slab instead of one per face. Filling a quad is a sweep between two opposite edges: joining equal parameters on two opposite edges of a convex quad covers it exactly. Picking the shorter screen span as the sweep direction matters more than it sounds, because a wall seen edge-on is a long thin quad, and sweeping it the wrong way is sixteen short strokes where one long one will do. Digging and building use a real 3D DDA (Amanatides and Woo) rather than marching the ray in small steps: it visits voxels in exact order, has no step size to get wrong, and the cell it entered last is exactly where a placed block belongs. All original - code, art and sound. See Inside is open.