Files
VoxelForge/fable-idea.md
T
Fr0zka 831ee2fbf7 docs: Q3 tick the stale PENDING BUILD markers + CODEMAP rows for the new symbols
Q3: fable-idea F20 phases 1/2 and F18, REVIEW_FINDINGS perf pass 2 and batch 3,
and ARCHITECTURE's biome full-param redesign were all still carrying
"CODE-COMPLETE, PENDING BUILD" markers dated 07-04/-06/-08. Jahni confirmed on
2026-07-26 that everything is built and working (AUDIT-2026-07.md §0), so the
documents were misreporting project state. Ticked with the date they were
ticked, not just the date they were built.

Deliberately NOT ticked: ARCHITECTURE's F6 master material graph. Its C++ half
is built, but the material graph itself is editor-side work that is genuinely
still open, and ticking it would recreate the problem this queue item fixes.

CODEMAP discipline for this batch: new §3.2b (the VoxelDensityOp contract), new
§3.12 (Private/Tests), the EVoxelTileClass move into §3.2, and
FChunkBiomeCache::Invalidate under the biome types row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:06:32 +02:00

268 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fable-idea.md — performance & feature ideas
*Fable 5, max-effort pass — 2026-06-09. Grounded in a read of the actual code (mesher, apply path, task launch, content manager), not generic advice. Companion to CODEMAP.md §8 — nothing here is implemented; it's a menu.*
*Note: the strict-whitelist `.gitignore` will ignore this file. Add `!fable-idea.md` if you want it tracked.*
---
## Part I — Performance
### 0. Measure before anything (half a session, directs everything else)
Add `TRACE_CPUPROFILER_EVENT_SCOPE` around the four stages of a chunk (density grid sample / MC loop / normals / RMC stream build+upload) plus a `stat VoxelForge` group: chunks pending, applies this frame, avg gen ms, verts/chunk. One Unreal Insights capture then tells us if we're density-bound (my bet) or upload-bound, and every item below gets a before/after number. Cheap insurance against optimizing the wrong thing.
### The verified cost model (what I found reading the code)
A LOD0 chunk today costs roughly:
| Stage | Cost | Source |
|---|---|---|
| Density grid | 33³ = 35,937 `GetDensityAt` | `GenerateMesh` pre-sample (already optimal shape) |
| **Vertex normals** | **+6 `GetDensityAt` per unique vertex** (~1236k more for 26k verts) | `ComputeGradientNormal` — central differences, *per vertex* |
| Heightfield redundancy | SurfaceWorld's XY-only terrain stack (~16 Perlin evals: warp 2 + relief 2 + continents 4 + detail 4 + mountains 4) recomputed **for all 33 Z samples of a column** | `GetSurfaceDensity` is pure per-call |
| Noise core | Scalar, **double-precision** `FMath::PerlinNoise3D(FVector)`, 4-octave loops | `FractalNoise3D`/`RidgedNoise3D` |
| Collision | **Cooked for every chunk at every LOD** | `UpdateSectionConfig(SectionKey, Config, /*bShouldCreateCollision=*/true)` |
| Apply | Unbounded `while (ProcessQueue.Dequeue(...))` drain — all finished chunks upload in one frame; `Enqueue(Result)` copies the whole MeshData; RMC StreamSet built on the **game thread** | `VoxelWorld.cpp:410`, `:602`, `ApplyMeshToChunk` |
| Components | `NewObject`+`Register` per load, `DestroyComponent` per unload (no pooling) | `LoadChunk`/`UnloadChunk` |
So: normals can cost as much as the entire density grid; a SurfaceWorld chunk does ~33× redundant heightfield work; and every distant LOD2 chunk pays Chaos tri-mesh cooking the player can never touch.
### Tier 1 — high win / low risk / small-medium effort (do these first)
> **STATUS 2026-07-05: Tier 1 COMPLETE.** T1.a ✅ (surface-column cache, now file-scope `GSurfColCache`);
> T1.b ✅ (grid-based normals — the old per-vertex `ComputeGradientNormal` trio is gone from the mesher);
> T1.c ✅ (`bShouldCreateCollision` = level 0 only, VoxelWorld.cpp ApplyMeshToTile); T1.d ✅ (v2
> `ClassifyTile`, trace-verified 44 % worker CPU); T1.e ✅ (`MaxMeshAppliesPerFrame`, default 4);
> T1.f ✅ (`BuildTileStreamSet` runs in the gen task; apply only uploads).
> **Per-tile generation cost is now in diminishing-returns territory — remaining perf lives in
> Tier 3, Transvoxel, and the streaming/crossing work (see voxelforge-lod-transition-cost memory).**
**T1.a — Per-column XY cache for heightfield work.** Split SurfaceWorld density into `f_XY` (everything up to `Terrain`, + WaterZ) and a trivial Z-combine. In the chunk task, compute a thread-local `(GridDim+margin)²` column grid once, then `GetDensityAt` reads it. Heightfield evals drop ~33× (36k → ~1.2k). Same pattern later for any archetype with an XY-only sub-field (biome map will be one). Keep it keyed like the existing per-chunk param cache; purity in world coords is preserved so it stays bit-identical and window-invariant.
**T1.b — Normals from the density grid, not 6 fresh samples per vertex.** Sample the grid with a 1-point margin ring — `(GridDim+2)³` = 42.9k at LOD0, +19% — then compute vertex normals by central differences *on the grid* (trilinear-interpolate the 8 cell-corner gradients at the vertex position). Net: ~4872k density calls → ~43k, normals become batchable, and chunk-border shading stays continuous because the margin uses the same pure world-coord samples a neighbor would. Trade-off: gradient resolution becomes `Step` instead of `GradientOffset` — slightly softer normals, smoother (good) at distance. Verify visually at LOD0.
**T1.c — Collision only where it matters.** `bShouldCreateCollision = (LOD == 0)`. Distant chunks are unreachable by definition (if the player got there, they'd be LOD0 — and the LOD reconciliation loop §8.10 guarantees a hot-swap on approach). Kills Chaos cooking + collision memory for the large majority of loaded chunks. Also check RMC's async-collision setting is on. **Likely the best win-per-line-changed in the whole document.**
**T1.d — Chunk classification: skip trivially solid/air chunks before sampling.** ✅ DONE 2026-07-05
(v2 — `UVoxelGenerator::ClassifyTile`, see ARCHITECTURE §8.10; a 2026-06-26 v1 with a global ceiling
bound was reverted for roof holes. Trigger: trace showed 84 % of GenerateMesh calls produced empty
tiles.) In a tall multi-strate world most chunks in the desired set are full bedrock or full sky. Conservative per-chunk test before the 33³ sample: for heightfield strates, min/max terrain over the footprint (free from T1.a's column grid ± noise amplitude bound) vs the chunk's Z range; for cave strates, "no room/tunnel/passage/spine/seal/diff-layer bounds intersect" (all bounding data already exists). Fully-solid/air ⇒ empty MeshData, no component, done. Cuts whole chunks, not percentages — compounds with everything else.
**T1.e — Bound the apply side.** The submit loop is budgeted; the drain loop isn't. Cap mesh applies per frame (~24, generous for carves), keep draining *results* into a pending-apply list sorted by player distance. Also `ProcessQueue.Enqueue(MoveTemp(Result))` — currently the whole vertex/index payload is copied. Smooths the burst hitch (the "upload spikes" already observed).
**T1.f — Build the RMC StreamSet inside the worker task.** `FRealtimeMeshStreamSet` is plain data — the per-vertex Builder loop in `ApplyMeshToChunk` can run in the chunk task; the game thread then only does `CreateSectionGroup(MoveTemp(Streams))` + config. Removes a few ms of game-thread work per applied chunk; pairs with T1.e.
### Tier 2 — multiplicative, more effort
> **STATUS 2026-07-04:** T2.a ✅ DONE (float SSE `VoxelNoise.h` core — see ARCHITECTURE §8.10);
> T2.b ✅ DONE (opt-in `LODOctaveDrop`, default 0); T2.c ✅ DONE (`TileComponentPool`);
> T2.d ✅ DONE (BackgroundNormal priority had already shipped; core-clamp via
> `GetMaxConcurrentTasks`); T2.e ✅ DONE (per-chunk passage shortlist). **Tier 2 complete.**
**T2.a — Float + SIMD noise core (the big multiplier).** Everything funnels into scalar double-precision `FMath::PerlinNoise3D`. Two routes: an **ISPC kernel** (UBT compiles `.ispc` natively — zero third-party deps; used by Chaos/Niagara) or **FastNoise2** (MIT, runtime SIMD dispatch). Evaluate fractal/ridged noise over the whole flat grid / column grid in one batch call per field. Realistic 410× on the noise-bound part, on top of T1.
⚠️ Two correctness notes: (1) world changes for existing seeds — do this *before* content lock-in, bump a generator-version constant; (2) seed offsets like `SeedF * 7.3f` can reach 1e8+ where float precision is ~64 units — hash the seed into a bounded offset range (e.g. [0, 16k]) in the float core or noise quantizes.
**T2.b — LOD-aware octave count.** At Step=4, octaves with wavelength < the cell size are pure aliasing cost. `EffectiveOctaves = Octaves - LODBias(Step)` per field (keep the *low* octaves identical so the coarse shape matches). 3050% off distant chunks; iso-surface shifts by sub-cell amounts that Transvoxel/skirts have to stitch anyway. Cheap, do together with T2.a.
**T2.c — Component pooling + no components for empty chunks.** Recycle `URealtimeMeshComponent`s through a free list on unload instead of `DestroyComponent`/`NewObject`/`Register` churn (T1.d already stops creating them for empty chunks). Reduces GC pressure and register/unregister hitches during fast travel.
**T2.d — Task priority + worker count.** `UE::Tasks::Launch(..., LowLevelTasks::ETaskPriority::BackgroundNormal)` so a 16-task gen burst can't starve game/render workers; consider `MaxConcurrentTasks = Clamp(NumberOfCores - 2, 2, 16)` instead of a flat 16 on smaller CPUs.
**T2.e — Per-chunk passage gather.** `EvaluateModifierSDF` sphere-tests *every* passage per voxel. Gather the passages whose bounds intersect the current chunk once into the thread-local chunk cache; the per-voxel loop walks that short (usually empty) list. Matters as passage counts grow with deeper worlds.
### Tier 3 — when carving becomes the moment-to-moment verb
**T3.a — LRU base-density grid cache for carve re-mesh.** Keep the LOD0 density grid for the ~3264 most recently carved/near-player chunks (~144 KB each ⇒ < 10 MB). A carve then re-meshes as *cached grid + diff + MC* — no noise at all. Dig feedback becomes effectively instant, which is exactly where the game's feel lives.
**T3.b — Streaming feel:** frustum-weighted priority bonus in the submit sort (load what the player looks at first), and the pre-load/unload hysteresis ring already discussed (kill leading-edge pop and boundary churn).
### Explicitly NOT now (and why)
- **GPU density/meshing** — 100× throughput on paper, but a rewrite: readback latency, CPU collision still needed, float determinism across GPUs. North star only if T1+T2 ever hit a wall; they won't for this scope.
- **Octree/adaptive within-chunk structures** — T1.d gets the win at chunk granularity for ~5% of the complexity.
- **CHUNK_SIZE change, Nanite, greedy meshing** — lever already documented (§8.10) / no runtime procedural Nanite / MC isn't blocky.
**Expected compound for a SurfaceWorld chunk: T1.a × T1.b × T2.a ≈ order-of-magnitude on generation; T1.c/d/e attack frame-time and chunk count independently.**
---
## Part II — Features
### A. Tooling first — force multipliers for everything after
**F1 — 2D world-preview editor tool.** ★ my top pick. An editor utility that samples `f_XY` (terrain height, relief M, water mask — later the biome map) over an N×N window into a `UTexture2D`, with the Surface|Macro knobs live. Tuning ReliefStrength/biome layout becomes seconds instead of regen-and-fly. Directly addresses the standing "hard to dial blind" pain (Worm passages, today's Stage 0 knobs). Extend with a top-view passage-path overlay (the data exists in `bDebugDrawPassages`).
**F2 — Determinism validator button.** ✅ DONE 2026-07-04 (`AVoxelWorld::ValidateDeterminism`,
Live Edit category). CallInEditor: sample a band of densities from two different chunk-window alignments, diff, report max delta. Turns the scariest invariant (§8.4 — window invariance) into a one-click regression test *before* biome code starts landing.
**F3 — `stat VoxelForge` + Insights scopes.** Same as Perf §0 — listed here because it's also the tool that tells us when a feature regressed something.
### B. The "this becomes a game" features
**F4 — Save/load.** The diff layer is the player's entire footprint and it currently dies with the session. Serialize: seed + settings hash + generator version + per-chunk modification lists (they're compact structs already). Versioning matters: stamp saves with a gen-version so a noise change (T2.a!) can refuse/migrate old saves instead of silently shifting terrain under bases.
**F5 — Biome system. ✅ DONE (warped-Voronoi + climate XY field, per-chunk resolve —
`ResolveBiomeSampleAt` / `FBiomeContext`, VoxelGenerator.cpp §biomes; deco borders follow the
Voronoi field).** Original sketch (kept for the cave-biome extension): Deterministic XY biome map = warped Voronoi/cellular cells (seeded, window-invariant by construction, same family as the relief map — and relief M should be an *input*: mountain biomes live where M is high). Resolution rules to protect §8.10: resolve **per chunk** (dominant biome + ≤2 neighbors + blend weights, stored in the thread-local chunk cache); per **voxel** blend only a handful of scalars (height offset, roughness, terrace, water tint index). Per biome: a *content profile* — decoration set, atmosphere/audio override, material palette index, water level offset. Archetype transitions stay Hard; biomes vary *within* SurfaceWorld first, cave-biomes (crystal/fungal/ice) reuse the identical pattern later.
**F6 — Material identity: vertex-data masks + triplanar palette material.** Geometry variety without *surface* variety still reads samey. At mesh time, pack per-vertex: slope (from the T1.b normal), relative height, biome/material index (from F5) into vertex color channels. One master material: triplanar rock/grass/sand/snow layers selected & blended by those masks + a macro-variation texture. This is the single biggest *visual* multiplier available and it's mostly material-graph work.
**F7 — POI / set-piece system.** Noise terrain everywhere = beautiful nowhere. Deterministic destinations: chunk-hash-placed stamps (composed SDF carves/fills + a decoration prefab + optional ambient actor), e.g. buried shrines, crystal gardens at passage mouths, ruins on mesas. Placement uses the same two-region COLLECT discipline as rooms (§8.4). Destinations are what turn wandering into stories ("found a shrine at 400 m").
**F8 — Ore veins / diggable resources.** The game's verb is digging; give digging a reward loop. A secondary material-id field (cheap 3D noise threshold, per-strate/biome tables with depth curves) evaluated **only at mesh vertices** (≈ free) → vertex color → material shows veins; on carve, query the field at the brush center → grant resource. No per-voxel density cost, fully deterministic.
**F9 — Audio/ambience manager.** The exact architectural twin of the atmosphere manager (player strate/biome → assets): ambient loop crossfade, cave reverb submix, dig impacts by surface type, a stinger + title card on first strate entry. Sound is half of cave atmosphere and this is days, not weeks.
**F17 — Generator surface-class tag (ceiling/ground/cave material the *right* way). ✅ DONE 2026-07-05** (per-vertex semantic class in the mesher — down-facing verts query a memoized `GetSurfaceHeightAt`, nearer CeilSurf ⇒ sky-cap — per-tri majority → two contiguous polygroup runs → RMC section per group, slot 1 = `CeilingMaterial`, per-section shadow. Trigger: the whole-tile normal vote painted mixed coarse tiles with one material. Cave-roof discrimination hook is in place: down-facing below TerrainZ ⇒ ground/rock. Remaining polish idea: fully sideways cap-fold tris (all 3 verts |N.Z|≤0.1) default to ground.) Original design note: ★ do this when caves land. Today `ApplyMeshToTile` picks one material per tile from a ceiling test — first a height-oracle sample (midpoint), now a worker-side **normal vote** over the tile's mesh normals (down-facing ⇒ `bIsCeiling``CeilingMaterial` + no shadow; gated to SurfaceWorld by one `GetSurfaceHeightAt` probe). That's a **stopgap that only works because down-facing == sky-cap *while no caves exist*.** The moment a mountain-biome cave uses the same density/mesh system, its roof is also down-facing and would wrongly get the sky-cap material — orientation can't tell a cave ceiling from a surface ceiling. **The discriminator is semantic, not geometric, and only the generator knows it:** a sky-cap surface is the `ComputeSurfaceCeiling` (`CeilSurf`) boundary; a cave ceiling is a 3D-noise **carve** below `TerrainZ`. Cheap test the generator already has the inputs for — at a down-facing surface vertex, compare world Z to the column's `TerrainZ`/`CeilSurf` (both from the surface-column cache the mesher already holds): near `CeilSurf` ⇒ sky-cap, below `TerrainZ` ⇒ cave. **Plan:** stamp a discrete *surface class* (ground / sky-cap / cave-ceiling / cave-wall…) per vertex/triangle **at mesh time** in the mesher → carry it as the **polygroup** (already enabled, `Builder.EnablePolyGroups()`, every tri currently group 0) → `ApplyMeshToTile` maps polygroup → material slot (slot 0 terrain, slot 1 sky-cap, slot 2 cave-rock, biome-specific via the F5 palette mask) and sets per-section shadow. Discrete "which material" → polygroup/slot; continuous masks (biome blend, slope — F6) stay in `Colors`. This **subsumes** the current ground/ceiling split (it falls out as a special case), fixes the coarse mixed-tile horizon artifact exactly (per-triangle, not per-tile dominant-wins), and is the only version that survives caves. Pairs naturally with F5/F6/F8 (all want generator-stamped per-vertex material identity). Cost: a per-tri classify in the mesher (cheap, has the cache) + multi-slot setup in the apply path (RMC supports it; confirm the v5 per-section `UpdateSectionConfig` / slot-per-polygroup calls + empty-polygroup = no draw). Until then: the normal vote is fine — it's commented as "no caves yet → down == cap."
**F18 — Far-field per-surface SHEETS (the render-distance ring, cheap). ✅ BUILT & WORKING 2026-07-06**
(marker ticked 2026-07-27). With `RenderDistanceChunks` the
outermost ring can reach many km — as MC tiles that's 600-1000 primitives paying per-frame visibility/VSM
forever, and each far tile runs full 3D marching cubes just to rediscover two heightfields. In an open
strate the far field IS two heightfields the generator already computes per column (`GetSurfaceHeightAt`:
TerrainZ + CeilSurf). So: the extended ring streams SHEET tiles instead (level `MaxClipLevel +
FarSheetSpanLevels`, so one sheet covers 4-16 MC-tile footprints) and the mesher builds each as two
regular displaced grids — ground sheet (polygroup 0) + sky-cap sheet (polygroup 1), tags true **by
construction** (no vote, no classify probes), same materials/UVs/biome color masks as MC, normals from
the height gradient, perimeter skirts per bucket. Gen ~3-6× cheaper per area than band-cut MC; primitives
~10-16× fewer. Caveats accepted: SurfaceWorld only (non-open strates produce empty sheets — their far
ring was invisible rock anyway; FloatingIslands has no far ring beyond MaxClipLevel), carved features
(passages/spine/chasms) don't show at sheet distance, sheets need the strate band armed (in the
inter-strate gap the far ring blanks until you land). Streaming: sheet ring in `BuildDesiredTiles`
(covered-check vs the MaxClipLevel box), `IsTileInClipRange` shares the same outer shell,
`LoadTile` routes `Level > MaxClipLevel` to `GenerateSheetMesh`. **Build-1 fix (XY hole):** a
partially-covered sheet rendered its *whole* footprint, overlaying the near LOD0-1 terrain with its
coarse sampling. So the MC-covered box around the player (level-MaxClipLevel box, shrunk 1 tile for a
seam-overlap ring) is cut from sheets at cell granularity (`AVoxelWorld::SheetHole*Vox``GenerateSheetMesh`
hole args; hole moves on a MaxClipLevel-tile crossing → overlapping sheets re-queue via `BandRemeshQueue`;
hole-edge cells emit no skirt).
**F19 — AI navigation & agents (function-based). PARKED (no NPCs yet); FOUNDATION BUILT 2026-07-07.**
Mobs need two things the world didn't give them: (1) to *exist* away from the local player, (2) to *route*.
- **(1) DONE — multi-anchor collision streaming + render-skip** (ARCHITECTURE §9.3/§9.4, built 2026-07-07):
`AVoxelWorld::RegisterStreamingAnchor(actor, CollisionOnly, thin box)` keeps a small box of level-0
collision tiles loaded around any actor; `CollisionOnly` tiles cook collision but are hidden (no draw/VSM)
unless the player clipmap also wants them. So an NPC/remote-player has ground to stand on / be hit on,
cheaply, anywhere. Same system serves MP (stream around every player) — [[voxelforge-multiplayer]] §9.
- **(2) TO BUILD — routing, function-based (NOT Recast).** The world is a cheap deterministic function, so
nav = a query, not a baked navmesh: coarse A* / flow-field over a grid sampling `GetSurfaceHeightAt`
(slope + water gated, + the diff layer so AI sees carves) → **funnel/string-pull****Catmull-Rom spline**
→ a **steering follow-component** for smooth, non-robotic, cheap locomotion (path once, re-path on a timer;
budget requests like the streaming loop). Needs **zero loaded geometry**, deterministic (same seed = same
path), and **digging costs it nothing** (no per-carve re-cook). Bridgeable to `AIController`/`CharacterMovement`.
- **Analytic surface-follow (the free tier):** a pure *surface walker* needs no collision AND no navmesh —
pin it to `GetVoxelSurfaceHeightAt` each tick. Reserve real collision (the anchor) for physics / caves /
carved terrain / being hit by player traces. Both coexist, decided per NPC type.
- **Recast rejected:** would need cooked collision everywhere AI roams, re-cooking on every dig — and it's
unusable on a future headless dedicated server (no meshes), where function nav is not just cheaper but
mandatory. AI is server-authoritative (§9.6). ★ Build when NPCs actually land.
**F20 — Biome-selected surface terrain ops (terrace / cliff / layer-lines / overhang / spike / hole). SPEC 2026-07-07.**
Terrain ops are cave-only today (per-room in `GetDensityWithParams`; `GetSurfaceDensity` applies NONE — that's
the "ops don't work on the surface" report). This brings them to the SURFACE, as a **biome** property,
**conditioned on local terrain** so they read geological instead of random. (Slots in ahead of the later
cave-system redo, which will add biome support cave-side reusing this same op→biome model.)
*Data:* add `TArray<FStrateTerrainOpEntry> SurfaceOps` to `UVoxelBiomeDefinition`, each entry gated by a
condition (Min/MaxSlopeAngle like `FStrateDecoration` already has, + optional relief/height band via the F7
`FTerrainCondition` set — add a `Slope` type). Resolved through the biome field (dominant biome per column —
already cached). Empty ⇒ early-out ⇒ **zero cost**, so the feature is free in every biome that doesn't use it.
*The cheapness architecture (the whole point — think per-COLUMN first, per-voxel only when forced):*
- **Two op classes.** HEIGHTFIELD ops modify the cached column → **~free**: **Terrace** (quantize TerrainZ into
steps), **Cliff** (sharpen the height transition where slope is high), **LayerLines/Ribbing** (a `sin(Z)`
groove in the near-surface band — no noise). VOLUMETRIC ops need genuine 3D near the surface → real but
bounded: **Overhang**, **Spike**, **Hole**.
- **Slope/relief conditioning is free AND is what makes them look right.** Slope = the surface height gradient
(2 extra cached-column samples, or reuse the mesh normal / F6 slope channel). Overhang strength ∝ slope ⇒
overhangs grow out of EXISTING cliffs, never poke out of flat ground (the "would it look weird" answer: no,
it's cliff-conditioned, not random). Terrace on moderate slopes; spikes where relief M is high (mountains).
Conditions cost ~0 (fields already computed) and double as the "not random" guarantee.
- **Volumetric work is banded + tile-skipped.** Overhang = low-freq 3D displacement only in a ±few-voxel band
around TerrainZ, only where slope-gated + the biome has it. Non-straddling tiles never pay (T1.d ClassifyTile).
Coarse/far tiles stay pure heightfield (LOD-cull the 3D ops — they're near-field detail; the sheet ring ignores
them entirely).
- **Spikes/holes = hash-placed SDF via a per-tile shortlist** (cone/capsule UP for spikes, shaft DOWN for holes;
placed on a hash lattice like landmarks, collected once per tile like rooms/passages; per-voxel tests a 0-3
shortlist). Biome+condition-gated so empty biomes collect nothing.
- **GOTCHA — the dominant cost, spikes/holes only:** a spike rises INTO otherwise-all-air tiles and a hole carves
INTO otherwise-all-solid tiles → they DEFEAT T1.d's trivial-skip for every tile they pass through (those must
now mesh). So ClassifyTile needs a spike/hole shortlist guard (cheap AABB, like `AnyPassageNearBox`), and a tall
spike "wakes up" the whole vertical stack of air tiles it crosses = more meshed tiles + collision. Overhang/
terrace do NOT do this (they stay in the already-meshed surface band). ⇒ keep spikes SHORT + SPARSE; they're the
priciest of the set in tiles-meshed + triangle/collision terms, not just density evals.
*Cost:* heightfield ops ≈ free. Overhang ≈ 1.5-2× noise on slope-gated surface tiles, ~0 elsewhere. Spike/hole ≈
that PLUS the woken tiles (the real cost — budget by count/height). All biome-gated ⇒ world-average cost ≈ 0;
you pay only near the player, in the biomes that opt in.
*Determinism/borders:* op strength blends by biome weight (like the surface height output-blend), conditioned ops
fade with slope ⇒ seamless at biome borders; placement hashes are pure `(seed, coord)` ⇒ window-invariant (§8.4).
*Build phasing:* **(1)** heightfield ops (terrace/cliff/layerlines) — cheap, high payoff, low risk, ships the
"ops finally work on the surface" win; **(2)** overhang (3D band, slope-conditioned); **(3)** spike/hole (placed +
shortlist + ClassifyTile guard) — most cost, do last.
**PHASE 1 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `AUDIT-2026-07.md §0`). Heightfield ops shipped: **Cliff** (slope-gated STEEPENING —
push height from the local mean where steep ⇒ sheer walls; the slope-conditioned one, hugs steep terrain;
v1 band-snap was too subtle, reformulated to steepening after Jahni's "doesn't change much"), **Terrace** (relief-gated plateau quantize, now with
`TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed). *Design
deviation from the spec above, deliberate:* instead of a `SurfaceOps` array of `FStrateTerrainOpEntry` +
`FTerrainCondition` gating on `UVoxelBiomeDefinition`, phase-1 ops are **direct fields on
`FSurfaceGenerationParams`** (the `Surface|Ops` category). Rationale: that struct is ALREADY the per-biome
surface-shape carrier (`B->SurfaceParams` when `bOverrideTerrain`) and is already biome-resolved +
border-blended by `ResolveSurfaceChunkParams`/`ComputeSurfaceColumn` (the height output-lerp) — so biome
selection AND seamless border blending come for FREE with zero new resolution path, and the conditioning
(relief `M`, analytic slope) is intrinsic to each op. Cost: a biome must set `bOverrideTerrain` to carry its own
ops (fine — biome-differentiated terrain already implies that), and a strate can also carry ops with no biomes at
all (more flexible than biome-only). All fields default OFF ⇒ current world byte-identical. Applied in the single
height oracle `ComputeSurfaceTerrainZ` (new `SampleSurfaceStructuralZ` helper = pre-op raw height, re-sampled at
an XY offset for Cliff's slope) so MC/sheets/ClassifyTile/deco/BP-bridge all agree, no T1.d interference. Revisit
the array+condition model for **phase 2 (overhangs)** where per-entry slope-gating earns its keep.
**PHASE 2 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `AUDIT-2026-07.md §0`). Overhang (first VOLUMETRIC op) as
`FSurfaceGenerationParams` fields (`OverhangStrength/Reach/Height/Frequency/ZScale/SlopeThreshold`, default
off). **Design NOTE — v1 additive-noise-band was WRONG (Jahni: "does nothing" + sketch of a real cliff lip):
band-additive noise can only bump the surface where it already is, never make rock jut OUT over a void.**
Rewritten to a **warped-terrain UNION**: for air voxels in `(TerrainZ, TerrainZ+OverhangHeight]` above a
steep slope, re-sample the heightfield UPHILL by a reach that GROWS with height (tiny low ⇒ air over the
void; full high ⇒ borrows the far cliff rock) and `max()` it in ⇒ a shelf attached to the cliff, tapering
out over the void with air beneath (matches the sketch). Per-column `OverhangAmp`(=strength·slope-gate) +
unit uphill `(DirX,DirY)` resolved in `ComputeSurfaceColumn` — gradient sampled at the REACH scale so a
point over the void can SEE the cliff — cached on `FSurfaceColumn`. **ClassifyTile guard:**
`FSurfSlot::OverhangMargin`=max `OverhangHeight`; a column Z in `(TerrainZ, TerrainZ+margin]` ⇒ Mixed
(UPWARD only — the union only adds rock). Thin cliff-edge band woken, NOT far tiles. Genuine 3D per-voxel
structural re-eval (gated hard to steep overhang columns → localized to cliff edges; flagged as a real but
bounded cost). KNOWN v1 LIMITS (flagged): applies at ALL LODs (may alias far — gate to fine later); shelf
sits at ~`OverhangHeight` above the ground below it, not necessarily at the cliff TOP (raise Height for
taller); terrain-overriding biomes use strate params for the warped sample (minor seam). NEXT = **phase 3
spike/hole** (hash-placed SDF + per-tile shortlist + the real T1.d wake-guard — keep SHORT+SPARSE).
### C. Experience polish (cheap, high feel-per-effort)
- **F10 — Swimmable water:** physics volume + underwater post-process tied to the existing water-chunk regions (the plane is visual-only today). Buoyancy later.
- **F11 — Depth & place HUD:** depth meter, strate name title cards (the atmosphere manager already detects strate change), simple explored-chunks map.
- **F12 — Day/night + weather on strate 0 only** — surface gets a sky lifecycle; underground untouched (free scoping).
- **F13 — Carve UX:** runtime brush ghost preview, tool tiers (radius/speed), material-aware dig speed (bedrock slow), rockfall-dust juice on carve.
- **F14 — The (0,0) spine as gameplay:** buildable lift/teleport anchors per strate — descent is the game, but re-ascent shouldn't be the chore. BP prototype on the existing carve/actor APIs.
- **F15 — Ambient life:** Niagara bats/fireflies/fish schools per biome profile (no AI, pure atmosphere). Real mobs = **F19** (nav strategy now decided: function-based).
- **F16 — Decorations as HISM: ✅ DONE** (region-granular HISMs, two-grid Near/Far streaming, Static mobility). Original note: scatter currently `SpawnActor`s every prop — actors tick, register, and pile up fast. Pure props should be `UHierarchicalInstancedStaticMeshComponent` instances (per mesh type, per chunk); keep actors only for lit/interactable things. This is also a perf item wearing a feature hat.
### D. Deliberately deferred
Multiplayer — **NO LONGER just "deferred": it's the confirmed direction (listen-server first), design + first foundation IN.** Full model in ARCHITECTURE §9 ([[voxelforge-multiplayer]]): determinism = replicate seed+layout+diff events, never geometry; server-authoritative diff; multi-anchor streaming + §9.4 render-skip BUILT 2026-07-07. Remaining netcode (§9.7): seed replication at join, `Server_RequestModification` RPC, late-join diff snapshot. Diff records stay compact/replicatable-shaped (they already are). — GPU generation, mod/scripting API, Nanite: all real, none load-bearing for the current vision.
---
## Suggested order (if it were mine to pick)
1. **Perf 0 + T1.c + T1.a + T1.b** — one focused session: measurement, the one-line collision win, the two big density cuts.
2. **T1.e + T1.f + F16** — smooth the game thread (apply budget, worker-side streams, HISM props).
3. **F1 preview tool + F2 validator** — before biome work starts, build the instruments.
4. **F5 biomes + F6 materials** — the look of the game. (F9 audio rides along cheaply.)
5. **F4 save/load** — the moment it feels like a game, players will want to keep one.
6. **F7 POIs + F8 ores** — destinations and rewards.
7. **T2.a SIMD noise** — after content direction settles (it changes seeds), before world-size ambitions grow.
8. **Transvoxel** (already chosen) whenever LOD cracks become the loudest remaining flaw.