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

Density functions

Verified against Minecraft 26.2 · Part XII · One number out of one point: how a JSON file becomes “stone or air”, and why the graph you can read in the registry is never the graph that runs.

Open the overworld’s depth function in a data pack and you can read the shape of the world out of it in eleven lines: a gradient down the Y axis, added to overworld/offset — which is fifteen hundred lines of continents and erosion wrapped in things called flat_cache and cache_2d. Both files are honest and readable. And the object it parses into is never sampled by anything: it is rewritten once per dimension, and again per chunk, and only the third form ever computes a number for a block. The caches named in that file cache nothing. They are requests, and something else grants them.

Terrain in 26.2 is a scalar field: a function from a block position to a double, where the convention is that zero is the surface and positive means solid. DensityFunction is the interface, DensityFunctions is the library of thirty-four node types you build one out of, and a data pack assembles them as JSON. This page is the three forms that one graph takes and the machinery that moves between them; the node types themselves are the node catalogue, and the terrain steps that sample the result are terrain.

Nothing here touches a block, and nothing here differs between two worlds built from the same seed and the same packs. This is the layer that the seed actually means.

The cast

classwhat it ownsits clock
DensityFunctionone method that matters — DensityFunction.compute, taking a position and returning a double — plus DensityFunction.fillArray for the batch form and the two static bounds
DensityFunctionsthe node library, and the codecs that dispatch a JSON type to one of themdata-pack load
DensityFunction.NoiseHolderthe seeding seam: a noise-parameters holder plus a NormalNoise that is null as parsedfilled once per dimension
DensityFunctions.Markera cache request, wrapping one function and computing nothing itselfreplaced once per chunk
NoiseRouterthe fifteen functions a generator asks for, as one record; NoiseRouter.mapAll rebuilds all fifteen at once
NoiseRouterDatavanilla’s graph, written in Java and emitted as the JSON that shipsbuild time
RandomStatethe per-dimension instantiation: the seeded router, the climate sampler, the noise memo, the SurfaceSystemonce per level
NoiseChunkthe per-chunk instantiation, and simultaneously the sample position and the loop driver — it implements DensityFunction.FunctionContext and DensityFunction.ContextProvider bothonce per chunk

Underneath all of it is the synth package: NormalNoise (two PerlinNoise stacks summed and normalised), PerlinNoise (octaves of ImprovedNoise), ImprovedNoise (one octave of 3-D Perlin over a permutation table), BlendedNoise (the pre-1.18 terrain noise, itself a DensityFunction.SimpleFunction), plus SimplexNoise and PerlinSimplexNoise. Noises holds the sixty-three keys for the parameter sets and Noises.instantiate builds one from a positional factory.

Three forms of one graph

flowchart TB
    subgraph AA["as parsed — shared by every world, unseeded and cacheless"]
        direction TB
        A1["Ap2, add"] --> A2["YClampedGradient"]
        A1 --> A3["HolderHolder — a pointer at another registry entry"]
        A3 --> A4["Marker, flat cache — delegates, caches nothing"]
        A4 --> A5["NoiseHolder — noise is null, answers 0.0"]
    end
    subgraph BB["as seeded — RandomState.router, one per dimension"]
        direction TB
        B1["Ap2, add"] --> B2["YClampedGradient"]
        B1 --> B3["HolderHolder — still a pointer"]
        B3 --> B4["Marker — still delegating"]
        B4 --> B5["NoiseHolder — a real NormalNoise"]
    end
    subgraph CC["as wrapped — one per chunk, and the only form that runs"]
        direction TB
        C1["Ap2, add"] --> C2["YClampedGradient"]
        C1 --> C3["the pointed-at graph itself"]
        C3 --> C4["NoiseChunk.FlatCache — a real array, filled"]
        C4 --> C5["NoiseHolder — the same NormalNoise"]
    end
    AA -- "RandomState.create — one visitor over the whole router" --> BB
    BB -- "NoiseChunk.forChunk — a second visitor" --> CC

Both arrows are DensityFunction.mapAll, which is the only interesting operation in this system: it applies a DensityFunction.Visitor bottom-up over a whole graph, rebuilding each node’s children through DensityFunction.mapChildren. A visitor has two channels — DensityFunction.Visitor.apply for nodes and DensityFunction.Visitor.visitNoise for noise leaves — and everything below is one or other channel doing its job.

Parse: one file, one graph

Every file under a pack’s worldgen/density_function directory becomes one registry entry through DensityFunctions.DIRECT_CODEC, which is an either: a bare number in the JSON is silently a DensityFunctions.Constant, and anything else dispatches on its type id through BuiltInRegistries.DENSITY_FUNCTION_TYPE. Every child slot instead uses DensityFunction.CODEC, a RegistryFileCodec, so a string id, an inline object and a bare number are interchangeable everywhere a function is expected. A string becomes a DensityFunctions.HolderHolder — a live pointer at another entry, which is how a graph references a graph.

Two things happen during construction that a reader of the JSON cannot see. Constructors fold: DensityFunctions.TwoArgumentSimpleFunction.create collapses an add or a mul with one constant argument into a DensityFunctions.MulOrAdd, so a node type in the file is not necessarily the class in memory. And the bounds propagate: DensityFunction.minValue and DensityFunction.maxValue are computed as each node is built and pushed upward, which makes them a static analysis of the data pack — one that talks back, because building a min or a max over two ranges that cannot possibly overlap logs a warning naming both arguments.

DensityFunctions.HolderHolder is the one node that cannot be written back out: it is not registered, and asking it for its codec throws. It exists only in memory, and re-serialising a graph goes through DensityFunction.CODEC, which recognises it and emits the id string it came from.

Seed: once per dimension

RandomState.create forks the seed into named positional factories — RandomState.aquiferRandom, RandomState.oreRandom, and whatever else asks through RandomState.getOrCreateRandomFactory — and then runs NoiseRouter.mapAll with a wiring visitor over all fifteen router fields. The visitor fills each DensityFunction.NoiseHolder with a real NormalNoise from RandomState.getOrCreateNoise, rebuilds BlendedNoise with a new random source, and replaces the end-islands node with a reseeded one. Everything else it passes through untouched: the markers and the pointers survive this rewrite intact.

Two details in there matter later. The two nether climate noises are special-cased into a legacy construction over a LegacyRandomSource and therefore skip the memo entirely. And the visitor keeps its own memo of what it has already rewritten, so a subgraph referenced from five router fields is rewritten once and stays one object — which is precisely what makes the per-chunk caching in the next step pay, because five router fields that share a subgraph will share its cache.

Then a second, different visitor runs, and it strips machinery rather than installing it: it unwraps every DensityFunctions.HolderHolder to its value and every DensityFunctions.Marker to its wrapped function, over the six climate functions only, to build RandomState.sampler. That is the Climate.Sampler biomes reads — a copy of the climate half of the graph with no caches and no indirection in it at all.

For a 1.21-era reader. Three of the six climate functions have two names. NoiseRouter calls them vegetation, ridges and continents; Climate.Sampler calls the same three humidity, weirdness and continentalness. Neither vocabulary is wrong and both ship.

Wrap: once per chunk

NoiseChunk.forChunk builds the workspace and runs NoiseRouter.mapAll a second time, and NoiseChunk.wrapNew is the switch that matters. A DensityFunctions.Marker becomes the real cache its type names. A DensityFunctions.HolderHolder is resolved to its value once instead of on every sample. And three singletons are swapped by object identity: DensityFunctions.BlendAlpha and DensityFunctions.BlendOffset become two flat caches the NoiseChunk constructor has already filled, before any router mapping ran, and DensityFunctions.BeardifierMarker becomes this chunk’s Beardifier. If the level’s Blender is empty, the blend nodes survive as the constants they are and a blend_density marker is replaced by its own child, erasing the node.

Afterwards NoiseChunk adds the beardifier marker to the router’s final density itself, wraps the sum in one more cache-all-in-cell, and maps that — which is why NoiseChunk.fullNoiseDensity is not any node the data pack wrote (terrain walks the cells that sample it).

The rewrite is reversible, for the caches: all six implement DensityFunctions.MarkerOrMarked, so they still report their original marker type and would serialise back to the id they came from. The blend nodes are not reversible — DensityFunctions.BlendAlpha comes back wrapped in a flat cache it did not start inside.

The six caches, and the three a single point may use

This is the payoff of the whole arrangement, and the split inside it is not the one the names suggest.

NoiseChunk.NoiseInterpolator, NoiseChunk.CacheAllInCell and NoiseChunk.CacheOnce each begin by checking that the sampling context is the NoiseChunk itself, and delegate to the wrapped function when it is not. They are meaningful only inside the cell loop — the interpolator throws outright if sampled while the chunk is not interpolating, and the other two key on a cell index or on an interpolation counter, neither of which means anything outside it.

NoiseChunk.FlatCache and NoiseChunk.Cache2D do the opposite: they key on position alone and will happily answer a DensityFunction.SinglePointContext. NoiseChunk.Cache2D could not do otherwise — it is the one nested class here that is static, so it holds no reference to the chunk to compare against. And that is exactly what makes NoiseChunk.cachedClimateSampler and NoiseChunk.preliminarySurfaceLevel cheap: both sample the wrapped graph with single-point contexts, and both hit the two-dimensional caches every time. A single-point sample is not a cache bypass — it is a bypass of the three-dimensional caches only.

The resolutions are worth saying once. The interpolated marker sits on the expensive three-dimensional terms and is evaluated at cell corners. flat_cache is not exact per column: it fills its array by sampling at the quart corner with y = 0, so one value serves a four-by-four block group. Only cache_2d is genuinely per column, and vanilla is not consistent about where it puts one: of the twenty-four in the shipped files, eleven sit inside a flat cache and thirteen do not — and no noise_settings file contains a flat_cache at all.

Questions players ask

Does editing a cache_once in a data pack do anything? Yes, but not what it says. It is a request that NoiseChunk.wrapNew install a cache in that slot; the node itself computes nothing and delegates. Worldgen performance lives in a switch statement, not in the data.

Is the readable graph ever actually sampled? Once, and you can watch it happen. NoiseBasedChunkGenerator.addDebugScreenInfo samples RandomState.router with single-point contexts to fill the F3 noise readout — the one production path that runs the graph with every marker a no-op, and the reason the once-rewritten form has to stay safe to sample from anywhere.

Why do two identical-looking subgraphs end up sharing one cache? Because both visitors key their memo on the node itself, and the nodes are records, so two separately-parsed but structurally identical subgraphs are merged into one object. DensityFunctions.Spline makes this explicit: it has a hand-written equality that compares only the spline and ignores the derived sampler beside it, so two identical splines from two different files become one node with one cache.

Are the bounds trustworthy? Mostly, with exceptions worth knowing, and they run in both directions. DensityFunctions.Marker passes its child’s bounds through except when its type is blend_density, where it reports infinities — the one place a marker is not transparent. The two blend leaves under-report before the wrap and not after: DensityFunctions.BlendAlpha and DensityFunctions.BlendOffset parse as the constants 1 and 0, and become NoiseChunk.BlendAlpha and NoiseChunk.BlendOffset with a range of zero to one and an infinite one — so a fold decided above a blend node was decided on the wrong range. DensityFunctions.HolderHolder reports infinities while its holder is unbound, which is what lets forward references parse at all. And DensityFunction.NoiseHolder answers a maximum of 2.0 while its noise is still null, where every one of the sixty-three shipped noise definitions comes out between 2.57 and 7.32 once seeded — so the freshly parsed graph reports noise bounds that are too narrow, and seeding widens them.

Is there anything in here that does not work? Three things. DensityFunctions.TransformerWithContext is the shape a position-dependent transform would take and has no implementation in 26.2. Density writes down the three conventions this whole system rests on — surface at zero, and the two values a node reaches for when it wants to end an argument — as constants that nothing anywhere reads; the routers spell the same numbers as literals. And DensityFunctions.shift, the three-dimensional domain warp, has no callers and appears in no shipped file: vanilla uses only the two two-dimensional warps, and those two read the same noise parameters with their axes swapped, behind a registry id that is called offset rather than shift.

Which of these nodes reads the world? Two, and both do it the same way — by harvesting what they need from neighbouring chunks at construction and never touching a chunk afterwards. The three blend nodes reach BlendingData (blending at the old-chunk border); the beardifier marker becomes a Beardifier, whose Beardifier.forStructuresInChunk reads the structure references out of chunks at ChunkStatus.STRUCTURE_REFERENCES (structure placement). Everything else — every noise, spline, selector and cache in the catalogue — reads no blocks, no chunks and no level, which is why the whole system can run on a worldgen worker with nothing loaded.

Where to look

DensityFunction.compute · DensityFunction.mapAll · DensityFunction.Visitor · DensityFunction.NoiseHolder · DensityFunctions.DIRECT_CODEC · DensityFunctions.Marker · DensityFunctions.MarkerOrMarked · DensityFunctions.HolderHolder · DensityFunctions.TwoArgumentSimpleFunction · NoiseRouter · NoiseRouterData.overworld · RandomState.create · RandomState.getOrCreateNoise · NoiseChunk.forChunk · NoiseChunk.wrapNew · NoiseChunk.NoiseInterpolator · NoiseChunk.Cache2D · NoiseChunk.cachedClimateSampler · Climate.Sampler · NormalNoise.create · ImprovedNoise.noise · Noises.instantiate · Density


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