45 KiB
VoxelForge — Code Map & Knowledge Index
Purpose: a navigation index so anyone (human or AI) can locate and modify code without re-reading the whole plugin. Anchors are
File:line— line numbers drift as code is edited, so trust the symbol name first and the line as a hint.Plugin root:
Source/VoxelForge/· ~8,300 lines of C++ across 25 files. Comments in the code are mixed French + English. UE module =VoxelForge(Runtime).
1. What this plugin is
A density-field voxel terrain plugin for Unreal Engine, built around underground "strates" (vertical geological layers, each a self-contained mini-world).
- No block grid. Terrain is a continuous scalar density field evaluated on the fly from world coordinates. Convention: negative = solid rock, positive = air (the Marching Cubes convention used throughout).
- Marching Cubes turns the density field into a smooth mesh per 32³ chunk.
- Strates stack downward from Z=0. Each strate is a
UVoxelStrateDefinitiondata asset that picks a generator type and a huge bag of cave-shaping params. - Async streaming: chunks load/unload around the player on background tasks; meshes are applied on the game thread under a per-frame budget.
- Player edits (carve/fill) are stored as a diff layer added on top of the procedural density — procedural generation stays deterministic.
Core terminology
| Term | Meaning |
|---|---|
| Chunk | 32×32×32 voxels (CHUNK_SIZE). Voxel = 25 cm (VOXEL_SIZE). |
| Density | Scalar field. < 0 solid, > 0 air, 0 = surface (IsoLevel). |
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
| Generator type | Archetype per strate: TunnelNetwork, FlatPlain, CrystalChamber, Maze, SurfaceWorld, VerticalShafts, FloatingIslands, Underwater. See §8. |
| (0,0) spine | Guaranteed open landing column at world XY (0,0) in every strate; descent is player-dug through the seals. See §8. |
| Terrain op | Optional density modifier (pit, arch, terrace…) attached to a strate. |
| Passage | Carved tunnel connecting two adjacent strates (progression path). |
| Diff layer | Player carve/fill modifications stored on top of procedural density. |
| Epoch | Generation counter; stale async results are discarded on mismatch. |
2. The big picture — data flow
AVoxelWorld (actor, orchestrator)
Tick → UpdateChunksAroundPosition
│ (load/unload around player, by distance + LOD)
▼
LoadChunk → UE::Tasks::Launch ──────────► background thread
│
UVoxelMarchingCubesMesher::GenerateMesh(chunk, step) ◄────────────────┘
│ samples density per cell corner
▼
UVoxelGenerator::GetDensityAt(x,y,z) ← THE density entry point
│ asks StrateManager which strate/params/generator-type applies
├─ TunnelNetwork → GetDensityWithParams() (rooms+tunnels+ops+worms+seal+passages)
│ └─ VoxelCaveMorphology::BuildChunkCache + EvaluateSDFCached (room/tunnel SDF)
├─ FlatPlain / CrystalChamber → GetSlabDensity() (floor+ceiling+columns+seal+passages)
└─ + UVoxelDiffLayer::GetDensityOffset() (player carve/fill)
│
▼ FVoxelMeshData (verts/tris/uvs/normals)
ProcessQueue (lock-free) ──► game thread: ProcessPendingChunks → ApplyMeshToChunk
(RealtimeMeshComponent)
UVoxelStrateManager is the side oracle: "what strate is at this Z, what params,
what generator type, and is there a passage/elevator SDF near here?"
3. File-by-file reference
Paths relative to Source/VoxelForge/. Public/ = headers, Private/ = impl.
3.1 Module & build
| File | Role |
|---|---|
../../VoxelForge.uplugin |
Plugin manifest. One Runtime module VoxelForge. Beta. |
VoxelForge.Build.cs |
Deps: Core, CoreUObject, Engine, GameplayTags, RealtimeMeshComponent. |
Public/VoxelForgeModule.h / Private/VoxelForgeModule.cpp |
FVoxelForgeModule boilerplate (Startup/Shutdown just log). |
3.2 Foundational types — Public/VoxelTypes.h (no UClass, everyone includes it)
| Symbol | Line | Notes |
|---|---|---|
CHUNK_SIZE (32), CHUNK_SIZE_SQUARED, CHUNK_VOLUME |
19-21 | Chunk dimensions. (64³ tried for fewer draws → reverted: streaming too bursty. fps is fixed render-side instead.) |
VOXEL_SIZE (25.0f cm) |
23 | World scale. |
EVoxelFace enum + GetFaceDirection / GetFaceNormal |
33-61 | 6 cube faces. |
WorldToChunkCoord / WorldToLocalCoord / ChunkToWorldPos |
74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
LocalToIndex / IndexToLocal / IsValidLocalCoord |
107-131 | Flat-array 3D↔1D indexing. |
SmoothStep01 |
140 | 3x²-2x³ — used everywhere for blends. |
VOXEL_NOISE_SCALE (1.25f) |
147 | Rescales UE PerlinNoise3D to ~[-1,1]. |
FVoxelMeshData struct |
157-173 | Mesher output (Vertices/Triangles/UVs/Normals). Plain C++, not USTRUCT. |
3.3 Chunk identity — Public/VoxelChunk.h
FVoxelChunk (USTRUCT, line 19): just a ChunkCoord + GetWorldPosition(). In a
density-only world the chunk stores no voxels — it's a coord wrapper. Room to cache
per-chunk info later.
3.4 Settings — Public/VoxelSettings.h
UVoxelSettings : UPrimaryDataAsset — the single tuning asset assigned on AVoxelWorld.
| Group | Fields (line) |
|---|---|
| Streaming | ViewDistanceXY=16, ViewDistanceUp/Down=5, MaxConcurrentTasks=16, MaxMeshAppliesPerFrame=4 (defaults — actual values live on the data asset) |
| LOD | LOD0Distance=4, LOD1Distance=8 |
| Rendering | VoxelMaterial (61) |
| Strates | Seed (69), CurrentSeason=1 (73), StratePool (78), FixedStrates map (83), TotalStrates=10 (87) |
| Carving budget | MaxModifications=0 (97), MaxBrushRadius=15 (102), MaxTotalVolume=0 (107). 0 = unlimited. |
3.5 World orchestrator — Public/VoxelWorld.h + Private/VoxelWorld.cpp
AVoxelWorld : AActor — owns everything, drives streaming. Also FChunkResult struct
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, Epoch).
Owned objects (UPROPERTY): Settings, Generator, Mesher, StrateManager,
DiffLayer (VoxelWorld.h:55-78). Storage: Chunks map, ChunkMeshes map,
ChunkLODs, ProcessQueue (TQueue), PendingChunkCoord (TSet) (VoxelWorld.h:85-312).
Async state: bShuttingDown, ActiveTaskCount (atomics), GenerationEpoch.
| Method | .cpp line | Role |
|---|---|---|
AVoxelWorld() ctor |
12 | Enables Tick. |
RegenerateAllChunks() |
21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
PostEditChangeProperty |
45 | Editor live-edit hook. |
OnObjectModifiedInEditor |
58 | Regenerates when a strate asset is edited (if bLiveEditStrates). |
EndPlay |
140 | Sets bShuttingDown, waits for ActiveTaskCount→0, unbinds delegate. |
BeginPlay |
177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
Tick |
220 | UpdateChunksAroundPosition(player) + ProcessPendingChunks(). |
GetPlayerPosition |
231 | Pawn position or zero. |
GetLODForChunk / LODToStep |
242 / 268 | Distance→LOD (0/1/2) → step (1/2/4). |
IsChunkInRange |
275 | View-distance test. |
ProcessPendingChunks |
301 | Drains ProcessQueue under per-frame budget; discards stale epochs; applies meshes. |
UpdateChunksAroundPosition |
362 | Builds desired set, sorts by distance, loads/unloads, handles LOD changes. |
LoadChunk |
445 | Budget check → UE::Tasks::Launch background gen+mesh; RAII task guard. |
UnloadChunk |
493 | Destroys mesh component + map entries. |
ApplyMeshToChunk |
503 | Get/create RealtimeMeshComponent, upload geometry, assign material. |
GetStrateAtPosition |
679 | Gameplay query → strate index. |
CarveAtPosition / FillAtPosition |
691 / 709 | Build FVoxelModification → DiffLayer → RemeshDirtyChunks. |
ClearAllModifications |
726 | Clears diff layer, regenerates. |
ChangeSeed |
740 | Season reset: new seed everywhere, clear diffs, bump season, reload. |
GetCurrentSeed / GetCurrentSeason |
784 / 789 | Accessors. |
RemeshDirtyChunks |
798 | Re-queue loaded chunks for async re-mesh (no visual pop). |
3.6 Density generator — Public/VoxelGenerator.h + Private/VoxelGenerator.cpp
UVoxelGenerator : UObject — lightweight; holds Seed, and injected services
StrateManager + DiffLayer (both nullable). This is where terrain shape lives.
| Symbol | .cpp line | Role |
|---|---|---|
FractalNoise3D (static) |
25 | fBM (layered Perlin). |
RidgedNoise3D (static) |
55 | Ridged multifractal — craggy. |
CellularNoise3D (static) |
101 | Worley/cellular — grotto/scallop. |
ApplyBoundarySeal (static) |
170 | Solidifies strate top/bottom shells. |
ApplyPassageCarving (static) |
197 | Punches passages/elevator through the seal. |
InitializeSettings |
211 | Copies seed from settings. |
GetDensityAt |
218 | Entry point. Picks strate + generator type, dispatches, adds diff offset. |
GetDensityWithParams |
277 | TunnelNetwork pipeline (~1000 lines). See §4. |
GetSlabDensity |
1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
ComputeSurfaceTerrainZ / GetSurfaceDensity |
— | SurfaceWorld heightfield → terrain Z, then density; biome output-blend lerps dominant/neighbour heights (ParamsD/ParamsN/weight). §8.14. |
SampleRelief / SampleMoisture |
— | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. |
SampleBiomeAt |
— | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + GetDominantBiomeAt. §8.14. |
ResolveBiomeSampleAt / RebuildBiomeGrid |
— | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to SampleBiomeAt. §8.14, §8.10. |
GetDominantBiomeAt |
— | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
3.7 Cave morphology (SDF rooms/tunnels) — Public/VoxelCaveMorphology.h + .cpp
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
-
namespace VoxelSDF(h:46):Sphere,Ellipsoid,Capsule,RoundedBox,TaperedCapsule,SmoothMin,SmoothMax— all FORCEINLINE SDF primitives. -
namespace VoxelHash(h:158):Mix,Cell,Pair,ToFloat01,ToFloatSigned— deterministic hashing for room/tunnel placement (no storage, infinite worlds). -
Cache structs (h:224-330):
FCachedRoom,FCachedTunnel,FCachedPit,FCachedChimney,FCachedColumn,FChunkSDFCache. -
namespace VoxelCaveMorphology:Function .cpp line Role BuildChunkCache47 Phase 1 (once/chunk): collect rooms, guaranteed backbone ( bTunnelsFlowTowardOrigin: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (TunnelHorizontalBiasnow applies to backbone too), decide tunnels, cull zero-connection rooms (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping atMaxInfluence), pre-bake pits/chimneys/columns, hash-roll per-room terrain op.EvaluateSDFCached589 Phase 2 (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. EvaluateSDF738 Convenience wrapper (builds temp cache) for one-off queries. Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per voxel is the single biggest CPU win.
3.8 Strate system
Public/VoxelStrateTypes.h — shared structs/enums (1228 lines, the data vocabulary):
| Symbol | Line | Role |
|---|---|---|
EVoxelPassageType |
38 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
ESurfaceType |
78 | Floor/Wall/Ceiling/Any (decoration placement). |
EVoxelNoiseType |
99 | FBM/Ridged/Mixed/Cellular. |
ECaveGeneratorType |
146 | TunnelNetwork / FlatPlain / CrystalChamber. |
EVoxelStrateTransition |
183 | Gradient / Hard / Interleaved boundary blends. |
FStrateGenerationParams |
213 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, all terrain-op transport fields, boundary seal). Lerp() static at 844 blends two sets at boundaries. |
FStrateTerrainOpEntry |
965 | Soft-ptr to a terrain op + Weight + Probability. |
FSlabGenerationParams |
1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
FStrateDecoration / FStrateAmbientActor / FStrateCreature |
1160 / 1192 / 1212 | Content spawn entries (consumed by future systems). |
Public/VoxelStrateDefinition.h — UVoxelStrateDefinition : UPrimaryDataAsset
(line 36). One asset = one strate type. Fields: identity, StrateHeightInChunks(60),
TransitionType(79)/TransitionBlendChunks(89), GeneratorType(102),
GenerationParams(113), SlabParams(124), Biomes[]+BiomeMapParams (the biome list +
field tuning — empty ⇒ unchanged world, §8.14), TerrainOperations(147), visuals/fog/light,
content lists, audio, GameplayTags(223). EditConditions show/hide param groups by generator type.
Public/VoxelStrateManager.h + .cpp — UVoxelStrateManager : UObject (h:108).
Maps depth→strate at runtime; owns passages.
FVoxelPassage(h:39): endpoints, radius, type, control points.FStrateSlot(h:84): definition + chunk-Z range + index.Method .cpp line Role Initialize10 Builds the stacked layout from settings+seed (fixed slots + shuffled pool), then GeneratePassages.GeneratePassages146 Deterministic passages between consecutive strates (per-type control points). EvaluateModifierSDF371 SDF of all passages + elevator shaft at a point (for carving). FindSlotIndexForChunkZ427 Z → layout index. GetStrateAt/GetStrateIndex443 / 455 World-Z queries. GetStrateForChunk466 Chunk → definition. GetGeneratorTypeForChunk476 Chunk → generator type. GetSlabParamsForChunk490 Slab params with runtime Z bounds (no blend — slabs use Hard). GetBiomeContextForChunk— Flatten the strate's Biomes[]+BiomeMapParamsinto a PODFBiomeContextfor the biome field. Empty ⇒ biomes disabled. §8.14.GetGenerationParams515 Blended TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). BuildParamsFromDefinition(static)771 Base params + merge all referenced terrain op assets. The one place ops fold into params.
Public/VoxelTerrainOpDefinition.h + .cpp — UVoxelTerrainOpDefinition : UPrimaryDataAsset
(h:67). One asset = one terrain op. EVoxelTerrainOpType (h:36): Terrace, LayerLines,
Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit, Chimney, Dome, Pinch. Per-type
param groups gated by EditCondition. ApplyTo(OutParams, Weight) (.cpp:6) copies only
the active type's fields into FStrateGenerationParams, scaled by Weight.
Public/VoxelBiomeTypes.h (NEW) — biome vocabulary. FBiomeMapParams (Voronoi cell size /
border warp+blend / climate field freqs), EBiomePreviewChannel (preview-bake selector), and plain
runtime PODs FBiomeResolved / FBiomeContext / FBiomeSample / FChunkBiomeCache (the
box-validated per-chunk grid cache). See §8.14.
Public/VoxelBiomeDefinition.h + .cpp (NEW) — UVoxelBiomeDefinition : UPrimaryDataAsset.
One asset = one biome: identity + DebugColor, climate placement box (ReliefMin/Max,
MoistureMin/Max), terrain override (bOverrideTerrain + GeneratorType + archetype params), content profile (Decorations/AmbientActors,
atmosphere override, WaterMaterial, reserved MaterialPaletteIndex), GameplayTags. Referenced from
UVoxelStrateDefinition::Biomes[]. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
3.9 Player edits — Public/VoxelDiffLayer.h + .cpp
UVoxelDiffLayer : UObject (h:77). Stores FVoxelModification (h:43: Center/Radius/Strength;
negative Strength = carve, positive = fill) grouped by chunk in TMap ChunkMods.
| Method | .cpp line | Role |
|---|---|---|
SetBudget |
10 | From VoxelSettings carving caps. |
CanModify |
20 | Budget check (no consume) — for UI. |
GetRemainingModifications / GetRemainingVolume |
47 / 53 | -1 = unlimited. |
ApplyModification |
63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
GetDensityOffset |
131 | Per-voxel combined diff (smoothstep falloff, additive). |
HasModifications |
160 | Fast reject for hot path. |
Clear |
170 | Wipe all (season reset). |
GetTotalModificationCount / GetModifiedChunkCount |
182 / 192 | Stats. |
3.10 Mesher — Public/VoxelMarchingCubesMesher.h + .cpp
UVoxelMarchingCubesMesher : UObject (h:21). Holds Generator ptr, IsoLevel=0,
GradientOffset=1.
| Method | .cpp line | Role |
|---|---|---|
GetDensity |
11 | Local coord → world → Generator->GetDensityAt. |
InterpolateEdge |
28 | Linear edge crossing between two corner densities. |
ComputeGradientNormal |
48 | Central-difference gradient → smooth normal. |
GenerateMesh |
75 | The MC loop over cells; Step controls LOD sampling. |
Public/MarchingCubesTables.h — EdgeTable + TriTable reference data (Paul
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
3.11 Per-chunk content & per-strate atmosphere (2026 redesign — see §8)
| File | Role |
|---|---|
Public/Private/VoxelContentManager.h/.cpp |
UVoxelContentManager — deterministic decoration scatter (LOD0) + aesthetic water planes, per chunk. Owned by AVoxelWorld. §8.5. |
Public/Private/VoxelAtmosphereManager.h/.cpp |
UVoxelAtmosphereManager — per-strate fog/skylight + persistent ceiling/floor layer actors + full AtmosphereActor override. Owned by AVoxelWorld. §8.6. |
The big 2026 redesign (8 archetypes, (0,0) spine, inter-strate gap, per-strate passages, disturbances, content/atmosphere, brush shapes, perf invariants) is documented in §8 — read it first when touching generation/strates/passages.
4. The density pipeline (most-edited hot path)
4.1 GetDensityWithParams (TunnelNetwork) — VoxelGenerator.cpp:277
Stage order (negative=solid throughout). Each stage's anchor:
| Step | Line | What |
|---|---|---|
| 1 — Vertical scale | 307 | Stretch Z before noise (VerticalScale). |
| 2 — Base density | 316 | Everything starts solid at BaseDensity. |
| 3 — Cave warp | 321 | Domain-warp the SDF query coords (organic shapes). |
| 4 — SDF morphology | 368 | Rooms+tunnels via BuildChunkCache/EvaluateSDFCached. |
| 4b — Surface roughness | 564 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
| 4c–4h — Terrain ops | 688 | Per-room op applied near surfaces. Sub-anchors below. |
| · Terracing | 727 | Step-like ledges. |
| · Layer lines | 823 | Horizontal grooves (sin of Z). |
| · Ribbing | 853 | Parallel ridges (sin of Z). |
| · Overhangs | 882 | Low-Z-freq noise shelves. |
| · Cliff sharpening | 918 | Amplify vertical gradient. |
| · Scallop | 962 | Cellular erosion bowls. |
| · Arch/Bridge | 998 | Hash-placed capsules across voids. |
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
| 4h — Pinch | 1142 | Passage bottlenecks. |
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (WormNetworkRange: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
| 6 — Boundary seal | 1274 | Solid top/bottom shells (ApplyBoundarySeal). |
| 7 — Inter-strate passages | 1281 | Carve passages/elevator (ApplyPassageCarving). |
4.2 GetSlabDensity (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
| Step | Line | What |
|---|---|---|
| 1 — Floor surface | 1317 | Noisy floor height. |
| 2 — Ceiling surface | 1343 | Formations hang downward (abs(noise)). |
| 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. |
| 4 — Columns | 1399 | World-space hash grid, full-height. |
| 5+6 — Seal + passages | 1461 | Same seal/passage carving as TunnelNetwork. |
5. "I want to change X" → go here
| Goal | Location |
|---|---|
| Chunk size / voxel scale | VoxelTypes.h:19-23 (rebuild everything). |
| View distance / task budget / LOD distances | VoxelSettings.h (no recompile of logic — data asset). |
| LOD step mapping | AVoxelWorld::LODToStep VoxelWorld.cpp:268; GetLODForChunk :242. |
| How chunks stream in/out | UpdateChunksAroundPosition VoxelWorld.cpp:362. |
| Async threading / stale-result handling | LoadChunk :445, ProcessPendingChunks :301, Epoch logic. |
| Add a new cave feature / terrain op | Add enum in VoxelTerrainOpDefinition.h:36, params there, ApplyTo (.cpp:6), transport fields in FStrateGenerationParams, consume it in a new Step inside GetDensityWithParams. |
| Tweak room/tunnel shapes | VoxelCaveMorphology.cpp BuildChunkCache :47 / EvaluateSDFCached :589. |
| Worm tunnel behavior | GetDensityWithParams Step 5, VoxelGenerator.cpp:1241. |
| Strate stacking / which strate where | UVoxelStrateManager::Initialize :10. |
| Boundary blend between strates | GetGenerationParams :515 + FStrateGenerationParams::Lerp (StrateTypes.h:844). |
| Passages between strates | GeneratePassages :146 + EvaluateModifierSDF :371 + ApplyPassageCarving (Generator.cpp:197). |
| Player carve/fill | CarveAtPosition/FillAtPosition VoxelWorld.cpp:691/709 → UVoxelDiffLayer::ApplyModification :63. |
| Mesh smoothness / normals | UVoxelMarchingCubesMesher::ComputeGradientNormal :48, IsoLevel/GradientOffset (h:51/55). |
| New slab/flat-world generator | GetSlabDensity Generator.cpp:1306 + FSlabGenerationParams (StrateTypes.h:1019). |
| Biome placement / layout | BiomeMapParams on the strate (cell size, warp, climate freqs) + each biome's climate box. Bake AVoxelWorld::BakeBiomePreview to tune. §8.14. |
| What a biome does to terrain | A full archetype param override on the biome (bOverrideTerrain + SurfaceParams); surface output-blends dominant/neighbour heights in GetSurfaceDensity. Caves = content/atmosphere only (determinism, §8.14). |
| Add a biome / biome content | New UVoxelBiomeDefinition asset → add to the strate's Biomes[]. §8.14 / §8.12. |
| Season reset | AVoxelWorld::ChangeSeed :740. |
6. Conventions & gotchas
- Density sign is the #1 source of confusion. Internally (MC) negative = solid,
positive = air. Carve = subtract density (toward positive); Fill = add (toward negative).
FVoxelModification::Strengthnegative = carve. Comments sometimes say the inverse in different layers — trust the MC convention at the mesher. - Coordinate units: density functions take voxel coords (not cm). World↔voxel
conversions live in
VoxelTypes.h. Mesher converts before calling the generator. - Determinism: all randomness is hash-of-(coord, seed, strateIndex) — no RNG state. Same seed ⇒ identical world. Player edits are the only non-deterministic overlay.
- Async safety: background tasks must check
bShuttingDownand only touch the generator/mesher (no UObject mutation). Results return viaProcessQueue.EndPlayblocks untilActiveTaskCount == 0. - Epoch: every regeneration bumps
GenerationEpoch; results tagged with an old epoch are dropped inProcessPendingChunks. Always carry the epoch through new async paths. - Generated code under
Intermediate/andBinaries/is build output — never edit.*.generated.h/*.gen.cppare UHT output for theUCLASS/USTRUCTabove.
7. Files NOT to touch
Binaries/, Intermediate/ — compiler/UHT output, regenerated on build.
MarchingCubesTables.h — canonical reference tables, only change if switching MC variant.
8. Archetypes, spine, disturbances, content & carving (2026 redesign)
A large A-to-Z expansion. The world is a stack of strates the player descends through; each strate can be a fundamentally different archetype, connected at (0,0).
8.1 Archetypes (ECaveGeneratorType, VoxelStrateTypes.h)
Each archetype has its own param USTRUCT (on UVoxelStrateDefinition, EditCondition-gated
by GeneratorType) and its own density function in VoxelGenerator.cpp, dispatched by the
switch in GetDensityAt.
| Archetype | Params struct | Density fn | Idea |
|---|---|---|---|
| TunnelNetwork | FStrateGenerationParams |
GetDensityWithParams |
rooms+tunnels (original) |
| FlatPlain / CrystalChamber | FSlabGenerationParams |
GetSlabDensity |
floor/ceiling void (original) |
| Maze | FMazeGenerationParams |
GetMazeDensity |
tight corridors on a 3D lattice (per-voxel, no cache; edge = lower node + axis hash) |
| SurfaceWorld | FSurfaceGenerationParams |
GetSurfaceDensity |
heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq relief map (M) that scales mountains/elevation for plains↔highland variety, opt-in plateau terracing, beaches at water line, high sky-cap ceiling. (`Surface |
| VerticalShafts | FVerticalShaftParams |
GetVerticalShaftDensity |
full-height shafts + horizontal connectors + partial ledges |
| FloatingIslands | FFloatingIslandParams |
GetFloatingIslandDensity |
asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
| Underwater | FStrateGenerationParams + water |
(reuses GetDensityWithParams) |
tunnel rock + high water table |
All density fns share the convention: internal positive=solid, apply origin spine →
boundary seal → inter-strate passages, then return -Density (MC: negative=solid).
StrateManager provides params per chunk via GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk
(macro VF_ARCHETYPE_PARAMS_GETTER) — no cross-boundary blend (Hard transitions between archetypes).
On top of the archetype, an optional biome layer (§8.14) modulates terrain & content WITHIN a
strate via a window-invariant XY field — currently wired into SurfaceWorld.
8.2 (0,0) spine & hybrid connections
ApplyOriginSpine(VoxelGenerator.cpp, static helper) carves a guaranteed open vertical column at XY (0,0) in every strate's interior (seals untouched). Radius =UVoxelGenerator::OriginSpineRadius←VoxelSettings::OriginSpineRadius. Called before everyApplyBoundarySeal.- Descent is player-dug through the thin seals at (0,0). The single auto-opened
connection is the surface entry shaft at (0,0) through the top of strate 0
(
GeneratePassages,bOpenSurfaceEntry). - Hybrid extras: auto-carved shortcut passages per boundary, placed away from (0,0).
Now fully per-strate — see §8.8 (the upper strate's
PassageConfigdrives count/style/shape).
8.3 Disturbance layer (the "wow" post-process)
FStrateDisturbanceParams (on the definition, all archetypes). ApplyDisturbances
(VoxelGenerator.cpp static, MC convention) runs in GetDensityAt after dispatch:
chasms (carve air), bridges (solid spans), ridges (solid blades). Stays inside seal bands.
Provided per chunk by StrateManager::GetDisturbanceParamsForChunk.
8.4 Cross-chunk determinism (the seam-prevention invariant)
BuildChunkCache (VoxelCaveMorphology.cpp) uses two regions: a wide COLLECT region
(2*MaxTunnelLength + MaxInfluence) over which connectivity is decided (NN filtered to
<= MaxTunnelLength, origin cap = deterministic top-N by hash), and a tight STORE region
(+MaxInfluence) kept for per-voxel eval. This makes the room/tunnel graph window-invariant.
If you add a connectivity rule with longer edges, the COLLECT region must still cover the
max edge reach, and decisions must not depend on the stored window.
8.5 Content scatter & water — VoxelContentManager.h/.cpp (NEW)
UVoxelContentManager (owned by AVoxelWorld, game-thread). Chunk lifecycle:
PopulateChunk(coord, mesh, LOD) in ApplyMeshToChunk, ClearChunk in UnloadChunk,
ClearAll/SetSeed in ChangeSeed, SetActiveStrate(GetStrateAtPosition(player)) each Tick.
Deterministic decoration scatter on mesh vertices (surface-type / water-relative /
align-to-normal / random-yaw / scale / MaxPerChunk, from FStrateDecoration).
Two render paths per entry: ActorClass (real actors — lights/logic/interaction,
keep MaxLODLevel=0) vs InstancedMesh (batched per-chunk HISM, no tick/actor cost,
no collision; safe at MaxLODLevel 1-2 so visual props don't pop out with LOD0 — emissive
materials still glow at distance). Placement samples the LOD's vertices ⇒ instances
re-scatter slightly on LOD swap (masked by terrain pop). Strate light culling:
light components on decoration actors are visible only while the player is in the SAME
strate (SetActiveStrate, no-op until strate change) — analytical occlusion: seals+gap are
light-tight, and shadowless lights otherwise BLEED through rock (shadowed ones render black
at full cost). Aesthetic water = one scaled engine plane (/Engine/BasicShapes/Plane) per
water-surface chunk (any LOD), material UVoxelStrateDefinition::WaterMaterial. Water Z:
bHasWater + WaterLevelRelative (Surface/tunnel params) → StrateManager::GetWaterLevelWorldZForChunk.
Per-biome content (§8.14): when the chunk's dominant biome (Generator::GetDominantBiomeAt)
supplies decorations they REPLACE the strate's; biome WaterMaterial overrides the plane's material
(level stays strate-global). NOTE: GetChunkStrateIndex (light culling) feeds GetStrateIndex which
expects Unreal cm — it now passes chunkZ*CHUNK_SIZE*VOXEL_SIZE (was voxel-Z, a latent strate-match bug).
8.6 Atmosphere — VoxelAtmosphereManager.h/.cpp (NEW)
UVoxelAtmosphereManager (owned by AVoxelWorld, gated by bManageAtmosphere).
UpdateForPlayer(pos) each Tick, reacts only on strate change. Drives a managed
UExponentialHeightFogComponent + movable USkyLightComponent from the player's strate
(FogColor/FogDensity/bVolumetricFog/AmbientLightColor/AmbientLightIntensity), and spawns
PERSISTENT ceiling/floor "layer" actors (Def->CeilingLayerActor/FloorLayerActor + ZOffsets
- rotations) that follow the player in XY — the sky-island sea-of-clouds / two-sided fog.
Def->AtmosphereActor(a full BP with your own fog/sky/postprocess) OVERRIDES the managed fog+sky for that strate.Reset()on ChangeSeed/EndPlay. (Skylight ambient underground is weak — captures a dark scene; fog is the strong visual.) Per-biome atmosphere (§8.14):UpdateForPlayeralso resolves the player's dominant biome and, when the biome hasbOverrideAtmosphere, its fog/sky beats the strate's (reacts on biome change, not just strate change).ApplyFogSky(Def, Biome)is the shared path; layer actors + the fullAtmosphereActorBP stay strate-level. Needs the generator injected (Initialize(..., Generator)).
8.7 Inter-strate bedrock gap
VoxelSettings::InterStrateGapChunks (N) inserts N chunks of SOLID bedrock between consecutive
strates (StrateManager::Initialize leaves the gap in the layout). IsGapChunk detects it;
GetDensityAt renders gap chunks as solid + passages only (no caves/spine/seal) so the player
digs (0,0) through the gap to descend. GetStrateUnrealZRange gives a strate's cm Z range.
8.8 Inter-strate passages — PER-STRATE (FStratePassageConfig on the definition)
Each strate's PassageConfig (VoxelStrateTypes.h) controls its descent tunnels to the layer
below: Connections, Style (EVoxelPassageStyle: Straight/Worm/Spiral/Cascading),
MouthRadius/MidRadius (tapered width → FVoxelPassage::ControlRadii + VoxelSDF::TaperedCapsule),
ReachMin/Max (depth into each strate), DistanceMin/Max (from the (0,0) spine), Wander,
Segments, VerticalWobble, Spiral/Cascade params. Built in StrateManager::GeneratePassages
as control-point chains. Worm = independent fBM per horizontal axis (PassageFBM static)
with a flat-top envelope → organic squirm (NOT a 1D zigzag, NOT a same-freq 2-channel spiral).
EvaluateModifierSDF (per voxel) bounding-sphere-culls each passage
(FVoxelPassage::BoundCenter/BoundRadiusSq) — perf-critical. The (0,0) surface entry is a
simple straight tube. Global passage settings were removed from VoxelSettings.
8.9 Carving — brush shapes + editor controls
FVoxelModification has EVoxelBrushShape {Sphere,Box,Capsule} + BoxExtent/CapsuleEnd/
Falloff + GetWorldBounds. UVoxelDiffLayer::GetDensityOffset switches per shape; chunk
overlap uses the shape AABB. AVoxelWorld: CarveBox/FillBox/CarveCapsule/FillCapsule/ ApplyModification (BlueprintCallable) + EditorCarveSphere/EditorFillSphere (CallInEditor)
driven by EditorBrush* props.
8.10 Performance invariants (DON'T regress)
- Streaming (
UpdateChunksAroundPosition): rebuild/cull the desired set ONLY when the player crosses a chunk boundary (LastUpdateCenter); use theDesiredSetTSet for the cull; idle viabAllChunksLoaded. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.) - LOD changes HOT-SWAP (
LoadChunkonly, never unload-first) → no holes. LOD reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads), NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task budget and strands it at a stale LOD. Idle (bAllChunksLoaded) only when a full scan finds no loads AND no LOD mismatches outstanding. - SDF cache (
GetDensityWithParams): search-BOX validity, not chunk-key — gradient ±1 sampling must not thrash the (expensive) rebuild. - Per-chunk param cache in
GetDensityAt: GenType + param struct + disturbance cached thread-locally per chunk; don't move the fetch/blend back to per-voxel. - Biome cache (
ResolveBiomeSampleAt/FChunkBiomeCache, §8.14): validity is a world-XY BOX + ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep the box halo (≥ CHUNK_SIZE) + cell margin (warp + CellSize) so the 3x3 lookup never misses. - Passage cull (§8.8) + morphology two-region (§8.4): both are per-voxel-cost critical.
- Mesher density grid + margin ring (
GenerateMesh): sample each grid point ONCE into a flat(CHUNK_SIZE/Step + 1 + 2)³array (the+2is a 1-point MARGIN ring, indices −1..GridDim, for T1.b normals). The cell loop reads 8 corners from it; per-cell sampling would callGetDensityAt~8× too often. Geometry is bit-identical (edge positions unchanged). Don't refactor back to per-cornerGetDensityand don't drop the margin ring (normals + seamless borders need it). - Normals from the density grid (T1.b) (
GenerateMesh): corner gradients = central differences on the (margin) grid; edge normals interpolate the two corner gradients by the SAMEtas the position → seamless across chunk borders (both sides use identical pure samples). NO per-vertexGetDensityAt(was ~6/vertex, often as costly as the whole grid).ComputeGradientNormalis now unused. Only NORMALS changed vs the old path; geometry is identical. - Surface column cache (T1.a) (
FSurfaceColumnCache,GetDensityAtSurfaceWorld branch): the heightfield + sky-cap + biome blend are XY-only but sampled ~33× per column (once per Z grid-point). Cached per integer XY (box-valid, like the SDF cache) and reused down the column. BoxHalo = CHUNK_SIZE + 8each side so the T1.b margin ring stays inside (no thrash). Used ONLY for integer-XY queries; fractional queries compute directly → bit-identical. Don't key it by chunk and don't feed it fractional coords. - Collision only at LOD0 (T1.c) (
ApplyMeshToChunk):UpdateSectionConfig(..., LOD==0). LOD1/2 chunks are unreachable (the §8.10 reconciliation hot-swaps to LOD0 before the player arrives), so cooking their Chaos collision is waste. Don't force collision on for all LODs. - No shadows on LOD2 (draw-call cut) (
ApplyMeshToChunk):MeshComp->SetCastShadow(LOD <= 1). Every shadow-casting chunk emits a SECOND draw in the shadow-depth pass; the farthest tier (LOD2, blocky) doesn't need it. Re-applied every apply (LOD hot-swaps reuse the component). PullingLOD0Distance/LOD1Distanceinward pushes more chunks into the shadowless LOD2 band → fewer draws (free fps knob). Widen to<= 0(drop LOD1 shadows too) if still draw-bound. NOTE: fps is a RENDER-side problem (draw calls ≈ visible chunk count × passes) — generation cost (worker threads) and LOD resolution (cuts triangles, not draws) don't move it. - Insights scopes
VoxelForge_GenerateMesh/VoxelForge_ApplyMeshToChunk(Perf 0) bracket the worker gen + game-thread apply — capture a trace to see if we're density- or upload-bound. - Float SIMD noise core (T2.a) (
Public/VoxelNoise.h): the density hot path usesVoxelNoise::Perlin3D(single-sample, float, table-free hash-gradient) andVoxelNoise::FBM/Ridged(octaves evaluated 4-wide via SSEPerlin3D_x4) — NOTFMath::PerlinNoise3D(double-precision, the old ~6.6 ms/chunk noise cost).FractalNoise3D/RidgedNoise3DinVoxelGenerator.cppare now thin wrappers over it; every call site is unchanged. It's a DIFFERENT noise field than FMath's ⇒ a ONE-TIME world re-tune (fBm/Ridged contracts/[-1,1] are identical). Pure function of (x,y,z) ⇒ every box-validity cache stays valid. ScalarPerlin3Dand SSEPerlin3D_x4are op-for-op identical (bit-identical on x86) — the SIMD path is a free speedup;#define VF_NOISE_USE_SIMD 0falls back to scalar with no re-tune if a toolchain rejects the SSE4.1 intrinsics. StrateManager's passage/transition Perlin calls were left onFMath(layout-time, not per-voxel). Don't reintroduceFMath::PerlinNoise3Don the density path. ProcessQueueMUST beEQueueMode::Mpsc(VoxelWorld.h): up toMaxConcurrentTasksChunkGenworker threadsEnqueueconcurrently; the game thread is the sole consumer. The defaultSpscis single-producer — concurrent enqueues race the tail link and silently DROP results, leakingPendingChunkCoordslots until the budget is exhausted and streaming stalls for good (intermittent; worst during the completion bursts right after the player moves).
8.11 Live tuning & debug (AVoxelWorld, CallInEditor / PIE)
RebuildStrates— re-reads ALL ofVoxelSettingsand rebuilds layout/gap/passages/spine + regenerates. Use after changing those (plainRegenerateAllChunkskeeps the old layout/passages).bDebugDrawPassages— draws every passage (cyan path, green=upper / red=lower endpoints).EditorCarveSphere/EditorFillSphere+EditorBrush*props — manual carve/fill in PIE.
8.12 Authoring a strate (data asset)
- Create
UVoxelStrateDefinition, pickGeneratorType→ its param group appears; tune it. PassageConfig→ how THIS strate connects DOWN (count / style / tapered width / length / placement).Disturbancesfor chasms/bridges/ridges;bHasWater+WaterMaterial(+WaterLevelRelative) for water.- Atmosphere:
FogColor/Density,AmbientLight*,bVolumetricFog, or a fullAtmosphereActorBP;CeilingLayerActor/FloorLayerActor(+offsets/rotations) for cloud seas. Decorations/AmbientActors(placement rules) for content + lights.- (Optional)
Biomes[]+BiomeMapParamsto vary terrain/content within the strate (§8.14). AuthorUVoxelBiomeDefinitionassets (climate box + modulation + content), then tune layout withAVoxelWorld::BakeBiomePreview. TurnReliefStrengthdown when biomes drive elevation. - Reference from
VoxelSettings(StratePool/FixedStrates). Global knobs there:OriginSpineRadius,bOpenSurfaceEntry,InterStrateGapChunks, view distances, LOD, carving budget.
8.13 New files this redesign
Public/Private/VoxelContentManager.h/.cpp (§8.5) · Public/Private/VoxelAtmosphereManager.h/.cpp (§8.6) ·
Public/VoxelBiomeTypes.h + Public/VoxelBiomeDefinition.h/Private/VoxelBiomeDefinition.cpp (§8.14).
Everything else extended existing files: VoxelStrateTypes.h (archetype params, disturbance,
FStratePassageConfig, enums), VoxelStrateDefinition.h, VoxelGenerator.h/.cpp (archetype
density fns + spine/disturbance/param-cache), VoxelStrateManager.h/.cpp (per-archetype getters,
passages, gap, atmosphere Z helper), VoxelWorld.h/.cpp (managers, streaming perf, brush API,
editor buttons), VoxelDiffLayer.h/.cpp (brush shapes), VoxelSettings.h, VoxelCaveMorphology.cpp
(two-region determinism). Status: compiles & runs in-editor.
8.14 Biome system (Stage 1 — climate-driven, full-param overrides)
Biomes vary terrain and content WITHIN a strate. A biome is a "mini-strate-variant": it
can carry a FULL archetype param override (its own FSurfaceGenerationParams, …) plus a content
profile, placed by a deterministic, window-invariant world-XY field. Empty Biomes[] ⇒ bit-identical
to the pre-biome world. (Replaces the earlier FBiomeModulation scalar bag — full params let a biome
change anything, e.g. frequencies, which scalar multipliers couldn't.)
- Assets/data.
UVoxelBiomeDefinition(one per biome):DebugColor, climate box (relief, moisture),bOverrideTerrain+GeneratorType+ the matching archetype param struct (Surface wired), content profile (decorations/atmosphere/water). +UVoxelStrateDefinition::Biomes[]&BiomeMapParams. Types inVoxelBiomeTypes.h(§3.8). - The field (pure XY, window-invariant — §8.4).
SampleBiomeAt(VoxelGenerator.cpp): warped Voronoi over a jittered grid → dominant cell + nearest neighbour (F1/F2) + border blend weight. Each cell's biome is chosen byClassifyBiomeAtSitefrom the site's climate =SampleRelief(the relief map M, shared with SurfaceWorld terrain) +SampleMoisture, matched against each biome's (relief, moisture) box → coherent geography. Climate must vary much slower thanCellSize(~4-6 cells/feature) or it's salt-and-pepper. - Per-chunk resolution (perf — §8.10).
ResolveBiomeSampleAt/RebuildBiomeGridbuild aFChunkBiomeCache: the expensive cell classification is done ONCE into a small grid; per voxel only a warp + 3x3 lookup, returningFBiomeSample(dominant + neighbour + weight). Cache validity is a world-XY BOX + ChunkZ + Seed (NOT a chunk key) — gradient-normal + boundary samples stay inside the box and don't thrash the noise-heavy rebuild (same as the SDF cache). Bit-identical toSampleBiomeAt, so the baked preview matches the terrain.GetBiomeContextForChunksupplies the flattened POD context per chunk (thread-localCP_BiomeCtx). - Consumption — SURFACE (output-blend). Per chunk,
CP_SurfaceBiomeParams[]holds each biome's resolved surface params (its override whenbOverrideTerrain+ GeneratorType matches, else the strate's) with structural fields forced from the strate (Z bounds, seal, base density, water level). Per voxel:ResolveBiomeSampleAt→ dominantPD(+ neighbourPN);GetSurfaceDensitycomputesComputeSurfaceTerrainZforPDand, in the border band, forPN, and lerps the resulting HEIGHTS. Blending heights (not params) is seamless across any difference (frequencies included) — what per-param blend never could.PD==PN, weight 0 ⇒ bit-identical, no biomes. - Consumption — CAVES: structural overrides are NOT applied (determinism). Rooms/tunnels are
decided over a wide COLLECT region spanning chunks (§8.4); making room params vary by region would
need the biome sampled per room site inside
BuildChunkCache, or it breaks window-invariance (a room near a border resolves differently per querying chunk → seams/holes). So SDF archetypes (Tunnel/Maze/Shaft/Islands) keep strate-level structure; biomes affect them via content + atmosphere only (below). Per-room-site biome params = a future deep task. - Consumption (content/atmosphere).
GetDominantBiomeAt(x,y,chunkZ)(game-thread, uncached) → biome ASSET. ContentManager: dominant biome's decorations (else strate's) + water material override. AtmosphereManager: player's dominant biome fog/sky (bOverrideAtmosphere). Works for ANY archetype. Water LEVEL stays strate-global (continuous plane); biomes retint material only. - Preview tool.
AVoxelWorld::BakeBiomePreview()(CallInEditor) bakes biome / relief / moisture toSaved/BiomePreview.pngvia a transient generator (no PIE). Needs theImageWrappermodule. - Status: A (field+asset+preview), B (terrain), C (content/atmosphere) verified in-editor.
Full-param redesign (surface output-blend) code-complete, pending build. Cave structural biomes
deferred (determinism, see above). Per-voxel biome warp (+2 Perlin) & content
GetDominantBiomeAtare future T1.a column-cache candidates.