3. Target architecture

What the decisions add up to: one grid, two representations, two meshers, two damage models, two pipelines — and a subsystem-by-subsystem delta from the code today.

The shape of it

                          VoxelWorld  (hash map of chunks, streamed)
                       per cell: type | material | density | class
                                        │
                 ┌──────────────────────┴──────────────────────┐
                 │                                             │
          class == Terrain                            class == Construction
                 │                                             │
       density field, 0..255                        discrete, always full
                 │                                             │
        ┌────────┴────────┐                           ┌────────┴────────┐
        │  MarchingCubes  │                           │   CubeMesher    │
        │     Mesher      │                           │   (+ greedy)    │
        └────────┬────────┘                           └────────┬────────┘
                 │  construction reads as                      │
                 │  full density → surface                     │
                 │  closes against walls                       │
                 │                                             │
        ┌────────┴────────┐                           ┌────────┴────────┐
        │ terrain pipeline│                           │ cube pipeline   │
        │ triplanar+splat │                           │ per-face UV +   │
        │ gradient normals│                           │ crack overlay   │
        └─────────────────┘                           └─────────────────┘
                 │                                             │
                 └──────────── interpenetrate ─────────────────┘
                              (no shared boundary)

Two damage models

The split from D5 / D6 reaches further than rendering — it decides where the state lives and what a strike costs.

Terrain: damage is density

  1. A strike subtracts from Voxel::density in the dense chunk array.
  2. The chunk goes dirty; Marching Cubes rebuilds it; the new surface is uploaded.
  3. The surface has visibly receded around the struck cell — because MC interpolates between neighbouring densities, lowering one cell pulls the surface inward and dimples its neighbourhood rather than cutting a cube-shaped bite.
  4. When density falls past the threshold, the cell is empty and the dimple becomes a hole.

No separate damage record exists. Persistence, collision solidity, the material blend and the visible shape are all driven by the one byte.

Construction: damage is a shader lookup

  1. A strike increments the sparse DamageMap entry for that cell.
  2. Nothing is remeshed. The geometry is unchanged by definition.
  3. Per chunk, a small damage buffer or texture — indexed by cell coordinate — is updated and read by the fragment shader, which blends a crack overlay from a stage texture array over the base tile.
  4. At full damage the cell is cleared, which does dirty the chunk — once, at the end, instead of on every hit.
Design constraint

The crack overlay must not be baked into vertex data. Baking it would make every hammer hit a remesh, which is precisely the cost D6 avoids by not changing shape. A per-chunk damage lookup read in the fragment shader keeps construction damage at zero meshing cost. Worth designing in from the start — retrofitting it means changing the vertex format.

Why chunks at all?

A reasonable question, given how much of the design spends effort on chunk seams: why not mesh all visible terrain as one buffer and have no seams to reconcile?

Because rebuild granularity is set by edit granularity, not by render granularity. Under D5 every strike changes one density byte, and the cost of that depends entirely on how much geometry has to be rebuilt in response. For a 12-chunk radius with bounded Y = 384:

One visible-world meshOne chunk
Cells to march~590k columns × depth323 = 32,768
Vertices~1M~2,000
Rebuild + reupload~56 MB~110 KB

Roughly 500× the work per swing. Patching sub-ranges of the big buffer instead requires knowing which byte ranges changed — which is a spatial partition of the buffer, i.e. chunking again, implicit and harder to reason about. Four further reasons, in rough order of severity: streaming (chunks enter and leave constantly while walking, each a full rebuild), frustum culling (one mesh is one draw and cannot be culled; per-chunk culling typically discards ~70% at that radius), parallelism (chunks are the unit of work for the job queue — one mesh is one job on one thread), and LOD (per-region resolution is its definition).

Against that, the seam cost is small: one ownership convention — a chunk owns the MC cubes whose minimum corner lies inside it — and one rule — evaluate each edge crossing from its lower-coordinate endpoint. Both are verified by the exhaustive table test.

What you can have instead: a suballocated arena

The instinct behind the question has a real answer. Separate the two concerns that "one mesh" conflates:

Place every chunk's mesh into one large GPU vertex/index buffer as sub-ranges and draw them with multi-draw-indirect: one buffer binding and effectively one submission for all visible terrain, while still rebuilding only the ~110 KB that changed. Strictly better than a monolithic mesh — the same render efficiency without the update cost.

Today's code is one buffer and one draw per chunk (ChunkRender in Engine.cpp), which is the simple version. The arena is a later optimisation, not a phase-3 requirement. One wrinkle if it is taken: chunks carry a per-chunk model matrix today, and batched drawing needs that offset per-draw instead — a push constant or an instance buffer. Keep positions chunk-local rather than baking world space: the world is bounded now (D7), but its size is a parameter, and float precision still degrades with distance from the origin — a centre-anchored multi-kilometre world puts geometry several thousand units out in every direction.

And the chunk size is a dial

323 is a default, not a law: bigger chunks mean fewer seams, fewer draws and a better surface-to-volume ratio, but rebuild cost scales with the cube of the side — 643 is 8× the rebuild per edit. Minecraft uses 163 sections; 7 Days to Die uses 16×16 columns with sub-chunks. If per-edit cost ever dominates, the lever is sub-chunk dirty regions rather than smaller chunks: keep the draw-side benefits of a large chunk and shrink only the rebuild unit.

Subsystem delta

SubsystemTodayTargetDelta
Generation Sine heightmap, 3 depth bands Seeded 3D field: fBm + domain warping, cave noise, strata, deterministic per chunk Rewrite
World storage Dense vector, fixed extent Hash map keyed by chunk coordinate, streamed around the player Rewrite (interface already designed for it)
Terrain mesher Marching Cubes behind IMesher, gradient normals, welded vertices New
Cube mesher Naive face culling Face culling + ambient occlusion + greedy merging Extend
Meshing execution Synchronous full-grid sweep behind waitIdle Worker threads, per-chunk job queue, bounded uploads per frame Rewrite
Pipelines One (construction) Two — construction cubes, terrain triplanar Restore the second
Terrain texturing Triplanar projection, per-vertex material weights, splat blending New (was deleted)
Construction texturing Per-face UV + array layer Unchanged, plus a crack overlay sampled from a per-chunk damage lookup Additive
Damage One sparse map for all voxels Terrain → density in the grid; construction → sparse map, no remesh Split & narrow
Edit ops removeVoxel / strikeVoxel, class-agnostic Class-dispatched: terrain reduces density, construction increments damage Branch on VoxelClass
Picking DDA + trilinear crossing, both normals already present Unchanged in mechanism; needs a highlight convention per class Mostly intact
Collision AABB swept against grid cells, snapped to grid lines Unchanged through phase 4; then the same cell enumeration as a broadphase plus a triangle narrowphase over candidate cells Staged — contained in VoxelCollision.h
Distant terrain A pre-generated static coarse mesh loaded from the world artifact, drawn behind the streamed near field; horizon culling for cubes New, later phase
Structural integrity Support propagation over both representations New, deferred

Things that stay exactly as they are

Worth naming, because they are the parts of the existing design that were built for this and do not need to move:

Where the two representations touch

Three interactions, all of which have an answer that avoids the trap from Epic #24:

InteractionResolution
Terrain surface adjacent to a construction cube Terrain marches with the cube reading as full density; the cube is drawn opaquely on top; they interpenetrate. No sealing, no snapping.
Placing a construction block into terrain Open — does the block displace the terrain density in that cell, or coexist with it? See question 4.
Picking a cell that could be either DDA finds the cell; VoxelClass dispatches the tool, the highlight style and the damage model. The mechanism already exists.