Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Terrain

Verified against Minecraft 26.2 · Part XII · One chunk’s rock: seven hundred and sixty-eight cells filled from their corners, a water table decided before the caves are cut, and the cave that fills with water because of it.

You dig into a cave and it is flooded. The water is not a fluid that flowed in and settled; nothing flowed anywhere. Before the cave existed, while the chunk was still solid stone, something decided that a point at that depth in that column belongs to water rather than to air — and when the carver came through and asked what to put in the hole it was digging, that answer was still on file. A carver does not choose the block it carves. It chooses the shape; the Aquifer chooses the material, and it chose it for the stone as well.

This page is the three chunk statuses that turn a scalar field into blocks — ChunkStatus.NOISE, ChunkStatus.SURFACE and ChunkStatus.CARVERS, plus the workspace that ChunkStatus.BIOMES quietly builds before any of them. The conveyor that runs the statuses, the dependency pyramid and the threading are the chunk generation pipeline in Part IV; this is the cargo. The scalar field itself is density functions, and the labels that steer the surface pass are biomes.

The cast

classwhat it decideswhen
ChunkGeneratorthe API the statuses call — ChunkGenerator.fillFromNoise, ChunkGenerator.buildSurface, ChunkGenerator.applyCarvers. ChunkGenerators.bootstrap registers exactly three implementationsworldgen executor
NoiseGeneratorSettingsthe whole per-dimension recipe: the NoiseSettings cell dimensions, the default block and fluid, the NoiseRouter, the surface rules, the sea level, the aquifer and ore-vein switchesdata, loaded with the world
NoiseChunkthe per-chunk workspace — the wrapped router, the cell interpolators, the Aquifer, the filler chainbuilt at ChunkStatus.BIOMES, dies with the chunk
MaterialRuleListwhich filler answers first: the aquifer, then the ore veinsinside the cell loop
Aquiferwhat liquid, if any, belongs at a point — and therefore what a carver may leave behindnoise and carvers
OreVeinifiercopper or iron, from the sign of one router functioninside the cell loop
SurfaceSystemthe column re-skin: grass over dirt over stone, sand, the badlands bandsChunkStatus.SURFACE, one instance per level
WorldCarverthe shape of caves and canyons, and nothing about their contentsChunkStatus.CARVERS

Everything here runs on the worldgen executor, one task at a time per dimension. Two of the steps fan out further. ChunkGenerator.createBiomes forks to the background pool as init_biomes for every generator — the base implementation does it, so FlatLevelSource and DebugLevelSource fork too — and NoiseBasedChunkGenerator overrides it only to use the chunk’s cached sampler. The second fork, NoiseBasedChunkGenerator.fillFromNoise as wgen_fill_noise, really is the noise generator’s alone. RandomState — the per-level seed root — is built once in ChunkMap and shared by every generating chunk, and it owns the SurfaceSystem, which is therefore per level, not per chunk.

Four statuses, and what each hands on

flowchart LR
    BIO["BIOMES"] -- "the NoiseChunk, with its caches and its Aquifer" --> NOI["NOISE"]
    NOI -- "solid rock, two worldgen heightmaps" --> SUR["SURFACE"]
    SUR -- "a skin, and a preliminary surface level" --> CAR["CARVERS"]
    CAR -- "holes, and a CarvingMask" --> FEA["FEATURES"]

The odd arrow is the first one. The workspace is born one status before the terrain needs it, because the biome sampler wants the chunk’s caches too (biomes reads the climate functions through NoiseChunk.cachedClimateSampler). So NoiseChunk.forChunk runs at ChunkStatus.BIOMES, wrapping the seeded router into chunk-local caches and constructing the Aquifer and the NoiseChunk.BlockStateFiller chain, before a single block exists.

That one instance then serves all three terrain steps, which is exactly what makes the aquifer’s answers agree between filling and carving. It is heavily mutated on the way — and NoiseChunk.stopInterpolation, at the end of the noise fill, disarms it: from the surface step onward, sampling an interpolator with the NoiseChunk itself as the context throws. A caller that passes any other context is quietly served by the wrapped function instead, which is what every post-fill caller does. What survives for reuse is the aquifer’s grid cache and the preliminary surface level. It is never cleared and it is not pinned to a thread; the three steps run as separate tasks on whichever worker takes them, and what serialises them is the chunk-status future chain rather than thread affinity.

Filling the noise: six loops, one number at the bottom

The overworld’s NoiseSettings asks for a horizontal noise size of one and a vertical size of two, and NoiseSettings.getCellWidth and NoiseSettings.getCellHeight turn those into blocks by multiplying by four. So the unit of overworld terrain is a cell four blocks wide, four deep and eight tall, and a chunk is four by four by forty-eight of them.

768 — cells in one overworld chunk, each holding 128 blocks (NoiseBasedChunkGenerator.fillFromNoise).

NoiseChunk evaluates the interpolated density terms at cell corners only. Everything inside a cell is three linear interpolations away from the eight corners around it, and the walk that does this is six loops deep:

flowchart TB
    subgraph CX["for each of the 4 cell columns in X: advanceCellX fills the next corner slice, swapSlices drops the old one"]
    subgraph CZ["for each of the 4 cell rows in Z"]
    subgraph CY["for each of the 48 cells in Y, downward: selectCellYZ loads its eight corner values"]
    subgraph BY["for each of the 8 block layers in the cell, downward: updateForY"]
    subgraph BX["for each of the 4 blocks across in X: updateForX"]
    BZ["for each of the 4 blocks across in Z: updateForZ, then getInterpolatedState — one block decided"]
    end
    end
    end
    end
    end

Read the nesting as the cost model. The two outer levels are where the sampling happens — a slice of corner values is filled per cell column and dropped one column later — and everything below NoiseChunk.selectCellYZ is arithmetic on eight numbers. Counting the slices, one interpolated term is sampled five times five times forty-nine per chunk:

1,225 — corner samples per interpolated density term, per chunk (NoiseChunk.fillSlice, five slices of five by forty-nine).

The Y direction runs downward at both nesting levels, which matters because the two worldgen heightmaps are updated as blocks are written and the first non-air block seen from the top is the answer.

Two things about that lattice are worth stating plainly, because “Minecraft terrain is a lattice” is true twice over at two different resolutions. Only the terms explicitly marked interpolated come from the eight-corner lerp, and resolving every reference in the overworld router finds eight of them: one round the whole final-density subtree, four inside the noodle-cave graph, and three across the two vein terms — vein_gap is not one of them, and neither is the aquifer’s barrier, which the Aquifer samples per block. The final density is then wrapped in a cache_all_in_cell, filled for every block in the cell. Meanwhile the 2-D shaping terms, continentalness and erosion and ridges and the splines, sit behind flat_cache, which samples once per four-by-four block column group at y = 0 and reuses that for all sixteen columns. Only cache_2d is exact per column (density functions owns the caches).

The write at the bottom of the loop does not go through ChunkAccess.setBlockState. The fill calls the section setter directly with the threading check disabled and updates Heightmap.Types.OCEAN_FLOOR_WG and Heightmap.Types.WORLD_SURFACE_WG by hand, and it skips air entirely — a chunk starts empty, so only non-air is ever written. The acquire/release pair around the whole fill is a concurrent-access assertion, not a lock.

The two fillers: what the number becomes

NoiseChunk.getInterpolatedState does not read the density and compare it to zero. It runs the MaterialRuleList — a chain of NoiseChunk.BlockStateFillers — and takes the first non-null answer. There are two of them, in this order.

The aquifer. Aquifer.computeSubstance receives the final density and decides, from its own barrier and fluid-level noises sampled on a coarse grid, whether this point is stone, air, or a fluid. Aquifer.FluidPicker and Aquifer.FluidStatus are the global fallback underneath its local water tables — the sea, and the lava. Aquifer.NoiseBasedAquifer is the real implementation; a dimension with aquifers switched off gets a trivial one.

The ore veins. OreVeinifier is the second filler, active only when the settings enable it, and it is why copper and iron veins are terrain rather than decoration: they exist before the surface pass and before the carvers, and no feature places them. Which of the two OreVeinifier.VeinTypes you get is the sign of one router function, NoiseRouter.veinToggle — there is no separate “which ore” noise.

If both fillers return nothing, the block becomes the settings’ default block. And Aquifer marks the positions where it placed fluid for post-processing, so the settled water table you can see in a cross-section becomes real fluid ticks the moment the chunk is promoted — which is why “nothing flowed in” is a true statement about worldgen and not about the chunk’s first live tick (scheduled ticks).

The surface pass, and the two places it breaks its own rule

SurfaceSystem.buildSurface compiles the dimension’s NoiseGeneratorSettings.surfaceRule tree once for the whole chunk — one rule tree per dimension, which then branches on biome inside itself, then walks each of the 256 columns downward from the worldgen surface heightmap, tracking depth below stone and water height in a SurfaceRules.Context that carries its own caches. Every write is gated on the existing block still being the settings’ default block, which is what makes ore veins and aquifer water immune to being turned into grass.

Two things sit outside the rule system entirely, and neither obeys that gate. SurfaceSystem.erodedBadlandsExtension runs before the column walk and fills air with the default block to raise the terracotta pillars. SurfaceSystem.frozenOceanExtension runs after it and writes snow and packed ice over air and over water, ungated. Both are selected by biome rather than by rule, and SurfaceSystem owns the noises they need along with the ones for the badlands bands and the icebergs.

Carving, and who chooses the block

ChunkGenerator.applyCarvers does not carve the chunk it was given from the chunk it was given. It loops over a 17×17 neighbourhood of source chunks, asks each configured carver of that source chunk’s biome whether a cave or a canyon starts there, and carves whatever does into the centre chunk. That reach costs the dependency pyramid nothing: the neighbours are read only as memo holders for ChunkAccess.carverBiome, and the biome itself is recomputed from the biome source. The reach the carvers need is already paid for: six of the generation steps ask for ChunkStatus.STRUCTURE_STARTS eight chunks out, written as a bare literal — the constant ChunkStatus.MAX_STRUCTURE_DISTANCE that holds the same eight is read by nothing — and the accumulated pyramid the ticket system sizes itself against is wider still (the chunk generation pipeline).

Three carvers are registered — WorldCarver.CAVE, WorldCarver.NETHER_CAVE and WorldCarver.CANYON — each paired with a CarverConfiguration as a ConfiguredWorldCarver, reading the world through a CarvingContext and recording what they touched in a CarvingMask, the per-chunk bit set.

And then the hook. WorldCarver.getCarveState returns lava below the configured lava level, and otherwise asks Aquifer.computeSubstance with a density of zero what belongs at this point. Aquifer.FluidStatus.at answers plain air above the local water table and the fluid below it — never null; the null is Aquifer.computeSubstance’s own, and it means do not carve here at all. So the water in a flooded cave was decided by the same object that decided the water in the stone around it, and a dry cave is the carver writing air one block at a time because the aquifer told it to. What a cave may eat through is itself a data-pack decision: WorldCarver.canReplaceBlock tests the configuration’s replaceable HolderSet, which is also what stops a carver from hollowing out an ore vein it was not told about. If a grass or mycelium block was passed on the way down, the dirt below is re-skinned through SurfaceSystem.topMaterial.

NetherWorldCarver is the exception that proves the rule. It overrides WorldCarver.carveBlock, never consults the aquifer at all, and writes lava below thirty-one blocks above the dimension’s minimum and cave air above.

Questions players ask

Why is there always lava at the same depth, in every world? Because the sea level moves with the noise settings and the lava level below it does not — it is a constant in the generator, not a data-pack field.

Do the caves change if I switch the dimension to the modern random source? Not their seeding. NoiseBasedChunkGenerator.applyCarvers hardcodes a LegacyRandomSource whatever the settings say. The settings’ NoiseGeneratorSettings.useLegacyRandomSource reaches further than the level’s root random in one other way, though: it re-seeds BlendedNoise, and it changes the Y at which the surface pass samples a column’s biome. The legacy nether biome noises are not part of that — RandomState’s wiring visitor builds those two from a LegacyRandomSource whatever the setting says.

Where does the flat shelf under a village come from? From this page’s density field, not from any block edit. NoiseChunk’s constructor adds DensityFunctions.BeardifierMarker to the router’s final density itself and then swaps that exact instance for the chunk’s real Beardifier while wrapping — so every noise dimension is beardified whether or not its router JSON ever mentions a beardifier, and “structures flatten terrain” is implemented as an object comparison inside a visitor (structure placement).

What height does a structure think the ground is at, then? The one before it changes it. NoiseBasedChunkGenerator.iterateNoiseColumn, behind ChunkGenerator.getBaseHeight and ChunkGenerator.getBaseColumn, builds a throwaway one-cell NoiseChunk with an empty Blender and the bare beardifier marker, samples a column, and discards it.

What happens at the boundary with chunks generated by an older version? Blender reads BlendingData harvested from the neighbours and enters the density graph as three nodes — blending at the old-chunk border is the page for it, and for BelowZeroRetrogen, the world-deepening path that rides the same hooks: it wraps the biome resolver, patches bedrock after the noise step, and gates the spawn step.

Is any of this really running in a superflat world? Almost none of it. FlatLevelSource and DebugLevelSource implement surface, carvers and spawning as no-ops, and DebugLevelSource writes its state grid at the decoration step rather than the noise step. Development builds can switch off much more: SharedConstants carries flags that disable the surface pass, the carvers, the aquifers, the ore veins and fluid generation outright, plus visualisation modes that make the aquifer and the ore veins write marker blocks instead of real ones.

Why does a half-generated chunk have the wrong heightmaps in it? Because which two are live is a property of the status, not of the step: ChunkStatus registers every status up to and including ChunkStatus.SURFACE with the worldgen pair and ChunkStatus.CARVERS onward with the four final ones. A chunk saved mid-generation really does persist its worldgen heightmaps, and they stop being written from the carvers status on (chunk anatomy).

Where to look

ChunkGenerator · ChunkGenerators.bootstrap · NoiseBasedChunkGenerator.fillFromNoise · NoiseGeneratorSettings · NoiseSettings.getCellWidth · NoiseChunk.forChunk · NoiseChunk.fillSlice · NoiseChunk.getInterpolatedState · NoiseChunk.stopInterpolation · MaterialRuleList · Aquifer.computeSubstance · Aquifer.NoiseBasedAquifer · OreVeinifier.create · SurfaceSystem.buildSurface · SurfaceRules.RuleSource · SurfaceRules.Context · NoiseBasedChunkGenerator.applyCarvers · WorldCarver.getCarveState · WorldCarver.canReplaceBlock · CarvingContext · CarvingMask · Beardifier · Blender · BelowZeroRetrogen · Heightmap.Types · RandomState.create


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.