1. The decided design
Eleven decisions, their reasoning, and what each one rules out. Plus the finding that falls straight out of the digging model and simplifies the data layer.
Background: the three axes, now answered
"Organic terrain" bundles three things that can move independently: the
silhouette (how the surface is shaped), the content (what the generator
produces), and the interaction (what a dig does). The all-cubes simplification in
commit 4656681 only reset the first; the other two were still on placeholder
implementations. All three are now decided, and they are decided in the same direction:
parity with the reference.
The decisions
Locked D1 — Real generation, on the cube renderer first
fBm with domain warping for the surface, 3D coherent noise for caves and overhangs, genuine strata for material layering, and ambient occlusion in the mesher.
Built and validated on the existing cube mesher before any isosurface work. The generator is the same either way — it writes density and material into cells, and cubes or Marching Cubes are two ways of looking at the same field. Building it first means the mesher change can be evaluated against terrain that actually has cliffs, cave mouths and overhangs, rather than against a sine hill that looks acceptable under any mesher.
Replaces: the single-octave
12 + 7·sin(0.1x)·cos(0.1z) heightmap in fillDensityTerrain.
Locked D2 — Terrain silhouette is Marching Cubes
Specifically Marching Cubes, not Surface Nets and not Dual Contouring.
The reasoning is parity: the goal is to match the reference's results, and Marching Cubes is what the reference is understood to use. Surface Nets is cheaper and was what this repo built before, but its vertex placement gives a subtly different surface. Dual Contouring is arguably better — it preserves sharp cliff creases that MC rounds away — but "better" is a divergence, and divergence comes after parity.
Accepts: MC's rounding of every sharp feature, its higher triangle count, and its ambiguous-case problem (see risk 4). Note: an earlier draft cited Transvoxel as MC's natural LOD companion. Distant terrain is now a pre-generated static mesh and Transvoxel is dropped, so MC is chosen purely on parity grounds (risk 5).
Locked D3 — Parity on look and feel, before anything else
Not "the look" or "the feel" — both, to reference parity, first. Optimisation and deliberate divergence are later phases.
This is the decision that removes the sequencing ambiguity from the previous draft of this wiki. It means the isosurface work is not optional, and it also means structural integrity and the digging model are not optional, because they are what "feel" consists of. It sets a clear bar: a change is done when it matches the reference, not when it is interesting.
Locked D4 — Caves and overhangs are required
Which forces genuinely 3D generation. A heightmap cannot express either — every column has exactly one surface by construction — so the generator writes a full 3D density field, with the surface as one contributor among several rather than as the definition of the terrain.
Rules out: any heightfield-based silhouette shortcut, and any storage scheme that assumes one surface per column.
Locked D5 — Terrain digging shrinks the voxel
A pickaxe-style tool damages a terrain voxel, and the damage reduces its density. Because the surface is marched from that density, the geometry visibly shrinks under the tool, hit by hit, until the cell is gone. No brush, no radius — the edit is still per-cell, but its effect is continuous.
This is exactly the reference behaviour and it is the reason a smooth surface is
needed at all: on a cube renderer there is nothing for the shrink to be expressed in.
It also explains why the density brush deleted in 4656681 was the wrong
tool — the reference does not brush. It quantises the edit to a cell while
leaving the surface continuous.
Locked D6 — Construction damage is texture, never shape
A hammer-style tool damages a construction voxel. Its geometry never changes — a construction block is discrete by definition. The damage is communicated entirely through its texture, progressing through crack stages until the block breaks.
Two representations, two damage models, matched to what each representation can express. This is a clean split, and as the next section shows, it also splits the data that backs them.
Locked D7 — Finite world, size a runtime parameter
Bounded exactly as the reference is — a map of fixed extent, not a world that continues indefinitely. The extent itself is not hardcoded: it is a parameter to experiment with, and the number that ships is deferred until there is real terrain to judge it against.
This reverses an earlier decision. A previous draft chose a Minecraft-style effectively infinite world and flagged it as the design's one deliberate divergence from the reference. That divergence is now withdrawn, which makes the design fully consistent with D3 — there is no longer any point at which parity is knowingly given up.
Unblocks: coherent hand-authored content, global generation passes, and precomputed distant terrain — all things a bounded map affords and an unbounded one does not. Does not remove: streaming. At reference scale the world is far too large to hold densely in memory, so chunk residency, async meshing and a bounded upload budget all survive unchanged. See section 5.
Changing the size must not change the terrain that was already there, or two sizes cannot be compared — you would be looking at two different worlds. So generation samples noise at absolute world position and the size is a pure clip rectangle over an unbounded field, and the world is anchored at its centre rather than a corner, so growing it adds terrain at the edges instead of translating everything. Signed coordinates and floor-division throughout follow from the centre anchor.
At the boundary: an invisible wall stops the player, and beyond it only the skybox is drawn — no cliff face, no sealed wall, nothing rendered past the edge. The simple option deliberately; something more considered can replace it later.
Height starts at 384, configurable on the same terms as the horizontal extent so other heights can be generated and judged by feel. Budgeted from the stratigraphy in D10: roughly 190 below the surface datum for the full sequence to read, and roughly 190 above it for relief and build headroom. One trade to know — height multiplies the resident working set directly, since residency is a 2D disc and the whole column is always loaded, so height and view distance compete for the same memory. At a 12-chunk radius, 256 tall is ~460 MB and 384 tall is ~695 MB.
In scope, deferred D8 — Structural integrity on both representations
Terrain and construction both participate: undermine a wall and the structure above collapses; hollow out a cliff and the overhang gives way. Design deferred to its own discussion.
Worth noting now: structural integrity on terrain is the more unusual half, and it interacts with D5 — a partially-mined cell has partial density, so "how much support does a 40%-dense cell provide?" is a question the digging model creates. Flagged so it is not discovered late.
Locked D9 — Worlds are generated up front, as a stored artifact
World generation is a distinct step that runs to completion and produces a stored world, rather than terrain being generated lazily as the player walks into it. A generated world is therefore an artifact on disk rather than something reconstituted on demand.
Sharing or downloading pre-generated worlds is an idea worth keeping, not a committed feature — the discussion is deferred. Nothing in the plan depends on it.
Importantly, the format requirements below do not rest on it. They were first argued from distribution, but each stands independently on the fact that the world is stored rather than regenerated: a player's own saved world must not mutate underneath them when the generator improves, which needs exactly the same guarantees.
This is reference behaviour, and it is what D7 buys. With the whole world in hand at generation time, anything needing global knowledge becomes ordinary work rather than a distributed-consistency problem.
Collapses: most of risk 7 — cross-chunk features no longer need a bounded-radius scheme to generate identically without their neighbours, because their neighbours are all present. Enables: pre-generated terrain LOD, which reshaped risk 5 from the plan's nastiest problem into a tuning decision, and removed the need for Transvoxel entirely.
The cost, and the thing to design against: world creation becomes a visible up-front wait, and iterating on the generator means regenerating. That makes generation speed a first-class concern rather than an afterthought, and it argues for a fast regenerate path from the start.
Storage imposes its own requirements: a versioned world format, and terrain reproducible from the artifact rather than from the running build — so the format carries the generated data and not merely the seed. Reconstituting a world by re-running the generator would make every generator improvement silently alter worlds the player already has, which is the opposite of what a stored world means. A version mismatch has to fail loudly rather than render subtly wrong terrain.
And the artifact is large: at 1 m voxels a 2 km world 384 tall is ~6.4 GB uncompressed, a 4 km world ~25 GB. Compression is therefore not optional, and density fields are unusually friendly to it — long runs of 0 and 255 mean the win should be large rather than marginal. Which scheme is an open phase-2 item; see section 8.
Locked D10 — Layered geology, from soil to unbreakable bedrock
The world has real vertical composition rather than three material bands by depth: types of soil and sand near the surface, distinct rock types below (sedimentary, igneous, metamorphic — marbles and the rest), seams of minerals occurring within specific host rock types, caves and underground rivers cutting through, and bedrock always present at the bottom.
Crucially, the sequence is not uniform across the map: a stratum may sit at different depths in different places, or be absent entirely. Bedrock is the sole exception — it always exists at the base, so the world cannot be dug through.
Two anchoring modes, not one
The layers divide into two kinds that behave differently, and conflating them is what makes strata look painted on:
- Surface-following — topsoil, sand, subsoil, weathered rock. Defined by depth below the local surface, so a hill's topsoil sits on top of the hill.
- Datum-anchored — the rock strata. Defined by absolute Y, warped by low-frequency noise so bands undulate, tilt and pinch out. This is what real stratigraphy does: deposited flat, then deformed.
A stratum is therefore a datum Y plus a thickness field. Where the thickness field falls to zero the layer pinches out and is simply not present in that column — so "different depths, or absent entirely" comes from the same mechanism that places the layer, with no per-region tables and no special cases.
And it makes the height parameter safe. Because the layers are expressed relative to the surface and to the bedrock datum rather than as absolute coordinates, changing the world height stretches the run of deep base rock and leaves everything else untouched — which is why the height in D7 can stay tunable rather than having to be settled before the generator is written.
Bedrock is a hardcoded special case
Not a stratum and not a material with a very high toughness — a special case, by decision. Bedrock cannot be damaged and never deviates from full density, enforced directly rather than falling out of the arithmetic.
The reason for making it explicit: under D5 a strike removes
(255 - 128) / toughness density, so a sufficiently large toughness rounds to
zero removed and the voxel is already effectively invulnerable. That is the right behaviour
arriving by accident, and it reads as a bug to whoever meets it next. An explicit
unmineable flag says what is meant.
It is also an unconditional clamp applied last in generation, not a layer written early. If bedrock were written as a layer, a later cave or river pass could carve through it and the "cannot fall out of the world" guarantee would quietly stop holding.
What this forces elsewhere
- Generation becomes an ordered pipeline. Ore seams that occur only within specific host rock must read the strata pass's output, so the generator is a sequence with barriers rather than a bag of independent functions. See risk 7.
- Four-way vertex material blending stops being a judgement call. Many thin bands stacked vertically means a dug wall cuts across them constantly, and a pinch-out is a point where three bands meet. That is precisely where a two-material blend fails, and it is where the player is looking most closely. See question 6.
- Material count grows.
colorIndexis a byte, so 256 materials is not the ceiling — texture-array memory and the cost of authoring art for each material is.
Natural gas, petroleum and underground rivers are wanted, but how they behave is a separate discussion, deliberately not settled here. The fork is large: a pocket that is merely a material which looks like oil and sits there until mined costs one more entry in the material table, while a fluid that flows when the cap rock is breached is a simulation subsystem that pulls underground rivers in with it. Both are legitimate; they are different projects. Nothing else in the design currently assumes either, so this can wait.
Locked D11 — Generation is a staged pipeline of steps and generators
The generator is an ordered list of steps, and each step holds one or more generators that run concurrently within it. Steps are separated by barriers; a step reads the world state every earlier step produced.
Flexible, but curated. New generation systems are added deliberately — which step they join, and in what order the steps run, is a decision each time rather than an open plugin surface.
The immediate reason is D10: mineral seams occur only within specific host rock, and caves and rivers cut through finished strata, so those passes must read earlier output rather than being independent functions of position. The larger reason is that the same mechanism carries everything that comes later — roads and railways, cities, points of interest — without redesigning the generator each time.
| Step | Generators | Writes | Status |
|---|---|---|---|
| 1 | Base density, strata assignment | Density, material | Phase 1 |
| 2 | Cave generation | Density | Phase 1 |
| 3 | Mineral seams | Material | Phase 1 |
| 4 | Top infrastructure — roads, railways, cities, POIs | Density and material | Anticipated, not scheduled |
| Last − 1 | Bedrock clamp | Density | Terminal, always |
| Last | Terrain LOD generation, construction LOD generation | The LOD artifacts — disjoint, so safely concurrent | Terminal, always |
The contract that makes concurrency within a step safe
"Parallelisable within a step" is not a property of the step — it is a property of what its generators write. Two generators in the same step must either write disjoint fields or combine through a commutative operator. Otherwise the result depends on scheduling, which breaks the determinism D9 needs for a stored world.
Two density-subtracting carves commute if they are combined with min(); two
generators both assigning material to the same cell do not.
But safe to share a step is not a reason to share one
An earlier draft of this table put seams and caves together in step 2, on exactly that reasoning: seams write material (ore instead of granite, still solid) and caves write density (they carve), so they are disjoint and may run concurrently. They are, and they may. It is still the wrong arrangement, because the test for sharing a step is not whether two passes can run together — it is whether either needs to read the other's output.
A generator only ever reads what an earlier step produced. Putting caves beside seams therefore denies caves any read of the material the strata assign, and conditioning caves on host rock is precisely what D10 describes when it has caves cutting through specific rock types. Separated and ordered strata → caves → seams, every pass reads what it needs: caves may condition on the material, and seams read both the material and the carved density, so ore is placed against the rock that is actually there rather than against rock a later pass removes.
And the concurrency given up is worth very little. Chunks already run concurrently within a generator, and there are far more chunks than workers, so the pool is saturated inside a single step regardless. All an extra barrier costs is one short tail as a step drains. Generator-level concurrency only pays when a step's generators are each too small to fill the pool alone — which is why the two terminal LOD generators still share a step, and why the rule above is worth keeping even though little now depends on it.
Top infrastructure is a step of its own for the stronger reason: a road and a cave both write density, so a cave passing under a road has no defined winner if they run together. Given its own later step, infrastructure can instead read what is beneath it and respond — bridge the chasm, fill the void, or route around it. That is exactly the dependency step ordering exists to express.
The last two steps are terminal and cannot be scheduled around
- The bedrock clamp. "After the carving stages" stops being sufficient once new steps can be added — railway tunnels and city foundations carve too. If anything can run after the clamp, the "cannot dig through the world" guarantee is only as good as whoever registered last.
- LOD generation. It consumes the finished world, so anything added after it is absent at distance — including the terrain modifications infrastructure makes, such as road cuts and flattened city ground.
And these two are ordered relative to each other, which is why they are separate steps rather than one. Terrain LOD reads the density the bedrock clamp writes, so they do not commute and cannot share a step under the rule above. The two LOD generators do commute — they write separate artifacts — so they share the final step quite happily.
Terrain LOD and construction LOD are separate outputs
The pre-generated terrain LOD is a downsample of the terrain field only. Construction sitting on top of it is not meshed into it — a coarse voxel field is the wrong representation for a building.
Instead, construction gets its own simplified LOD models, generated separately by the terminal step and stored alongside the terrain LOD. Generated structures — cities, POIs, the infrastructure step's output — therefore remain visible at distance rather than popping in when the near field reaches them, which matters because a city's silhouette is one of the reference's most recognisable features.
The same mechanism should eventually regenerate LOD models for player-built structures, so a base the player raises is visible from a distance too. How that works is its own discussion — it means producing simplified models at runtime rather than at world-build time, on content that changes. Until then, player-built construction is culled at a horizon and only generated construction has a distant representation.
What the structure imposes
- Parallelism is per-step, twice over. Chunks run concurrently within a generator, and generators run concurrently within a step — but a barrier separates steps, since a seam or a road originating in one chunk may reach into another.
- Every generator must be deterministic given the seed and the prior state, since the pipeline's output is what D9 stores and distributes.
- Generators write band-limited density, never binary. Recorded here because it is a property of the generator contract rather than of any one generator — see the note below.
The iteration payoff: cache each stage's output and re-run from the first changed stage. Under D9, tuning the generator means regenerating; if only the ore pass changed, strata should not be paid for again. This is the main thing that makes D9's up-front-generation cost tolerable in development, and it comes free with the staged structure provided stage outputs are storable.
Steps fix the order, and the disjointness rule above fixes concurrency. Neither fixes what a generator writes into the field — and that turns out to be load-bearing for D2.
A cave generator can carve as if (caveNoise > t) density = 0: the wall
goes 255 → 0 between adjacent cells, Marching Cubes has nothing to
interpolate, and the cave renders as cubes. Or it can subtract a smooth amount so
density ramps across the wall over 2–4 voxels, and the cave renders smooth. Every
generator faces that choice independently, and D10 puts caves, rivers and ore pockets
throughout the world.
So it is a contract on the generator interface: generators write smoothly varying density, never binary. Stated once for all generators rather than remembered per generator, and enforced by a single max-Δdensity test at the pipeline level — sample 6-adjacent cell pairs across a generated world and assert the difference stays within a bound.
Why the test matters more than the rule: on the cube renderer a
binary field and a smooth one render identically, because
isVoxelSolid thresholds at 128. So phase 1's own acceptance test — fly around
and find caves — cannot see a violation, and nothing would surface it until Marching Cubes
lands two phases later.
The finding: damage and density are the same thing for terrain
D5 and D6 together produce a simplification worth calling out, because it changes the data model rather than just the rendering.
Today there is one DamageMap — a sparse map from world cell to accumulated
damage, deliberately kept out of the dense voxel array because almost no cell is damaged at
once. Under D5, terrain damage is not a separate quantity: it is the density byte
that already exists on every voxel. A struck terrain cell has its density reduced, and that
single value drives the surface, the collision solidity, the material blend and the
persistence.
| Terrain | Construction | |
|---|---|---|
| Damage is stored in | The dense grid, as Voxel::density | A sparse map, as today |
| Damage is expressed as | Geometry — the surface shrinks | Texture — crack stages |
| A strike triggers | A remesh of the chunk | No remesh — shader state only |
| Persistence | Free — density is already the saved state | Must be saved separately |
| Toughness controls | Density removed per strike | Crack stages per strike |
Three consequences follow, and all three are load-bearing:
- The DamageMap narrows to construction only. Its documented invariant — clear a cell's entry whenever its occupant changes — stops applying to terrain entirely, since there is no separate state to go stale.
- Terrain damage is automatically persistent. Density-as-damage means a half-mined hillside is already captured by the same bytes that describe the terrain — nothing extra to serialise. Under D9 the world is stored anyway, so a mined cell and a generated cell are indistinguishable to the save layer.
- Every terrain strike dirties a chunk. This is the cost side, and it is a
new risk that D5 creates: at swing rate, each hit triggers a Marching Cubes
rebuild of a 323 chunk plus a GPU upload, currently synchronous behind a full
waitIdle. See risk 2.
Density runs 0–255 and the isovalue is 128, so a cell stops counting as solid halfway down its range. Whether a tool should drive density 255→0, 255→128, or something else is a real question with a visible consequence — it decides whether a terrain voxel shrinks smoothly to nothing or shrinks partway and then pops. Covered as question 1.