Real geography

How real land is made, and how a game can make it the same way, in the same order: plates and uplift raise the land, rivers cut it down, water fills what’s left, wind and height set the climate, the climate sets the biomes, and people settle, walk and clear the land last. Every serious whole-map generator, from Dwarf Fortress to mapgen4, converges on one cheap backbone. Priority-flood fills depressions and orders every cell in O(n log n), flow accumulation is one reverse pass, an implicit stream-power solve carves the valleys, and one sorted sweep along the wind makes a rain shadow. At 256×256 the whole pipeline takes about half a second of plain JS on a laptop, so a seeded quest-sized world can be generated on a phone in a worker. Only full erosion to equilibrium has to be precomputed or moved onto the GPU. Settlements, paths and trees then go where real ones would, by scoring sites (Emilien 2012), costing every step (Tobler, Galin) and reading habitat (Cordonnier, Merryfolk): that’s the next page.

Proposed: none of this is in Tycho yet. The demos run here in the page, written in plain JS with the study’s own kit (demos/kit.js), not Tycho; each says how it would run in Tycho: per frame (the GPU stand-in’s calls), pre-bake (painted once offscreen), or shader (per-pixel work every frame). What would move into Tycho, and when, is in the roadmap.

Text marked (inference) is reasoning from the sources, not a sourced fact. Times marked “bench” were measured for the notes with world_generation_bench.js: Node v26.8.1 on a 2019 Intel i9-9880H laptop at 2.3 GHz, one thread, typed arrays, 8-neighbor grids. No phone was measured.

A world made the way the land is. Plates collide and raise ranges, rivers carve them down, and the wind leaves deserts behind the mountains, built in stages you watch: the tabs show the height before erosion, the plates, erosion, water, temperature, rain, biomes, soil and the final map. Villages settle by water on sunny, sheltered flats, and roads join them along the easiest ground. The Rock tab shows the rock the plates left: basalt, granite, and a folded layer cake of limestone, sandstone and shale. Tap anywhere to walk into that place in 3/4 view: cliffs banded with the beds they cut, waterfalls, a village shaped by its site, trees by habitat with blended edges, and under Underground the caves that rock makes: karst passages draining to springs, lava tubes, sea caves. Show scores heats the village sites. Drag and pinch to move round a region; New seed makes another world.

The demo’s limits, plainly:

Landmass and plates

World generators come in two families:

Family Examples How What it gives
Field and threshold Minecraft 1.18+, Terrafirmacraft’s layers, Vintage Story’s strata Noise and splines evaluated per point, with no global pass Worlds that stream forever, but no real drainage: Minecraft’s “rivers” are just the valleys band of a noise
Whole-map simulator Dwarf Fortress, O’Leary, Azgaar, mapgen4, WorldEngine, Undiscovered Worlds A bounded map, then global passes: depression filling, flow accumulation, wind sweeps Rivers that join and reach the sea

A bounded phone world, such as a quest’s island, belongs in the second family (inference). Almost all of them fake erosion cheaply, and the physically based tools (WorldEngine’s plate simulation) are slow and run offline.

Dwarf Fortress, stage by stage

Dwarf Fortress doesn’t paint biomes. It generates six coarse fields, fills them fractally, derives biomes from thresholds, carves rivers, recomputes the climate from the final terrain, and throws the world away if it fails its checks. Tarn Adams gives the order (Gamasutra Q&A, quoted on the DF wiki), and this page follows it:

  1. Fields. Allocate the map and choose the poles. “The basic map field values (elevation, rainfall, temperature, drainage, volcanism, wildness) are seeded along a grid of variable size, respecting various settings (oceans, island sizes, other variances, etc.), and then filled in fractally.” The poles vary temperature, and points are picked for the highest peaks. A first check “attempts to adjust some altitudes to fit the map within the desired parameters if it missed. The world can be rejected at this point if it is unfixable.”
  2. First biomes. Vegetation, the first derived field, is set from elevation, rainfall and temperature, and the biomes are tested. “The mid-level elevations are smoothed at this point to make more plains areas, and volcanoes are placed respecting the hot spots in the volcanism field.”
  3. Erosion and rivers. “Small oceans are dried out, and it locates edges of mountain sides where it can run test rivers … Many fake rivers flow downward from these points, carving channels in the elevation field if they can’t find a path to the sea. Extreme elevations differences are often smoothed here so that everything isn’t canyons. Ideally we’d use mineral types for that, but we don’t yet. Lakes are grown out at several points along the rivers.”
  4. Climate again. “Elevations are smoothed again from the mountains down to the sea … Now that the elevations are finalized, it makes adjustments to rainfall based on rain shadows and orographic precipitation. Temperatures are reset based on elevation and rainfall and the dampening effects of forests, and it uses the new values to set the vegetation level one final time. Salinity values are set for the ocean and tiles neighboring the ocean.”
  5. Regions, rock and history. Biome regions are found and named; geological and underground layers are added (“though the geological stuff should really be earlier”); “a final verification process against the parameters” runs; wildlife and weather are seeded. Then “Civilizations and caves are placed … there’s a giant zero-player strategy game going on with somewhat loose turn rules and bad AI (but thousands of agents), and history is just a record of that.”

The exact date of the Q&A couldn’t be confirmed; the wiki cites gamasutra.com/view/news/343859. In 2008 Adams told John Harris that elevation came from midpoint displacement, temperature was biased by elevation and latitude, drainage was a fractal 0–100 field, and salinity was 100 at the oceans, tapering quickly over land. He noted that midpoint displacement leaves artifacts, which erosion later addresses (Game Developer, 2008). A 2014 Polygon account, paraphrased by Isegoria, adds a separate fractal for mineral strata, rivers carving valleys “only after biomes exist”, a salinity pass for deltas and mangroves, and region names from four cultures (Isegoria, 2014).

The order is the part worth copying. The climate is computed twice: once roughly to pick and test biomes, and again after erosion, so rain shadows follow the final mountains (inference).

The grid is tiny. Worlds are 17, 33, 65, 129 or 257 squares a side (Pocket to Large); 257×257 is 66,049 squares. Detail comes only at embark, where each square expands to 16×16 local blocks of 48×48 tiles. In the wiki’s words, “the essential factor for the gen duration is the history”, not the map (DF wiki, Advanced world generation). Each field is a token [FIELD:min:max:xVariance:yVariance]:

Field Example Range and notes
Elevation ELEVATION:1:400:401:401 0–400
Rainfall RAINFALL:0:100:200:200  
Temperature TEMPERATURE:25:75:200:200 −1000 to 1000
Drainage DRAINAGE:0:100:200:200  
Volcanism VOLCANISM:1:100:200:200  
Savagery SAVAGERY:1:100:200:200  

Variance runs 0–3200 and sets how wildly a value can differ between adjacent squares, per axis. Setting only one axis high gives bands; both at 0 give a single value everywhere. Under the noise sits a mesh of “weighted ranges”, [ELEVATION_FREQUENCY:mesh:w0-20:w20-40:w40-60:w60-80:w80-100], for example ELEVATION_FREQUENCY:2:1:2:3:4:5. Mesh 1 means ignore, 2 is 2×2, up to 6 for 32×32, limited by world size; the finest mesh always makes grid cells of 8×8 world squares. The steps are: make the grid, set its intersections to random values drawn from the five weighted bands, “smooth out the area between the intersection points”, then “add noise according to the variance parameters”. A large enough variance swamps the mesh, and the effect depends on the mesh count, not the cell size. The poles are POLE:NONE|NORTH|SOUTH|NORTH_AND_SOUTH|NORTH_OR_SOUTH|NORTH_AND_OR_SOUTH, with the equator at the opposite edge or the middle; latitude can shift temperature by more than 75 °C (the wiki marks the exact values “unsure”) (DF wiki). No primary source gives DF’s exact fractal routine, roughness falloff or river-dig depth.

Other ways to raise land

Generator Cells Base height
Minecraft 1.18+ Every block A 3D density per block (> 0 is solid), squeezed with altitude by a height bias. Continentalness, erosion and peaks-and-valleys (PV) map through splines to a height offset and a vertical stretch factor, and the same noises pick the biome, so terrain and biome agree. PV = 1 − |3·|weirdness| − 2|: valleys below −0.85, low below −0.2, mid below 0.2, high below 0.7, then peaks (Minecraft Wiki)
O’Leary, “Generating fantasy maps” 16,384 points, one round of Lloyd relaxation, a Voronoi mesh A sum of primitives (a random tilted slope, a cone, 50 random “mountain” blobs), 10 rounds of neighbor averaging, then “peaky” square-root sharpening (mewo2/terrain). The essay page returned 404 when fetched; the source is the record
Azgaar’s Fantasy Map Generator A jittered lattice (jitter 0–0.9, default 0.9); the FAQ says “set Points number to 10K” for performance, and several constants scale by (points/10000)^0.25 Templates of blob primitives (Hill, Pit, Range, Trough, Strait, Add, Multiply, Smooth, Invert), each with a count, a height (0–100, with 20 as sea level) and x and y placement ranges in percent (Advanced options; Knowledge Base; template editor)
mapgen4 (Amit Patel, 2018) Precomputed points loaded from build/points-5.5.data: Poisson disc at spacing 5.5 in a 1000×1000 square, plus mountain peaks at spacing 35, triangulated by Delaunator. The code supports 1 million+ cells (spacing 0.7), but “the rendering code and other parameters are designed to look prettiest around 25k cells” A painted constraint map (bilinear) plus coastline noise. On land, low simplex “hills” (there to make rivers meander) mix with “mountains” from a jittered BFS distance-from-peak field, em = 1 − slope/sharpness·distance, weighted by e². Six simplex octaves are precomputed per triangle once per seed (mesh.ts; config.js; README; map.ts)

Minecraft’s spline trick is the cheapest good-looking idea here, and it suits a phone. Three low-frequency 2D noises through hand-tuned piecewise-linear splines give coasts, plains and jagged peaks for a few noise samples per cell, and the same values pick biomes. It gives no connected rivers, so pair it with one priority-flood pass (inference). The exact spline control points live in the game’s noise_settings data and weren’t extracted. Henrik Kniberg’s talk on the 1.18 generator is “Minecraft terrain generation in a nutshell” (6 Feb 2022) (YouTube).

Island masks push the edges down. Red Blob gives d = 1 − (1 − nx²)(1 − ny²) (a square bump) or d = min(1, (nx² + ny²)/√2), then e = lerp(e, 1 − d, mix) (Red Blob, making maps with noise). mapgen4 adds coastline noise weighted by (1 − e⁴), so it acts mostly near the shore (map.ts).

Plates

Approach How Cost
WorldEngine Elevation from pyplatec, a C plate-tectonics simulation (defaults: 10 plates, 2 cycles, erosion_period 60, folding_ratio 0.02, aggr_overlap_rel 0.33, sea_level 0.65); then 8-octave simplex; the land centered by rolling the map to put the least land at the borders; borders faded to ocean over min(30, size/5) cells; the shallow ocean floor flattened. Hills and mountains are the top 10% and 3% of heights by quantile (plates.py; generation.py) The README warns generation “could take a few minutes” (WorldEngine)
Frozen Fractal, “Around the World” (2023) Plates grown from continent Voronoi regions by simultaneous flood fill (about 15 plates for 6 continents), with random velocities. Boundaries are classed only as converging or diverging (transforms dropped), and boundary type × crust type picks a noisy profile curve: mountains (continent meets continent, taller with speed), rift valleys, mid-ocean ridges, island arcs, trenches with volcanic arcs (Frozen Fractal, 2023) A flood fill and a profile lookup per boundary cell
Undiscovered Worlds (Jonathan Hill) Global maps of 512×257, 1024×513 or 2048×1025 pixels at “roughly 16km” per pixel; regional maps at about 1 km per pixel on demand; “tectonic” (Earthlike continents and ranges) or “non-tectonic” terrain; mountain ranges a separate layer from elevation; monthly temperature and precipitation per point (blog; GitHub) A perfect climate simulation “would require a lot more processing power and time than I have available”; “Rivers and lakes occasionally go haywire.” In December 2025, rivers and lakes were “more or less” working, rift lakes had a simpler method, and hydraulic erosion was only being considered (blog, Dec 2025, via search summary)

Cordonnier’s method, next, skips plates: the user paints the uplift directly. WorldEngine’s platec internals (Viitanen’s thesis) and Undiscovered Worlds’ algorithms weren’t read.

Uplift and erosion

The stream-power law

Land is raised by uplift and cut by rivers in proportion to how much water drains through a point and how steep it is. Cordonnier et al. write it as dh/dt = u(p) − k·A(p)^m·s(p)^n: uplift minus fluvial erosion, with A the drainage area and s the slope. “As in most geomorphological studies, we use n = 1 and m = 0.5” (m/n ≈ 0.5 is constrained by stream profiles) (Cordonnier et al. 2016).

Their method places Poisson-disc nodes on a Delaunay graph, and the user paints an uplift map. Each step builds stream trees, with each node’s receiver its steepest downhill neighbor. Then it sums Voronoi cell areas from the leaves to the roots for the drainage area, and solves the implicit update

h_i(t+δt) = (h_i(t) + δt·(u_i + k·A_i^m·h_j(t+δt)/‖p_i − p_j‖)) / (1 + k·A_i^m·δt/‖p_i − p_j‖)

which needs the receiver j solved first, so nodes are visited in stream-tree order. Lakes (internal roots) are joined by a lake graph in O(N + M log M), with M lakes ≪ N, instead of Braun and Willett’s optional O(N√N) step. Braun and Willett 2013, the O(n) implicit solver itself, wasn’t fetched; its use and complexity are cited through Cordonnier.

Their parameters: a 50×50 km terrain, maximum uplift U = 5.0e-4 m/yr, k = 5.61e-7 /yr (for peaks of about 2000 m), δt = 2.5e5 years. “Convergence is obtained after around 200 time steps”, and thanks to the implicit solve the step count “does not depend on the resolution”. Timings on a 3 GHz Core i7, single thread, which they “did not fully optimize” (§6 and Table 1):

Samples Per step To converge
10,000 0.031 s 6.4 s
40,000 0.177 s 35.4 s
90,000 0.401 s 78 s
160,000 1.273 s 252 s

In plain JS the same implicit solve (flood, accumulate, solve) costs 31 / 118 ms for the first step at 256² / 512², and 289 / 1015 ms for 20 steps, about 14 / 50 ms per step (bench).

Stream power for the GPU

Schott, Paris, Fournier, Guérin and Galin (ACM TOG 2023) made stream power an authoring tool. The user paints uplift, fluvial erosion is “continuously simulated through the Stream Power Equation”, and “a fast yet accurate approximation of drainage area and flow routing” keeps it interactive, with copy-paste, warping for folds and faults, and point and curve elevation constraints (project page; StreamPowerErosion). The released code is an OpenGL 4.5 compute shader (8×8 work groups, ping-pong buffers). Per cell, per step (spe_shader.glsl):

Parameter uplift k k_d k_h p_sa (m) p_sl (n) dt p_flow
Default 0.03 0.0005 10 200 0.8 2.0 50 1

The paper’s own GPU timings couldn’t be retrieved (HAL blocked the fetch; ACM is paywalled). Ported to a CPU loop, the kernel costs 39 / 129 ms per step at 256² / 512², and 2.8 / 10.5 s for 100 steps (bench). That is about twice the implicit solver per step, and it needs at least as many steps as the grid is wide before drainage area reaches the outlets. So it fits Tycho only on Tycho.gpu: each cell reads only its 8 neighbors, which maps onto a fragment-shader ping-pong, and with float render targets thousands of steps per second at 256² are plausible (inference, not measured).

Cheap erosion, as games do it

Generator Erosion
O’Leary rate = min(200, 1000·√flux·slope + slope²), river incision plus creep, normalized to the maximum and scaled by a random amount from 0 to 0.1; run 5 times, filling sinks between (mewo2/terrain)
Azgaar An optional one-pass downcut: cells above height 35 are lowered by floor(flux / mean flux of higher neighbors), at most 5 (river-generator.ts)
mapgen4 None: where a trunk river is higher than its tributary, its elevation is lowered to match (map.ts)
Dwarf Fortress Fake rivers dig when stuck. EROSION_CYCLE_COUNT:250 (0–1000; higher is less jagged with wider rivers, and the maximum dissolves mountains into plains and causes rejections); RIVER_MINS:200:400 (pre-erosion minimum and post-erosion desired river starts, 0–800; “Extremely high pre-erosion values speed erosion greatly”); PERIODICALLY_ERODE_EXTREMES:1 turns impassable cliffs into ramps (DF wiki)

DF’s “fake rivers dig when stuck” is an ad hoc stand-in for depression filling, which priority-flood does deterministically in one pass (inference).

Droplets

Droplet erosion is the detail pass: gullies and sediment fans. In Beyer’s 2015 TU München bachelor’s thesis, each drop carries a direction, speed, water and sediment, reading height and gradient by bilinear interpolation. Its capacity is max(−Δh, minSlope)·speed·water·p_capacity. Under capacity it erodes over a radius; over capacity it deposits bilinearly at its current point, with no radius, so pits fill. The change map can be blurred and blended by a factor b (Beyer 2015). His timings, on a 3.2 GHz Core i5-3470 with inertia 0.3, capacity 8, deposition 0.2, erosion 0.7, evaporation 0.02, minSlope 0.01, gravity 10, radius 4 and maxPath 64:

Map Drops Radius Time
256×256 10,000 4 2.27 s
256×256 100,000 4 32 s
2048×2048 100,000 4 43.8 s
256×256 10,000 1 / 2 / 4 / 6 / 8 0.48 / 0.83 / 2.27 / 5.51 / 9.69 s

Time barely depends on map size but grows with radius². About 16,500 of 100,000 drops ran off a 256×256 map.

Implementation Defaults
Sebastian Lague’s Unity port Map 255×255, 50,000 droplets, brush radius 3, inertia 0.05, sedimentCapacityFactor 4 (3 in the generator), minSedimentCapacity 0.01, erodeSpeed 0.3, depositSpeed 0.3, evaporateSpeed 0.01, gravity 4, maxDropletLifetime 30, initial water 1 and speed 1; brush weights precomputed per cell; a compute-shader version too (SebLague/Hydraulic-Erosion)
Erodr (C, Beyer’s algorithm) 70,000 particles, ttl 32, radius 2, gravity 1, inertia 0.3, capacity 8, evaporation 0.02, erosion 0.7, deposition 0.3, min slope 0.0001 (henrikglass/erodr)

In JS, 0.5 droplets per cell with Lague’s defaults and a radius-3 brush (33k / 131k droplets) costs 128 / 445 ms at 256² / 512², about 4 µs a droplet (bench). That is about two orders of magnitude faster than Beyer’s table, and the gap is implementation (precomputed brush offsets and weights, typed arrays, a shorter life) more than language. So budget droplets by droplets × life × brush cells (inference).

Thermal erosion

Thermal erosion moves material from a cell to lower neighbors while the height difference exceeds the talus angle, the steepest stable slope for that material, which evens out sharp changes. Olsen (2004) described speed-ups for real-time game maps (Smelik et al. 2009). The Olsen constants quoted across blog posts (talus T = 4/N for an N-cell map, move half the excess, about 50 iterations) are folklore here: they couldn’t be checked against the paper, and the bench uses that form with a hand-picked T. Fifty iterations with a 4-neighbor talus cost 86 / 318 ms at 256² / 512² (bench). Schott’s shader folds hillslope processes into the stream-power step as Laplacian diffusion (k_h) plus a debris term (k_d), a cheaper continuous stand-in for talus moves.

The order for a phone

On a CPU-only phone build the order of work is (inference):

  1. A noise or spline base.
  2. Priority-flood.
  3. 10 to 40 implicit stream-power steps, at a δt large enough to converge fast.
  4. 0.25 to 0.5 droplets per cell for gullies.
  5. Thermal or Laplacian smoothing for talus.
  6. The wind sweep, then the biome lookup.

Cordonnier’s 200 steps to steady state are about 3 s at 256² in JS, too slow for a phone. Start instead from terrain already near equilibrium (noise shaped like ridges), or run on a coarse 128² grid, at roughly a quarter of the cost, and upsample (inference).

Water: rivers, lakes and oceans

Priority-flood

Barnes, Lehman and Mulla’s priority-flood fills depressions and is “optimal for both integer and floating-point data, working in O(n) and O(n lg n) time, respectively”. An improved variant, with a plain FIFO queue inside depressions and a priority queue elsewhere, is O(m lg m) with m ≤ n and ran “up to 37% faster”; a parallel version of an older algorithm needed six processors to match it on one. It “can also be adapted to label watersheds and determine flow directions”, with epsilon increments or by carving (Barnes et al. 2014).

mapgen4 uses exactly this shape on its triangles: a priority queue seeded with ocean walks uphill in elevation order, which gives every triangle a downslope side and a topological order (map.ts). O’Leary used iterative Planchon–Darboux filling instead, repeating full sweeps until nothing changes with epsilon 1e-5, and Azgaar iterates lake-raising passes. Both are cheaper to write but have no tight bound (mewo2/terrain; river-generator.ts). Merryfolk runs it on its tile map: the heap is seeded with the river, the pond and the map border, every cell filled to at least its parent + 1e-5, then each cell sends its flow to its lowest neighbor in reverse flood order (Merryfolk terrain.js).

Kernel (bench) 256² 512²
Priority-flood with epsilon fill and a binary heap, also recording receivers and flood order 15.8 ms 55.9 ms
Flow accumulation in reverse flood order 1.2 ms 2.7 ms
D8 steepest receivers plus sort-by-height accumulation (the textbook way) 25.1 ms 99.8 ms

The flood order makes accumulation one O(n) pass: 1.2 ms against 25 ms for the sort-based way at 256².

Rivers

Generator Rivers
mapgen4 Flow = flow_param·moisture² per land triangle, summed in reverse flood order; trunk beds lowered to their tributaries (map.ts)
Azgaar Land cells sorted high to low accumulate precipitation flux; a river forms at flux ≥ 30, navigable at ≥ 100 (river-generator.ts)
WorldEngine Rivers from flow sources above RIVER_TH = 0.02; rivers erode; sinks with no path to the sea become lakes (simulations)
Dwarf Fortress (2008) Pick mountain bases (squares above an elevation), run temporary paths downhill, digging when stuck, until they reach the ocean; run real rivers along the best channels, force paths for those that fail, bulge out some lakes, apply loop-erasure; flow amounts decide tributaries; rivers are named; invisible brooks are added (Game Developer, 2008). Rivers are placed only if the elevation maximum is at least 104 (DF wiki)
Minecraft No flow at all: rivers occur where PV is “Valleys” at low continentalness or high erosion (Minecraft Wiki)

Lakes

With priority-flood, a lake’s depth is just the filled height minus the original height, and the fill surface is the lake level. Cordonnier treats lakes as internal root nodes and routes their overflow through the lowest saddle (Cordonnier et al. 2016 §4.3).

Closed (endorheic) lakes come from a water balance. Azgaar resolves depressions iteratively, raising lakes to their lowest shore + 0.2 and closing deep ones as endorheic after 75–85% of the allowed iterations. A lake drains through its lowest shore cell only if inflow (shoreline precipitation plus rivers) exceeds evaporation, estimated by a Penman-like formula of temperature and altitude. Lake subtypes are freshwater, salt, dry, sinkhole, frozen and lava (lakes.ts; river-generator.ts; Geographical features). Dwarf Fortress grows lakes at several points along its rivers and bulges some out.

Oceans and coasts

Climate and biomes

Temperature

Azgaar sets sea-level temperature by latitude, with tropics from 16° N to 20° S at 0.15 °C per degree and a linear fall to the poles, then subtracts 6.5 °C per km of altitude (temperature-generator.ts). Cordonnier et al. use the same lapse rate, θ(p) = θ̄ − 0.0065·elevation in meters (Cordonnier et al. 2017). DF resets temperature after erosion from elevation, rainfall and “the dampening effects of forests”. For a quest-sized island, one prevailing wind plus a 6.5 °C/km lapse rate is enough (inference).

Wind, rain and the rain shadow

The rain is one sorted sweep along the wind. Humidity is carried from upwind neighbors, gained over water, and capped by height or lost on climbing slopes as orographic rain, so the lee side is dry.

One west-to-east sweep with an orographic cap costs 1.4 / 2.4 ms at 256² / 512² (bench).

Biomes

Every biome table is a lookup on two or more climate axes. Red Blob’s Whittaker-style table works on elevation e and moisture m (Red Blob):

Elevation Biome by moisture
e < 0.1 Ocean
e < 0.12 Beach
e > 0.8 Scorched, bare, tundra, snow, at m < 0.1, < 0.2, < 0.5, else
e > 0.6 Temperate desert, shrubland, taiga, at m < 0.33, < 0.66, else
e > 0.3 Temperate desert, grassland, temperate deciduous forest, temperate rain forest, at m < 0.16, < 0.5, < 0.83, else
Otherwise Subtropical desert, grassland, tropical seasonal forest, tropical rain forest, at m < 0.16, < 0.33, < 0.66, else

Patel: “every project I’ve worked on I’ve had to change” these numbers.

The others add temperature as an axis:

Dwarf Fortress’s thresholds, on its 0–400 elevation and 0–100 fields (the wiki tags the biome rows “verify”) (DF wiki):

Biome Rule
Ocean, land, mountains Elevation 0–99, 100–299, 300–400; peaks only at 400
Volcano Volcanism 100
Swamp Rain 33–100, drainage 0–32, not freezing
Desert and badland Rain 0–9: drainage 0–32 sand desert, 33–49 and 50–65 rocky wasteland, 66–100 badlands
Forest Rain 66–100, drainage 66–100 (as listed)
Grassland Rain 0–66, drainage 0–66
Hills Rain 0–66, drainage 66–100
Tundra Freezing, drainage 0–66
Glacier Freezing, drainage about 80–100

High drainage is “important for forming hills”; low drainage makes lakes, rivers, swamps and reportedly thicker soil. Adams’s 2008 rule for swamps, where “rainfall is >=66/100 and drainage is less than 50”, differs from the wiki’s row.

Caves and rock

Caves

Method How Cost
Minecraft noise caves Part of base terrain. Cheese caves are the “white” region of a 3D noise (hollowness sets the size, with pillars inside); spaghetti caves are the thin band where a noise crosses zero (“the edge of black and white part … becomes air”); noodle caves the same, thinner, 1 to 5 blocks wide. Frequency, hollowness and thickness noises control them. Old-style “carvers” still cut caves and canyons afterward (Minecraft Wiki, Cave) Evaluated per block with the terrain density; no global pass
Cellular automata (Johnson, Yannakakis and Togelius, FDG 2010) A 50×50 grid, initial rock ratio r = 0.5, n = 4 iterations, Moore neighborhood M = 1, rock if the neighborhood value ≥ T = 5 (the paper’s wording: greater than T); cost ∝ (2M + 1)² (Johnson et al. 2010) 0.41 ms per 50×50 map in C#/XNA; a 3×3 grid of joined rooms (2 extra iterations on the joined borders) in 349 ms. In JS, 45% fill and 5 passes of the 4-5 rule cost 23 / 54 ms at 256² / 512² (bench)
Karst networks (Paris, Guérin, Peytavie, Collon and Galin, CGF 2021) Skeletons by a gridless anisotropic shortest path from inlets to outlets, steered by faults, inception horizons, fractures and permeability contrasts; conduits as a signed-distance construction tree with blending and warping. A related tool, KarstNSim, reports sub-vertical conduits above the water table and sub-horizontal ones below it (Paris et al. 2021; the water-table detail via search summary) Offline
Dwarf Fortress caverns CAVERN_LAYER_COUNT:3, each layer with openness 0–100 (open expanses against passages), passage density 0–100 (a maze of passages) and water 0–100, plus magma sea and bottom-layer toggles; MOUNTAIN_CAVE_MIN:25 and NON_MOUNTAIN_CAVE_MIN:50 reject worlds without enough cave entrances (DF wiki) Part of world generation
Minecraft’s aquifers decide what fills every empty block. The world is cut into cells of 16×12×16 blocks, each with one jittered center that has a fluid level and type, and a block takes the nearest center’s status, a 3D Voronoi, so levels step at cell boundaries, with stone barriers between differing neighbors. Partly flooded levels cluster near Y = 40·floor(Y/40) + 20 ± 10. Lava appears where the level is at or below Y = −10 and lava noise > 0.3, sampled per 64×40×64 blocks. Under the sea floor, cells are almost always flooded to sea level; inland, mostly dry. There are no aquifers where erosion < −0.225 and depth > 0.9, the deep dark (Minecraft Wiki, Cave).

Karst and lava tubes have no game-ready algorithm in the sources; the PLUME framework (arXiv 2508.20926) targets planetary shelters, not games. The hydrogeology tools above do have one at their core, a shortest-path tree from springs to sinkholes, and it turns out cheap enough to run per region: Caves made by the rock below.

Rock

Province-driven strata or chained layer tables cost nothing at generation time, a lookup per column. They matter only if a world shows cliffs, mines or caves (inference).

The demo’s rock is a lookup of that kind, driven by the plates. During the plates stage a volcanic field gathers island arcs, continental arcs (at 45%), rift floors (four times, as flood basalt), mid-ocean ridges and hotspot shields, plus any land on an oceanic plate far from a continent; above 0.45 it’s basalt. Granite goes where uplift was fast enough to strip a range to its core, or where old ranges are worn to their roots. Everywhere else is a layer cake of sedimentary beds 150 m thick, each bed’s rock drawn from a hash of its index (42% limestone, 30% sandstone, 28% shale), folded by a slow noise of ±0.45 km. So the rock at a point depends on its height, and a valley cut deep enough reaches the next bed down. A region’s cliffs are banded with the beds they cut, and the Rock tab marks karst where limestone gets over 45 cm of rain. It costs about 3 ms for the world map (measured).

Caves made by the rock

Real caves come from rock and water, not from noise. Most are dissolved in limestone along joints and bedding planes, at and just below the water table, and the recharge decides the pattern (Palmer 1991, abstract). Sinkholes feeding water to points make branchwork, tributaries joining downstream into fewer, larger passages: about 57% of Palmer’s roughly 500 caves, and 72 to 84% of 633 Missouri caves. Diffuse recharge, as through a permeable caprock, opens every fracture at once into a network maze (17%); floodwater backing up from a river braids anastomotic loops (3%) (Palmer 1991; Palmer 1975; Dom and Wicks 2003, the shares via search summary). Cave levels record old stands of the base-level river: as the valley deepens, the old level drains and dries, and only then can speleothems grow, since calcite comes out of water losing CO₂ to the air (NPS). Lava tubes are rivers of lava under their own crust, 1 to 15 m down, following the flow downslope, open through skylights where the roof fell in (USGS; Wikipedia). Sea caves are cut by waves along weak zones in cliffs at sea level (Wikipedia). The full sourcing is in caves_villages_edges.md.

Games don’t do any of this. Dwarf Fortress’s cavern layers are world-wide horizontal bands with openness and passage-density sliders; Minecraft’s cheese, spaghetti and noodle caves are thresholds and zero-crossings of 3D noise, its carver caves Perlin worms; Terraria runs random-walk “TileRunner” passes; Noita fills its biomes with herringbone Wang tiles and rejects layouts a pathfinder can’t cross; Spelunky lays a guaranteed path through a 4×4 grid of room templates (DF wiki; Minecraft Wiki; Terraria wiki; Noita wiki; Kazemi). Only the hydrogeology tools grow passages from inlets to outlets, Paris et al. by an anisotropic shortest path steered by fractures and inception horizons, KarstNSim and pyKasso by fast marching (Paris et al. 2021; HAL, via search summary).

That core is a single Dijkstra, and on a 64×48 region it’s nearly free. The demo:

Cave Where How
Karst, active level Limestone at the water’s own level within two tiles of a river, lake or the sea, under at least a terrace of ground: up to six springs, 7 tiles apart Joints in two sets at right angles, square to the plate’s drift, 4 or 5 tiles apart and wavering. A multi-source Dijkstra grows from the springs through limestone at each spring’s level, 1 along a joint and 2.8 through solid rock. Up to nine sinkholes (limestone at the surface, dry, open, 6 tiles apart, wetter and flatter first) trace back to their springs, so the passages join downstream: branchwork. A passage carrying three or more sinkholes’ water is two tiles wide. Streams run along them to the springs, which issue at the foot of a cliff or well up on the bank
Karst patterns Under a sandstone bed just above the passage level; near a spring on a big river or a lake Every joint within 3 tiles of a capped passage opens (a network maze, from diffuse recharge). Near a flooding river, side passages braid off the last tiles before the spring and rejoin (anastomotic): a fresh Dijkstra between two points of the trunk with a random cost that avoids the trunk
Karst, old level Two to four terraces above the springs (about 70 m), or without springs the limestone level most cut by valley walls The same growth, from entrances where a valley wall cuts the level; dry, with speleothems; mouths open in the cliff face at the level’s own height when the wall faces the viewer
Lava tubes Regions over a fifth basalt From the highest basalt (and a second vent 18 tiles off), a tube runs down the smooth world slope with momentum and a little wander, a terrace under the surface, never climbing; a skylight every nine tiles
Sea caves Where the sea meets a cliff two or more terraces high in rock other than shale Two to four tiles inland at sea level, up to four, the mouth at the waterline
Rock shelters The foot of a sandstone cliff three or more terraces high A one-tile alcove, up to two

Limestone dissolves only where there’s water to do it, so a region averaging under 25 cm of rain gets no karst. Each passage tile keeps its level, so the underground cutaway draws it at its own depth, its back wall in the rock of the bed above. A region with none says so (“no caves in its granite”). How common caves are depends on the world: in seed 4242, whose interior is mostly limestone, every village’s region had karst; in seed 777, mostly shale and sandstone, five of sixteen did, one had a lava tube, two sea caves, and one no cave at all. With the caves, a region still builds in 0.09 to 0.42 s, against 0.16 to 0.35 s before (measured). Not done: spongework and ramiform caves, shafts below the sinkholes, flooded passages below the spring level, talus and glacier caves. A cave level crossing a region edge doesn’t continue into the next region, which would need the springs and sinkholes chosen per world cell instead of per region (inference).

What fits on a phone, and what to precompute

On a 256×256 grid everything but long erosion runs costs tens of milliseconds in plain JS: base noise, priority-flood, flow accumulation, rivers, lakes, wind and rain shadow, biomes, CA caves. The whole pipeline, with about 20 implicit stream-power steps and a light droplet pass, takes about 0.5 s on a laptop, roughly 1 to 2 s on a phone in a worker. Full stream-power equilibrium (about 200 steps) is the one stage that doesn't fit at 512² on a phone.

The measured costs, in ms (bench):

Kernel 256² 512²
fBm value noise, 6 octaves 10.2 28.6
Diamond-square midpoint displacement 3.5 9.0
Priority-flood (epsilon fill, binary heap, receivers and flood order) 15.8 55.9
Flow accumulation in reverse flood order 1.2 2.7
D8 receivers plus sort-by-height accumulation 25.1 99.8
Stream power, implicit Braun–Willett, 1 iteration (flood, accumulate, solve) 31.3 118.4
Stream power, 20 iterations 289 1015
Schott-style local stream power on the CPU, per step 39 129
Droplets, radius-3 brush, 0.5 per cell (33k / 131k) 128 445
Thermal erosion, 50 iterations, 4-neighbor talus 86 318
One wind sweep with an orographic cap 1.4 2.4
CA caves, 45% fill, 5 passes of the 4-5 rule 23 54

For comparison, the published costs: Cordonnier’s stream power at 10,000 nodes, 0.031 s a step and 6.4 s to converge on a 3 GHz i7; Beyer’s droplets at 256², 10,000 drops in 2.27 s on a 3.2 GHz i5; Johnson’s CA, 0.41 ms per 50×50 map in C#. DF says world-map generation is “generally much faster than history generation” (DF wiki). The shipped tools’ grids are small: DF’s worlds 17 to 257 squares a side, Lague’s demo 255², mapgen4 tuned to about 25k cells from precomputed points, Azgaar 10K points with no mobile app (“There is no mobile app”, per its FAQ), O’Leary 16,384 points, Johnson’s caves 50×50 chunks. mapgen4 moved generation into a Web Worker “to make it run acceptably fast”, keeping the map in the worker and transferring geometry buffers to the main thread (Patel, mapgen4 threads).

A budget for 256×256 from the laptop numbers, times an unmeasured phone factor, plausibly 2 to 4× for a mid-range Android (inference):

Stage Laptop
Base: Minecraft-style splines, or fBm plus an island mask about 30 ms
Priority-flood plus accumulation about 20 ms
20 implicit stream-power steps about 300 ms
0.25 droplets per cell about 60 ms
20 thermal iterations about 35 ms
Wind sweep, temperature (latitude plus lapse rate) and biome lookup under 10 ms
A final priority-flood for lakes and rivers about 20 ms
Total about 0.5 s; roughly 1 to 2 s on a phone

512×512 is about 4× per stage, about 2 s on the laptop with the same step counts. On a phone, cut stream power to about 5 steps, or run it at 256², upsample, and add droplets at 512², or precompute (inference).

When What (inference)
Precompute and ship Poisson or mesh points (mapgen4’s choice), splines and biome tables, rock-strata tables, and any “fully eroded” mountain set: a coarse 128² stream-power run upsampled, or a stock of eroded tiles
Per seed, in a worker Noise, flood, rivers, lakes, climate, biomes, caves, settlements, paths, vegetation
Never per frame Erosion. A GPU path (Schott’s kernel as a fragment-shader ping-pong on Tycho.gpu) is the only way to make full steady-state erosion interactive on a phone

Typed arrays (Float32Array heights, Int32Array receivers and order), a hand-rolled binary heap and precomputed brush offsets are what make these numbers. Array-of-objects or Array.prototype.sort-heavy code, as in O’Leary’s D3 version, is several times slower (inference).

Generate, measure, reject

Dwarf Fortress’s reliability comes from “belt and braces”: parameters steer generation, and separate tokens reject the world and reroll. “Certain factors, such as the number of mountain tiles, can’t be determined ahead of time … worlds are generated with parameters which are likely to produce worlds that can support a required number of mountains, and are then checked.” Rejections are logged with LOG_MAP_REJECTS:YES (DF wiki, World rejection).

Check Tokens
Biome counts REGION_COUNTS:<biome>:<initial squares>:<initial regions>:<final regions> for swamp, desert, forest, mountains, ocean, glacier, tundra, grassland and hills; example defaults for a 257 world are SWAMP 1032:7:6, FOREST 4128:13:12, and 8256 squares for mountains, ocean, grassland and hills. Initial counts are checked before erosion, final region counts after
Field spread ELEVATION_RANGES, RAIN_RANGES, DRAINAGE_RANGES, SAVAGERY_RANGES, VOLCANISM_RANGES require minimum numbers of low, mid and high squares; “Volcanism not evenly distributed” is a named rejection
Patchwork SUBREGION_MAX:2750 (1–5000) rejects worlds with too many separate biome regions
Placement VOLCANO_MIN, PEAK_NUMBER_MIN, PARTIAL_OCEAN_EDGE_MIN and COMPLETE_OCEAN_EDGE_MIN (0–4; 4 is an island world, 3 a peninsula), RIVER_MINS, MOUNTAIN_CAVE_MIN:25, NON_MOUNTAIN_CAVE_MIN:50, TOTAL_CIV_NUMBER:40, PLAYABLE_CIVILIZATION_REQUIRED:1

Rejection warnings can take “a few hundred” to thousands of rerolls to appear. The loop is cheap at this size, and a phone game can copy it with a twist: reject on a coarse 64² or 128² pass (a few ms), testing land fraction, a river reaching the sea, a mountain count and the biomes the quest needs, then run the costly stages once at full size. Because Elsewhere requires ctx.rng-seeded determinism, the reroll must derive the next seed from the current one (seed, seed + 1, …), so a saved world always regenerates the same (inference).

No phone measurements were made, and no reliable mid-range-phone JS benchmark was found (Speedometer comparisons online are flagship-only and vendor-sourced). The phone factor waits on a run of the bench, or a browser port of it, on target devices; worker startup and transfer costs on phones weren’t measured either.

What Tycho would need

The roadmap puts terrain and biomes in wave 3’s gen, and names Merryfolk’s priority-flood and D8 routines as donors for a later gen.flow if a second game asks for rivers. This page adds what that flow module would hold (inference):

Sources

The full research is in the notes on realistic world generation, with its benchmark world_generation_bench.js, on settlements, paths and vegetation, and on caves, village forms and biome edges.