Terrain generation
import BiomeClimateMatrix from ‘../../components/BiomeClimateMatrix.astro’;
verifiedAgainst f1eaefe · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.
In one paragraph
Section titled “In one paragraph”Terrain is not generated per chunk in the conceptual sense. It is a pure function of world seed, generator version, settings, and absolute world coordinate; chunks are only 16³ windows that evaluate that function in parallel. WorldGenerationSettings is the authored, versioned ScriptableObject; TerrainGenerationParameters.FromSettings() flattens it into a blittable snapshot for Burst. The managed TerrainGenerator is the readable parity oracle for tools and direct queries. The streamed path precomputes each relevant X/Z column’s climate and surface height, discovers deterministic tree roots, then runs TerrainGenerationJob once per voxel. Both paths use the same coordinate-hashed noise, salt values, operation order, geology rules, and feature precedence. Saved edits are deliberately absent from this page’s pure base-world answer: streaming applies WorldEditOverlay after the job has returned ChunkData.
Figure 1: High-altitude survey of world seed -1L rendered in HDRP 17.6.0 with physical sunlight (EV 13.7). Snow peaks, mountain ridges, and river reaches generated purely from coordinate-hashed Burst jobs.
Figure 2: Geological strata in the Badlands biome. Continuous horizontal sedimentary banding resolved deterministically across chunk seams without seam discontinuities.
The pieces
Section titled “The pieces”| Symbol | File | Responsibility |
|---|---|---|
WorldGenerationSettings |
Data/Generation/WorldGenerationSettings.cs:11 |
Versioned authoring data: seed, profile knobs, palette, biome catalogue, and tree catalogue. Changing it changes the base world. |
TerrainGenerationParameters.FromSettings |
World/Generation/TerrainGenerationParameters.cs:232 |
Converts managed authoring references into scalar fields and stable block ids that a Burst job may safely read. |
TerrainColumnMath |
World/Generation/TerrainColumnMath.cs:18 |
Shared, Burst-safe climate, macro-relief, river, noise, hash, and negative-coordinate math. |
BiomeClassifier |
World/Generation/BiomeClassifier.cs:11 |
Managed climate-window classifier; the Burst equivalent is TerrainColumnMath.ClassifyBiomeIndex. |
TerrainGenerator |
World/Generation/TerrainGenerator.cs:19 |
Managed oracle for GenerateChunk, direct block queries, editor tooling, and test assertions. |
TerrainColumnJob / TerrainColumnSample |
World/Generation/TerrainColumnJob.cs:19 · TerrainColumnSample.cs:3 |
Burst pre-pass: surface height and biome index for the chunk and a ten-block X/Z margin. |
TreeRootDiscoveryJob |
World/Generation/TreeRootDiscoveryJob.cs:20 |
Finds each valid cell-owned tree root within canopy reach exactly once. |
TerrainGenerationJob |
World/Generation/TerrainGenerationJob.cs:19 |
Burst IJobParallelFor that maps every local voxel index to one stable ushort block id. |
OreDistributionMath |
World/Generation/OreDistributionMath.cs:46 |
Shared deposit predicate: depth window, host-rock/fracture signal, biome exoticness, and sparse outlier districts. |
ChunkStreamingRuntime.ProcessGeneration |
World/Presentation/ChunkStreamingRuntime.cs:404 |
Schedules the three jobs with dependencies, then turns completed output into a ChunkData and applies the edit overlay. |
The flow
Section titled “The flow”flowchart LR S["WorldGenerationSettings\nseed + generator version + authored catalogues"] --> P["TerrainGenerationParameters\nblittable snapshot"] P --> C["TerrainColumnJob\nheight + biome, 16x16 + margin"] C --> T["TreeRootDiscoveryJob\ncell-owned roots in reach"] T --> G["TerrainGenerationJob\nparallel voxel evaluation"] C --> G G --> D["ChunkData\nbase generated blocks"] D --> E["WorldEditOverlay\nsparse player changes last"] E --> M["Chunk meshing + view pool\npresentation only"] P -. same fields/salts/order .-> O["TerrainGenerator\nmanaged oracle + tools/tests"]
The runtime takes a fresh parameter snapshot before scheduling work. It allocates the column table, schedules TerrainColumnJob; schedules TreeRootDiscoveryJob after that handle; then schedules TerrainGenerationJob after the tree handle (ChunkStreamingRuntime.cs:413). There is no synchronous gap between those jobs. On completion, the runtime copies the flat ushort output to ChunkData, applies WorldEditOverlay, retains it under the chunk coordinate, and refreshes affected meshes (ChunkStreamingRuntime.cs:458).
1. Authoring becomes a job-safe snapshot
Section titled “1. Authoring becomes a job-safe snapshot”WorldGenerationSettings owns the seed and generatorVersion along with all tuning fields (WorldGenerationSettings.cs:5). It also points to the GenerationBlockPalette, BiomeCatalog, and TreeSpeciesCatalog. None may leak into a worker job. FromSettings() resolves those catalogues to scalar configuration and stable ushort block ids, including one surface block and tree-shape record per known biome/species.
This is why a terrain change has two parts: author the value in Data, then make it part of TerrainGenerationParameters. Adding a field only to WorldGenerationSettings makes it invisible to Burst; adding one only to the snapshot makes it impossible to author. A base-world-affecting change also needs an intentional generator-version compatibility decision; a saved world records the seed and generator version, rather than serializing all generated terrain.
2. X/Z context: climate, relief, and rivers
Section titled “2. X/Z context: climate, relief, and rivers”For every world X/Z column, the code first derives a biome and final surface height. Climate is two independent coordinate-hashed fields: temperature and moisture. The managed classifier tests the authored BiomeCatalog.ClassificationOrder windows and falls back to Plains, so catalogue order is part of the classification contract (BiomeClassifier.cs:16). The Burst path produces the same choice as a compact 0–8 index, not a BiomeType flags value (TerrainColumnMath.cs:45).
Uncarved height is the sum of a base height, continent signal, biome/mountain uplift, peakness, tectonic ridge, structural basin, broad gradient noise, and detail gradient noise. Erosion changes the relief amplitude rather than selecting a biome. Rugged Badlands quantize the result into terraces (TerrainColumnMath.cs:120). Five separate macro signals—continentalness, erosion, peakness, tectonic ridge, and basin depth—are sampled before biome decoration (TerrainColumnMath.cs:154).
River routing deliberately queries uncarved height to choose viable highland springs and sea-level mouths; it cannot recursively consult a river it is deciding to create. The selected route supplies both a narrow channel and a broader dry valley carve. The final GetSurfaceHeight applies those carves after the base height (TerrainColumnMath.cs:102).
3. Why the column and tree pre-passes exist
Section titled “3. Why the column and tree pre-passes exist”A 16³ chunk has 4,096 voxels but only 256 own X/Z columns. Re-evaluating climate and height once per voxel would repeat Y-independent noise sixteen times per column. TerrainColumnJob computes the 16×16 footprint plus a margin of ten blocks on each side, a 36×36 table, so nearby tree-root and slope queries can reuse it (TerrainColumnJob.cs:19). Queries outside that deliberately generous table compute directly through the same TerrainColumnMath, so the margin is an optimization rather than a different generation rule.
Trees are owned by deterministic cells, not by whichever chunk happens to load first. TreeRootDiscoveryJob searches every root cell whose canopy might touch the chunk, rejects candidates in river channels, wrong biomes, underwater terrain, or non-flat ground, then chooses an allowed species and trunk height from stable hashes (TreeRootDiscoveryJob.cs:52). The voxel job only tests each voxel against that small discovered-root list. That is both faster and the rule that prevents seams, duplicate trees, and missing canopies at chunk borders.
4. One voxel’s actual precedence
Section titled “4. One voxel’s actual precedence”The following is the existing order inside TerrainGenerationJob.GetGeneratedBlock, not a suggested order (TerrainGenerationJob.cs:83). A new pass needs an explicit place in this ladder; inserting it casually changes what it can overwrite.
- Below Y 0: air. The bedrock floor itself is at Y 0.
- Hidden structure: if a valid buried discovery owns this voxel, return its block before ordinary terrain.
- Above surface: ocean water fills low terrain to sea level; river water fills a carved channel; a qualifying column gets a one-block snow cap; otherwise air.
- At or below surface: bedrock at Y 0; then caves, organic caverns, and ravines carve to air while respecting floor/surface clearance.
- Surface material: exposed rock wins on non-sandy steep slopes; otherwise coordinate-stable sand sediment or the biome’s authored surface block.
- Subsurface: Badlands gets its sand/stone bands; then sand or dirt filler to its stable depth; deeper material resolves to a natural deposit or stone.
- Above-ground features: a tree may replace a snow-cap block; a landmark may occupy otherwise-air; then a tree trunk/leaves may occupy otherwise-air.
The core terrain branch is visible together in TerrainGenerationJob.cs:158. The managed path uses the same precedence in TerrainGenerator.GetGeneratedBlockForColumn; when the two implementations disagree, that is a correctness bug, not an acceptable preview difference.
5. Caves, deposits, and grade are separate questions
Section titled “5. Caves, deposits, and grade are separate questions”Granular caves use 3D value noise under a threshold, bounded away from the bedrock floor and surface. Organic caverns and biome-limited ravines are additional carve predicates; any one returns air before sediment or ore selection. A cave does not delete an ore record—it means that coordinate’s final base block is air.
Deep geology starts only after filler depth. OreDistributionMath checks profiles rarest first, so overlap deterministically chooses the more valuable material (OreDistributionMath.cs:126). A profile has a vertical likelihood band, host-rock and fracture fields, a biome-exoticness penalty, and a sparse outlier district that can make an otherwise poor local occurrence. The resolver then maps OreDepositKind to the profile’s stable palette ids (TerrainGenerationJob.cs:221). Only natural geological materials appear in this list—never refined products.
Ore grade is not stored in a voxel or a second block id. TerrainGenerator.GetOreGradeAt first asks whether a deposit body occurs there, then computes the pure OreGradeMath field on demand; it returns zero outside a body (TerrainGenerator.cs:82). That separation lets the same visual ore body contain a lean halo and richer core without making chunks carry an extra channel.
6. How the whole world loop connects
Section titled “6. How the whole world loop connects”Terrain generation is one section of a larger ownership chain. The important fact is that information moves in one direction at each boundary: authored settings become an immutable job snapshot; job output becomes mutable loaded-world data; that data becomes a view/collider; player actions become sparse overrides; a save records only those overrides. No presentation component writes directly into the generator, and no generator pass reaches into a scene object.
sequenceDiagram participant S as GameSessionRuntime\nslot + seed owner participant W as ChunkStreamingRuntime\nloaded-world owner participant Q as ChunkScheduler\nversioned work policy participant J as Generation jobs\ncolumn → roots → voxels participant D as ChunkData + overlay\nworld model participant M as Meshing job\nvisible-face data participant V as ChunkView pool\nmesh + collider participant G as Gameplay/presentation\npublic world API S->>W: configure selected/saved seed W->>Q: apply desired chunk plan Q->>J: begin token-budgeted generation J-->>W: completed base blocks + token W->>D: copy output, apply overrides W->>M: snapshot blocks + six borders M-->>W: visible faces W->>V: upload mesh, collider, activate W-->>G: ChunkActivated / GetBlockOrAir G->>W: validated edit or query W->>D: mutate loaded data + delta S->>D: save/load sparse overrides
1. A slot chooses the world before any chunk exists. GameSessionRuntime asks the save service whether the selected slot has metadata. For a new slot it converts a user seed, calls ConfigureWorldSeed() before streaming has started, creates an empty WorldSaveData, and records its internal seed and generator version. For an existing slot it installs the saved seed, then loads its edits (GameSessionRuntime.cs:184). ChunkStreamingRuntime.ConfigureWorldSeed() clones the inspector-authored settings before changing its seed and refuses to run once a plan, chunk, or job exists (ChunkStreamingRuntime.cs:181). That prevents one play session from mutating the authored asset—or from silently changing the formula underneath loaded data.
2. Player movement chooses which coordinates to evaluate, not what they contain. The focus transform drives the streaming plan. The scheduler returns versioned tokens under a per-frame budget; the runtime uses each token to schedule the generation chain described above. The generator itself only sees absolute coordinates and parameters. This separation is why walking quickly may defer a distant chunk, but can never make that chunk generate differently.
3. ChunkData is the hand-off from pure generation to the mutable loaded world. Completed job output is copied out of native memory, then the overlay is applied. Only after that does meshing see it. ChunkMeshingJob receives immutable snapshots of the 16³ blocks and all six one-voxel neighbor borders, emits only visible faces, and creates no Unity mesh (ChunkMeshingJob.cs:49). The main thread turns those faces into a mesh/collider and only raises ChunkActivated after the lifecycle reaches Active (ChunkStreamingRuntime.cs:585). Environment, spawn, audio, and presentation companions therefore react to an active public boundary rather than polling or owning the dictionaries themselves.
4. Gameplay reads and changes the loaded-world boundary, never a generator result. ChunkStreamingRuntime.GetBlockOrAir() returns from an already-loaded ChunkData and returns air outside the loaded set (ChunkStreamingRuntime.cs:337). A break/place request enters BlockEditService, which re-reads the raycast target and rejects a stale mesh hit before it calls IVoxelBlockWorld.TrySetBlock() (BlockEditService.cs:11). The runtime updates the loaded block, compares it against TerrainGenerator.GetGeneratedBlock(), records only a true difference in WorldEditOverlay, and queues remeshing for the edited chunk plus all six neighbors. The neighbors matter because one changed border voxel can expose or hide a face on either side.
5. Save/load closes the loop by preserving differences, not an enormous terrain snapshot. WorldEditOverlay.SetOverride() removes an entry if the player puts the generated block back; otherwise it records a local block id under its chunk coordinate (WorldEditOverlay.cs:21). It serializes deterministically in coordinate order. On load, TryRestoreWorldSaveData() rejects a mismatched seed or generator version, validates the sparse data, regenerates currently loaded chunks from the compatible generator, reapplies their overrides, then remeshes them (ChunkStreamingRuntime.cs:245). The result is the same player-modified world without ever treating a rendered mesh or a streamed chunk cache as authoritative save data.
Invariants you must not break
Section titled “Invariants you must not break”- Base generation is coordinate-pure. Same seed, generator version, settings, and world coordinate must yield the same block regardless of chunk load order, player position, time, or worker scheduling. Never introduce
UnityEngine.Randomor mutable global RNG state. - The managed and Burst paths are one algorithm. Preserve salts, rounding, precedence, biome-index mapping, and block resolution in both.
TerrainGenerationJobTestsexplicitly compare job output withTerrainGeneratorat negative coordinates and across representative settings (Tests/EditMode/World/TerrainGenerationJobTests.cs:16). - All job configuration enters through
TerrainGenerationParameters. NoScriptableObject, managed catalogue, or Unity object is legal in a Burst job. ExtendFromSettings()and its tests whenever you add an authored generation input. - Features are world-owned, never chunk-owned. Tree, landmark, river, cavern, and structure candidates derive from absolute coordinate cells and query beyond a chunk’s edge. Do not add a
one tree per chunkshortcut. - Keep biome indices distinct from
BiomeTypeflags. The Burst/sediment index is a fixed 0–8 ordering; casting a flags enum would silently select the wrong surface or deposit behavior. The managed mapping documents this trap (TerrainGenerator.cs:289). - Generated base terrain is never persisted. Apply sparse
WorldEditOverlaychanges after generation and serialize only those changes with seed/version metadata. Regeneration is the save format. - World-gen deposits must be geological. Add a real ore, mineral, evaporite, clay, native element, or analogous natural occurrence; never spawn a refined alloy or manufactured chemistry block as terrain.
Where it connects
Section titled “Where it connects”- Authoring:
GenerationBlockPalette,BiomeCatalog, andTreeSpeciesCataloglive inVoxelSandbox.Data;WorldGenerationSettingsis their runtime-facing bundle. Catalogues & authoring explains the broader authored-data boundary. - Streaming: Chunk streaming owns job lifetime, plan priority,
ChunkData, the edit-overlay application point, and subsequent mesh refresh. Terrain code itself knows nothing about a player or a view. - Meshing: the generated
ChunkDataand its neighboring boundary snapshots feedChunkMeshingJob, thenGreedyMeshMergerandChunkView. Meshing is presentation; it must not affect a future base-world query. - Session, interaction, and saves:
GameSessionRuntimechooses the seed / slot and orchestrates save/load;BlockEditServiceturns a validated raycast intent into a loaded-world mutation;WorldEditOverlayis the durable delta. The detailed loop is in §6 above. - Science and progression:
OreDistributionMathdetermines where a deposit body exists;OreGradeMathsupplies its measured grade to the mined-material and vessel chain. SeeDocs/MASTERPLAN.md§36.4a and §39.7–39.9 for the design contract behind that resource geography. - Tests and tools:
TerrainGeneratorTests,TerrainGenerationJobTests,TerrainColumnJobTests,TreeRootDiscoveryJobTests,RiverFieldTests, andTerrainGeographyFieldsTestsprotect deterministic seams and parity. The Voxel Workshop’s World Studio is the intended tuning surface, not ad-hoc constants in jobs.
See also
Section titled “See also”- Chunk streaming — the runtime that schedules the generation pipeline and consumes its output.
- Add a voxel block type — a worked example touching the palette, deposit ordering, managed resolver, Burst resolver, and generation tests together.
- Synced Masterplan §8 (determinism and procedural-generation direction), §36.4a (ore-grade evidence chain), and §39 (chemical geography).
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.