Files
2026-09-25 17:02:24 +03:00
..
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00
2026-09-25 17:02:24 +03:00

Painted templates

A template is a picture of a world and a legend saying what its colours mean. The generator turns the two into terrain: Tools/Terrain/bin/terrain.exe plan reads them and says what a bake would involve, and terrain bake solves it. The contract between them is ../Planet.json.

World.json next to it is a different world — the square 14.28 km canvas the numpy pipeline and create_world.py still build. The two share nothing but their defaults, and nothing here touches that path.

# a minute: read the painting, cut the planet into regions, solve nothing
Tools/Terrain/bin/terrain.exe plan

# an hour or two: solve every region and composite the world. Run it detached.
# The output goes into the next free RawContent/World/Bake_NNN, so a re-bake never
# destroys the one before it - the interesting question is almost always "what did
# that change", and answering it needs both.
Tools/Terrain/bin/terrain.exe bake

# one landmass, short, for tuning the legend
Tools/Terrain/bin/terrain.exe bake --only 11 --steps 200 --out /tmp/try

# the same painting as a different world: massifs, rock, faults and coastline detail all re-rolled
Tools/Terrain/bin/terrain.exe plan --seed 9342

# the upland fabric at another size, without editing the manifest
Tools/Terrain/bin/terrain.exe plan --massif-km 5

# the painting tool: brushes that carry the legend's numbers, and Plan as a button
Tools/Terrain/bin/terrain.exe studio          # http://127.0.0.1:8099

The studio

terrain studio is the tool to reach for first. It exists because the two halves of a painted world used to live in different programs: the shapes in an image editor that knows nothing about uplift rates, the meanings in this legend, which cannot show you where they land.

The brushes are the classes. Picking highland is picking 0.25 mm/yr, and the panel tells you that is 11.7° of ground and reads as hill country while you are painting it — with the 32° divide angle beside it in grey, because that is where the number comes from and what the repose clamp eventually binds against. So the table in the section above stops being something to look up. Change a number and every swatch re-reads instantly; nothing is written until you press a Save button.

paint left-drag. [ and ] change the brush size
pan shift-drag, middle-drag or right-drag
zoom wheel
Plan runs the plan against what is on screen, not what is on disk, and prints the class table, the seam check and the region cuts
the map buttons draw class, uplift, regions, erodibility or overlay over the canvas, each with a key under it. Press again, or Escape, to go back to painting
layers the two tabs at the top, or press o. classes is the geology; overlay is the annotation layer, where the brushes are its marks and the last one is an eraser. See The overlay

Plan is seven seconds the first time and about a third of a second after that, as long as all you changed was a number. That is the tuning loop, and it is worth knowing why it works: the classification, the despeckle, the coast mask, the projection and the region cuts depend on the painting and on the class colours only — an uplift rate decides how the maps are coloured in and nothing else — so a plan that differs from the last one only in the legend's numbers reuses the whole of it. Touch the painting, or any of the coast or margin settings, and the next plan is a full one again. The report says which you got.

Rendering the maps smaller does not make it quicker, which is worth stating because it is the obvious guess. Measured on the 100 km template, prepare is flat at about 6.5 s from a 400 px map to a 2400 px one, and it breaks down as classify 0.09 s, dissolve strokes 0.93, despeckle 1.52, the coast mask 1.62, project 0.05 and region.Build 2.68 — and the last one works on the 76-million-cell planet grid, which the size of the painting and the size of the maps both have nothing to do with.

Three things about it are deliberate:

  • The brush is hard-edged and writes exact colours. No antialiasing and no soft edges, because a blended pixel is not a colour between two classes — it is a pixel that classifies as whichever third class happens to sit near the midpoint. See the note on the coastline below; that defect is real and this is where not to create it.
  • The canvas wraps. Paint off the left edge and it arrives at the right, because the map is a cylinder. The first template disagrees with itself on 9.4 % of its rows and this is how that gets fixed.
  • There are two sheets, and only one of them is geology. The class painting is what the solve reads; the overlay rides on top of it and, coast_jitter aside, changes no height anywhere. It is dimmed while the brush is on the classes so that a road can never be mistaken for something the solve will act on.
  • It never overwrites anything, and the base map least of all. Saves go to the next free Map3_001.png, Map3_002.png, ... beside the template, and the manifest is repointed at the new one — the same convention as Bake_001, for the same reason: the interesting question is almost always "what did that change", and answering it needs both. Rolling back is pointing planet.template at an earlier one. A painting is something you made by hand and there is no undo for it outside the studio, and writing back over a JPEG would additionally re-create the blended boundary pixels the despeckle pass exists to remove, compounding them on every save.

Legend and manifest saves patch the text rather than rewriting the file, so the commentary in both survives and a change still shows up as a small diff.

Baking from the studio

The Bake panel runs the real solve on the painting as it is on screen, and you can watch it. Because the geology is decomposed per landmass, each one comes out whole — so the preview redraws every time a region lands and the world fills in a continent at a time. The first one out tells you whether the numbers are right; the other seventeen do not have to finish for you to know.

regions blank bakes everything (~2 h on the 100 km template). A few ids — 11,13 — is the quick version, and the ids are the ones in the regions key
steps 0 is the manifest's full count. A few hundred is under-eroded but enough to see the shape
jobs how many landmasses solve at once. The wall time is set by the largest single region, which runs at about one core, so this helps least on exactly the template that needs it most
Cancel stops it. Regions in flight stop at the end of their current step, so the wait is one step of the biggest one

A finished bake goes to the next Bake_NNN. A cancelled one goes to Partial_NNN instead — the regions that finished are complete and worth keeping, everything else in it is still at sea level, and the separate name is so that nothing downstream (tiles --bake takes the newest Bake_NNN) can mistake one for the other.

The picture

Flat cylindrical: X wraps — the left and right edges are the same meridian, so a landmass may straddle them and comes out whole — and Y does not: the top and bottom rows are the poles. Any aspect works; the paint is never stretched to 2:1.

The painting has to wrap too, and this is the thing to check first. The left and right edges are the same meridian, so anything that does not continue from one to the other is a real discontinuity down one line of the world — and it is the one defect you cannot see by looking at the picture, because the two edges are as far apart on screen as they can be. terrain plan measures it:

wrap      the left and right edges are the same meridian: they disagree on 355 of 3761 rows (9.4%),
          261 of those land against water, and 48 px in the outermost columns match no class.
          THAT IS A VISIBLE SEAM. The generator wraps; the painting has to as well.

That is Map3.jpg as it stands. The crater island crosses the seam perfectly — sampled at the geology grid, the heights run continuously from the last column into the first — but the islets near the left edge were drawn touching x = 0 with nothing to meet them at x = W-1, so the world has a 400 m cliff down the seam wherever that happens. Painting round the edge, or nudging those islets inwards, fixes it.

PNG is better than JPEG, and the seam is where it shows. JPEG bleeds colour across every class boundary, and two legend colours closer together than the bleed will swap pixels where they meet — survivable in the middle of the map, but on the outermost columns a lossy encoder leaves a halo, and a halo on the seam is a stripe of the wrong class down the one line of the world that cannot hide it. Map3.jpg's first two columns are lighter than the ocean behind them, which the classifier reads as shelf: a 400 m ledge two pixels wide, running the height of the map.

The paint does not have to match the grid. Map3.jpg is 7738 px across a 100 km world, so a pixel is 12.9 m against an 8 m geology cell: the painting is slightly coarser than the simulation, which is the right way round. The rule from Docs/Terrain-Next.md §3.2 is that the painted map owns the wavelengths above its pixel size and noise owns those below.

Paint the uplift, never the height

This is the one thing to understand before painting anything. A class does not say how high the ground is; it says how fast the rock is rising and how easily it erodes. The simulation then produces the terrain.

Painting a heightmap instead does not work and the reason is worth knowing: a stream-power solve handed a painted surface erodes it into something else within a few hundred steps, and what comes out has no relationship to what was drawn — while the drainage network, which is the entire reason this generator exists, is thrown away. Paint intent; get rivers.

Rivers cannot be painted either. A river is an output of the drainage solve. What does work is biasing: raise k_mult along a line so the water finds the soft rock, and the solve chooses to put a river there for its own reasons — and the result is still a coherent network.

The legend

One JSON file beside the image. Keys beginning with an underscore are comments; any other unknown key is an error, because a misspelt key silently ignored is a class quietly running on the default.

{
  "image": "Map3.jpg",
  "warn_distance": 60,        // how far in RGB a pixel may sit from every class before the run says so
  "classes": [
    { "name": "ocean", "rgb": [91, 175, 185], "sea": true, "depth_m": 512 },
    { "name": "lowland", "rgb": [153, 204, 102], "uplift_mm_yr": 0.08, "k_mult": 1.0 }
  ]
}
key meaning
name what it is called in the reports and the maps
rgb the painted colour. Every pixel takes the nearest one, so there is no unclassified pixel
sea water. The solve holds every sea cell at base level for its whole run
depth_m sea only: how deep the open water is, in metres below sea level, positive
uplift_mm_yr land only: rock uplift, and the hillslope angle. See below: tan(divide) = 2.5 × rate ÷ k_mult at an 8 m cell, and the ground is a third of that in tangent — so 0.08 is 3.8° of median hillslope with 11.3° at its divides, and 0.25 is 11.7° with 32°
k_mult land only: the multiplier on stream-power erodibility. Soft rock above 1, hard below
stroke decoration rather than data — dissolved into whichever real class is nearest
edge_class a stroke touching the top or bottom row of the map is not a stroke; it becomes this class
derived never painted: no colour matching, exists only as something else's edge_class. Its rgb, if any, is for the diagnostic maps
coastal_plain_km land only: how far inland the rate ramps up to its full value, so the range sits behind a plain
coastal_floor_mm_yr land only: the rate at the waterline. Defaults to 0.02, never raised above the class rate
massif land only: {floor_mm_yr, fraction} — the class breaks into plain and upland instead of holding one rate everywhere. Read the next section before writing a legend without one
faults land only: {per_1000km2, throw_m, length_km} — traces placed in this class's ground. Absent means none, which is right for a plain. See One painting, many worlds
lithology_mix land only: 0 to 1, how much of the planet's rock field shows through here. Default 1; 0 is uniform rock, which is what an ice cap or a crater floor should be
crater land only: {rim_m, floor_m, rim_at, wall_at}, stamped on the finished terrain
snow land only: permanently under ice. Display and material only — no height moves, no pass reads it, and it is drawn with the palette's ice colour. What makes a polar cap terrain is its uplift rate and its detail block like any other class
detail land only: {droplets_per_cell, strata_contrast, amplitude_m}, what the detail passes do differently on this ground

A rate is an angle, and one rate is one landscape

This is the second thing to understand, and it is the one that caught the first painted planet out.

For n = 1 the steady-state slope is S = U / (K·A^m), and with critical_area_m2 at 0 that law reaches down to a single cell — so at a drainage divide A is one cell and the uplift rate, on its own, fixes the hillslope angle. At the 8 m geology cell and K 5e-5 it works out at:

tan(angle) = 2.5 × uplift_mm_yr ÷ k_mult

But a divide is the steepest place in a catchment, and almost none of a map is divide. A is smallest at the top of a hillslope and grows all the way down, so the divide angle is the ceiling on a class and not its landscape. Measured on a 600² grid of 8 m cells at 1000 steps with the manifest's own constants, the median slope comes out at a third of it in tangent, and the ratio barely moves over a factor of twenty in rate (0.34, 0.33, 0.33, 0.32). So there are two columns, and the one to steer by is the second:

uplift_mm_yr divide typical ground reads as
0.012 1.7° 0.6° plain
0.02 2.9° 0.9° plain
0.04 5.7° 1.9° plain
0.08 11.3° 3.8° rolling
0.10 14.0° 4.7° rolling
0.25 32.0° 11.7° hill country
0.28 35° 12.9° the angle of repose at a divide — above this the clamp shapes the ground, not erosion

terrain plan prints both for every class in the legend, along with the word in the right-hand column, and so does the studio panel as you type. Read it before starting a bake; it is four seconds against two hours.

Reading the divide column as the landscape is how a legend ends up two or three times too hot everywhere, and it is the same mistake as the one in the paragraph below, one level up.

Do not read internal/stats' "plain below 0.1 mm/yr" as a description of terrain. It is a reporting bucket calibrated for the old procedural path, and 0.1 mm/yr is a fourteen-degree hillslope: hill country wherever it is painted. That sentence is how the first planet's lowland ended up ten times too hot.

Flat ground, which needs two rates and not one

Because a class was one rate over every cell an author painted with it, a landmass painted one colour came out uniformly dissected — from the waterline to the summit, at whatever angle the rate named, with no flat ground anywhere on it. That is not what a continent looks like. Europe away from the Alps is a plain at a fraction of a degree with isolated massifs standing out of it, and what separates the two is not the rate, it is that the rate is not the same everywhere.

So a class carries a massif block: the class rate is re-read as the rate a massif reaches, floor_mm_yr is the plain between them, and fraction is how much of the class stands above the midpoint of the two.

{ "name": "lowland", "rgb": [153,204,102], "uplift_mm_yr": 0.08,
  "massif": { "floor_mm_yr": 0.012, "fraction": 0.16 } }

A sixth of it is rolling ground at 3.8° (11.3° at its divides); the rest is a 0.6° plain that tops out about 30 m above the sea over a whole continent. Half the fraction again — a twelfth — reaches 0.08 outright, and half again above that is off the plain at all.

So "the lowlands come out too hilly" is almost always one of two numbers, and neither of them is the floor. It is fraction, because the ramp opens at 1 - 1.5 × fraction in probability, so 0.16 leaves about a quarter of the class off the plain rather than a sixth; or it is uplift_mm_yr, which is what those raised parts climb to. The floor itself has been measured baking out at 0.58° median with 4 % of it over three degrees, which is a plain by any reading.

Three things worth knowing about it:

  • The floor wants to be about a tenth of the rate. It is the number that decides whether the class has flat ground at all. If a bake comes out too busy, though, drop fraction first: the floor is already a plain and lowering it further only flattens ground that was flat.
  • fraction is a share of the planet's surface, so it is only the expected share of any one island. A small island may get all of a massif or none of it, exactly as it would if it were a real island that happened to sit on or off an orogen. That variance is the point; normalising it per landmass would hand every island its quota of hills, which is the thing this exists to stop.
  • There is one fabric for the whole planet, planet.massif_wavelength_km in Planet.json, and every class cuts the same one at a different level. So a highland belt and the hills in the lowland beside it come out as the high and low parts of one structure — a foreland and its outliers — rather than as two unrelated noises meeting at a painted edge. Set the wavelength well below the size of a landmass: at 12.5 km against islands of 20–45 km one island came out entirely above the cut, which is the original defect over again, only smaller. 7 km puts several blocks across every continent.

A class with no massif block is one rate all over, which is what every class was before this existed. That is still the right answer for a small class, for a crater's flanks, and for anything an author would rather paint by hand.

Coastlines, which are not drawn lines

A shore drawn with a bezier tool is smooth, and a real coast is not: it has bays inside bays inside bays, and how long it is depends on the ruler you measure it with. Projected straight, the painting's own smoothness survives all the way to the heightmap and the result reads as what it is — a shape somebody drew.

So the waterline is roughened before the painting is projected. Fractal noise is added to the signed distance from the shore and the sign is read again, so land juts out where the noise is positive and the sea reaches in where it is negative. Nothing away from the shore moves.

// in Planet.json, not the legend: it is about the world, not about one colour
"coast_jitter_px": 48,               // how far the shore may move, template px
"coast_jitter_wavelength_px": 384,   // how wide the biggest bay is
"coast_jitter_octaves": 5,
"coast_jitter_gain": 0.55            // near 0.5: each scale as prominent as the last

At 12.92 m a pixel that is bays about 5 km wide and up to 620 m deep. The ratio of the two is the knob that matters — amplitude far below wavelength gives a rough line, amplitude approaching half the wavelength gives a fjord coast. Tune it in the studio, which prints both in kilometres as you type. 0 projects the painting exactly as drawn.

Small islands are safe: the amplitude is capped per cell at two thirds of the widest land within reach, so an islet gets a ragged edge instead of being swallowed by a wavelength ten times its size.

And a coastline you drew on purpose is exempt. The argument above is true of a shore nobody thought about and false of one traced off a real map, so the amplitude is a field rather than a number: paint a coast_jitter mark from the overlay over the stretch you want kept and it stays exactly where you put it while the rest of the world is still roughened. Zero pins it; above one chews it harder, which is how a fjord coast is made without turning the whole planet into one. See The overlay below.

A note on JPEG, which is not a style preference

The README says export PNG. Here is what it costs not to. Classification gives every pixel its nearest class in RGB, and a lossy codec blends across every boundary. On this template the blend of surf (221,238,238) and lowland (153,204,102) comes out at about (186,219,174) — whose distance to desert (238,221,153) is 53.8 against 77.9 to either of the colours it was actually mixed from. So the antialiased edge between the sea and the shore classifies as desert, and the map gains a one-pixel ribbon of it along every temperate coast: 1607 pixels on this one.

One pixel wide that is invisible. It stopped being invisible the moment the coast mask arrived, because a stray pixel is still land — so it became the nearest land to a stretch of open water, and every cell the mask turned into shore inherited its class. An eleven-pixel band of desert appeared along a green continent.

A despeckle pass now removes any class that holds a one-pixel line with two others either side of it, which costs 0.068 % of the map and takes that count to zero. It is a safety net and not a licence: paint in the studio or export PNG, and none of this happens.

The overlay

A second painting, the same size as the template and registered to it, for everything that is placed on the finished world rather than solved: where the forest is, where the village is, what a road follows, and which coastlines to leave alone.

It is a separate image rather than more colours on the template because the two answer different questions. Every colour on the template is geology — an uplift rate the solve answers for — and there is no uplift rate for a town. A mark has to be able to sit on top of any class without changing it.

// Planet.json
"overlay_legend": "Templates/Map3.overlay.json",
"overlay": "Templates/Map3.overlay_001.png",   // written by the studio; leave it out to begin
// Map3.overlay.json
{
  "image": "Map3.overlay.png",
  "match_distance": 40,      // an opaque pixel further than this from every mark is dropped
  "min_area_px": 24,         // below this a blob is a speck and is not reported
  "marks": [
    { "name": "drawn_coast", "rgb": [255,   0, 255], "coast_jitter": 0 },
    { "name": "wild_coast",  "rgb": [255, 128,   0], "coast_jitter": 2.5 },
    { "name": "forest",      "rgb": [  0, 128,   0] },
    { "name": "city",        "rgb": [220,  30,  30], "min_area_px": 400 },
    { "name": "road",        "rgb": [ 90,  60,  30], "kind": "path", "width_m": 8 }
  ]
}

Nothing is a colour. An unpainted pixel is transparent, not a background colour, so no colour has to be spent on emptiness and an export with a white matte behind it does not turn the world into whatever mark white is nearest. An opaque pixel that matches no mark inside match_distance is dropped and counted — terrain plan says how many, which is the only way a colour the legend forgot ever shows.

What the generator reads, which is one thing

coast_jitter and nothing else. It scales how far the waterline roughening may move the shore inside the mark: 0 pins it exactly as painted, 2.5 chews it two and a half times as hard as the rest of the world. Painting either side of the waterline is enough — the roughening already knows, for every cell it might move, which cell on the far side it would take its class from — so a brush stroke drawn along the shore covers both and nothing has to be traced precisely.

The check is exact: an overlay painted coast_jitter: 0 over every pixel produces a class raster byte for byte identical to a run with coast_jitter_px at zero.

What travels, which is everything else

No pass reads a forest or a city or a road. Two bakes with and without them are the same terrain to the bit. What those marks do is come out the other end in two shapes:

  • A mask per tile. Planet_x11_y07_overlay.png beside every heightmap: 8-bit, one mark index a detail cell, 0 for nothing. Marks cannot overlap — the overlay is one painting and a pixel is one colour — so 254 of them fit in the file one would have taken.
  • Features in world metres. overlay.json beside tiles.json, and beside the maps of every plan and bake. An area mark gives a centre, an area, a radius and an extent per connected blob; a path mark is thinned to its centreline and gives an ordered polyline and a length, because the thing built from it on the other side is a spline. Both are in metres east of the seam and metres south of the top painted row.

A blob that crosses the seam is one feature, not two, and its centre lands on the blob rather than on the far side of the world. A road across the seam comes out as one polyline whose X runs past the circumference, which a consumer can wrap back knowing how wide the world is.

One stroke is one path. A fork reports its two longest arms as a single line and drops the third, so paint each run separately — terrain plan prints how many pieces each mark has, which is where a fork shows.

Generating a first draft

The overlay starts blank, and the three things most worth putting on it - woodland, settlements and the roads between them - all depend on ground you cannot see while painting: the classes are painted before the solve exists, and once it does exist it is a seventy-six-million-pixel heightmap. So there is a command that reads a finished bake and proposes them.

terrain overlay                      # the newest Bake_NNN, writes the next Map3_NNN.overlay.png
terrain overlay --no-save            # say what it would place and write nothing
terrain overlay --bake RawContent/World/Bake_022
terrain overlay --replace            # start from a blank sheet instead of filling in around what is painted

In the studio it is a button. Switch to the overlay tab and press Generate marks. That is the loop this is really for: press it, look at where the towns landed, press it again, keep the third one. Two things differ from the command, and both follow from a button being pressed repeatedly rather than once.

  • Every press is a new seed, so it is a re-roll by construction. The painting fixes where the land is; the seed decides everything it does not - which patch of eligible ground becomes woodland, and which of several equally good sites gets the town.
  • Each press replaces the last draft instead of piling on top of it. The studio remembers exactly which pixels the previous generation put down and clears those, and only those, first. Anything you painted is never in that set.

The sheet is replaced wholesale rather than stroked, so ctrl+z will not walk back over a generation and the undo history is dropped when you press it. The status line says so. Nothing you painted is lost either way, because generation only fills blanks.

It works before the first bake, on the painting alone, and says which of the two ran. That matters: with no solve there are no rivers to sit on and no slope for a road to bend around, so a draft made that way is a sketch that respects land, sea and classes and nothing else. Bake, then press again for one that reads the terrain. A bake made from a different painting is ignored rather than used - two paintings of the same planet encode their heightmaps identically, so nothing else would catch it and the marks would be placed against terrain from another world.

It never touches a pixel you painted. The sheet on disk is loaded first and generation fills around it, so running this against a half-painted overlay adds to it, and running it twice is safe. That is what makes the round trip work in both directions: generate a draft, move the towns where you want them in the studio, regenerate the roads around your edits. --replace is the explicit way to throw a generation away.

It is opt-in per mark. A mark generates only if it carries a generate block, so a legend written before this existed produces the blank sheet it always did, and a mark you want to own completely simply says nothing.

{ "name": "forest", "rgb": [0,128,0],
  "generate": { "kind": "forest", "cover": 0.45, "wavelength_km": 6,
                "max_slope_deg": 22, "not_classes": ["ice", "desert"] } },

{ "name": "city",  "rgb": [220,30,30], "min_area_px": 400,
  "generate": { "kind": "settlement", "count": 3, "min_spacing_km": 9, "not_classes": ["ice"] } },

{ "name": "road",  "rgb": [90,60,30], "kind": "path", "width_m": 8,
  "generate": { "kind": "road", "max_slope_deg": 20 } }

Four kinds:

  • forest fills ground that is shallow enough and below the treeline, broken up by a noise field so it reads as woodland rather than as a contour band. cover is the share of eligible ground it takes and wavelength_km how big the patches are. The treeline defaults to a quantile of the land's own heights, because a number in metres means nothing until the world is baked - the same legend has to put trees on a 47 m plain and on a 2800 m range.
  • settlement scores the land on three things the terrain actually knows - drainage, flat ground and distance to the sea - and takes the best sites with a minimum spacing. Several marks may use it and they share one spacing rule, largest first, so a village never lands inside a city. Everything else about where a town is - trade, history, who won a war - is yours, which is why these are proposals in a sheet you edit.
  • road joins the settlements along least-cost paths, as a minimum spanning tree rather than every pair, so there is exactly enough road to reach everywhere. Water is impassable, so each landmass gets its own network and a bridge or a ferry stays a deliberate act.
  • coast bands the waterline. It is not enabled in the shipped legend on purpose: coast_jitter is the one overlay property a pass reads, so generating that mark changes the next bake's coastline everywhere, and that is a decision to take deliberately rather than to inherit from a default.

only_classes and not_classes keep a mark off ground you painted a particular way. They matter more than they sound: height and slope cannot tell an ice cap from a meadow, and the first run of this generator grew woodland across both polar caps, which are flat, below the treeline, and no business of anybody's. A class name that is not in the class legend is an error rather than an empty filter.

Read the run's report. It says what it kept, what it painted, and - the useful one - when it placed fewer settlements than you asked for, which means the spacing or the flat ground ran out. On this template asking for 26 villages at a 9 km spacing gets 17.

Painting it

Press o in the studio, or use the overlay tab. The brushes become the legend's marks, the last one is an eraser, and the sheet rides over the class painting — full strength while you are on it, dimmed while you are painting classes, because a mark means nothing except against the coastline it was drawn along. Save overlay writes the next numbered PNG and points the manifest at it, exactly as the class painting does.

A planet with overlay_legend set and no image yet is the normal way to start: the legend says what the marks mean and the sheet is empty until the first save.

One painting, many worlds

A painting is a composition: where the continents are, where the ranges run, which coast you drew on purpose. It is not the whole world. Everything inside it that nobody drew comes from the seed, and changing the seed re-rolls all of it while the painting stays exactly as painted.

Tools/Terrain/bin/terrain.exe plan --seed 9342          # four seconds; the maps say what moved
Tools/Terrain/bin/terrain.exe bake --seed 9342 --out RawContent/World/Bake
Tools/Terrain/bin/terrain.exe tiles --seed 9342 --bake RawContent/World/Bake

Or in the studio: the seed box and the Re-roll button, then Save planet and Plan.

--seed is on tiles as well, and that is not a convenience: the detail passes hash the seed into every droplet, so a tile run has to be told the seed its heightmap was baked under. CheckBake refuses a mismatch rather than giving you gullies from a different world.

What moves

the upland fabric where the massifs stand inside every class that has one
the rock which lithology province is where, and so which flank of a range is gullied and which is benched
the faults where every trace is, which way it runs, and how much it throws
the regional swell the long-wavelength warp that puts divides on a plain
the initial relief the small symmetry-breaking the solve starts from
the coastline detail the bays and headlands the waterline roughening cuts, except where the overlay pins it

Measured on this template, seed 7 against 9342: 13.8 % of map_uplift.png and 24.9 % of map_erodibility.png change, against 2.1 % of map_class.png — and that 2.1 % is only the coastline.

Rock

One low-frequency pattern over the whole planet, cut into pipeline.lithology's types at quantiles of the planet so every region agrees, multiplying each class's own k_mult:

// Planet.json
"lithology_wavelength_km": 9,            // how big a province is. 0 switches it off
"pipeline": { "lithology": { "types": 3, "k_multipliers": [0.6, 1.0, 1.8] } }

Set the wavelength well below a range or a range is one rock and you have gained nothing: 9 km against belts of 15–25 km puts two or three provinces across each of them. The bands are equal-area, so every type appears whatever the seed did — a seed that produced no hard rock anywhere would be a seed that quietly removed a process.

Boundaries are graded rather than blurred, and pointwise: the width of a transition on the ground follows the field's own gradient, sharp where the rock changes fast. That is not a style choice. A blur is a neighbourhood operation, and one near a region's edge would read cells a different decomposition of the planet would not have given it — the same rule that makes the cut a planet quantile rather than a regional percentile.

Faults

A fault is a difference in uplift rate across a line, steep on one side and gentle on the other, which erosion carves into a scarp. That is why it is not something to paint: painted as terrain the solve erodes it away, applied as a rate it maintains itself. It is what puts an escarpment and a straight fifteen-kilometre valley inside a range, and neither is a shape a brush can draw.

{ "name": "highland", "uplift_mm_yr": 0.25,
  "faults": { "per_1000km2": 25, "throw_m": [120, 400], "length_km": [6, 18] } }
  • per_1000km2 is a density over this class, not a count, because a class covers whatever you painted with it. This template has 549 km² of highland, so 25 is about fourteen traces before the long ones step.
  • throw_m is the whole step across the fault over the run, not a rate — how much higher the upthrown side would stand than the downthrown one if nothing eroded either, which is a number you can picture. It is the step, not the height of each side: before D-62 the profile put a full throw on each flank and so built two, and a legend written against that number is now asking for half what it used to get.
  • Put the block on the classes that are orogens. A plain should have none. The influence is not restricted to the class, because a range-front fault runs along the edge of a range by definition and its scarp faces the lowland.

planet.fault_grain_km is only how they are aimed: a grain field, so traces near each other come out sub-parallel and the set swings gradually across the world rather than running as corduroy.

A fault is kilometres wide, not a line. The rate crosses from the downthrown side to the upthrown one over about a kilometre and the upthrown flank reaches six, so what a fault makes is a range front — a belt of ground with its own valleys cut into it, its crest a kilometre or so back from the trace. It used to be a step across one cell inside a 600 m welt, which erosion could not touch, so it came out as a smooth ruled ridge; that is D-62. Two consequences for you: a fault influences ground several kilometres from where you would draw the line, and it needs that much room, so faulting a class painted in strips two kilometres wide will not give you what the picture in your head has.

And a set of them saturates rather than stacking (D-63). Faults near each other are sub-parallel on purpose — that is what fault_grain_km is for — so once each one is kilometres wide they overlap, and overlapping faults that all push the same way used to add. Where several reach the same ground the total is now bent onto at most 1.6 times whatever the strongest single fault there asked for. One fault is untouched by that bound, so throw_m still means exactly what it says; what it stops is five parallel faults delivering five throws. The practical consequence for you: raising per_1000km2 past the point where faults overlap buys texture, not height. If you want a belt to stand higher, raise throw_m.

Watch fault_grain_km against the size of your landmasses. It is the wavelength of the field the strikes are drawn from, so a landmass much smaller than it sits inside one grain cell and every fault on it comes out near-parallel. The shipped 45 km against a 22 km landmass is exactly that case.

Traces over twelve kilometres are drawn as two or three overlapping en-echelon segments, the throw rises and falls along strike as a bell rather than holding flat through the middle, and the only ceiling is the repose angle — or whatever your own numbers asked for, whichever is higher. A bake's per-region line says what share of the land that bound touched, and says nothing when it touched none; a large share means the throws are big for the class they are sitting in.

Expect erosion to take most of the throw. Measured on this template, region 15 baked whole: five traces with throws of 139 to 399 m left scarps of 2.7 to 50 m, all five facing the side the fault raises. That is the point of applying a fault as a rate — a painted 400 m step would have been eroded to nothing in a thousand steps, while a rate difference keeps rebuilding the scarp as fast as the rivers cut it down. (Those five were measured 600 m either side of the trace and under the pre-D-62 profile, so they straddle the old welt rather than spanning a range front; treat them as the right order of magnitude and not as a table to tune against.)

They are drawn on map_uplift.png in cyan — the line rather than the rate it contributes, because a scarp is a couple of hundred metres wide and that map is a hundred kilometres across. The geometry is in the bake's meta.json, so "which fault made that valley" is answerable afterwards.

Making a desert

A desert is not a low uplift rate — a wet lowland has one of those too. At the geology grid the only lever is k_mult, and below 1 it means less water doing less work, so the ground comes out steeper and more angular and further from being worn down. That is right, and it is nowhere near enough: at eight metres a desert and a meadow are the same picture.

The difference is at two metres, in the detail block:

{ "name": "desert", "rgb": [238,221,153], "uplift_mm_yr": 0.10, "k_mult": 0.5,
  "detail": { "droplets_per_cell": 0.035, "strata_contrast": 0.92, "amplitude_m": [7, 14] } }
  • droplets_per_cell is the load-bearing one. A fifth of the running water turns a dendritic gully network into a few isolated wadis, which is most of what "arid" looks like from the air.
  • strata_contrast near 1 keeps the ledges sharp: mesas and benches survive because nothing is rounding them off.
  • amplitude_m raised on flat ground is dune. Two metres on a plain is a plain; ten is a sand sea.

Anything left out keeps the manifest's own number, and a legend that overrides nothing costs nothing — the class raster is not even carried through the detail passes.

Putting the range inland

A uniformly painted island comes out at the angle of repose right down to the water, and that is arithmetic rather than a tuning miss: for n = 1 the uplift rate alone fixes the hillslope angle, so the rivers cut down to sea level but the ground between them has no idea how far from the coast it is. Fjords, end to end.

coastal_plain_km ramps the rate up from the waterline, so there is a plain in front of the range:

{ "name": "highland", "rgb": [68,170,102], "uplift_mm_yr": 0.25,
  "massif": { "floor_mm_yr": 0.045, "fraction": 0.30 },
  "coastal_plain_km": 4.0, "coastal_floor_mm_yr": 0.03 }
distance inland   0km   1km   2km   3km   4km   5km+
uplift mm/yr     0.03  0.06  0.13  0.20  0.24  0.25
at a divide        4°    9°   18°   27°   31°   32°
typical ground     1°    3°    6°    9°   11°   12°

(on a massif cell — the ramp runs up to whatever rate the massif field left there, and it only ever ramps down towards the sea, so a cell of plain between two massifs keeps its floor rather than being raised into a rim of hills round the edge of the continent.)

It never goes to zero at the water, and that matters: an earlier version of the generator tapered the uplift to nothing at the shore as a hidden side effect, which flattened the hundred-metre strip the surf works in and moved every cliff inland (D-52). This is you saying where the range starts.

Craters

An impact is an event, not a rate, and it cannot be painted as one — a closed basin built from negative uplift is filled in by the priority-flood within a hundred steps, because the flood raises every depression to its spill level on every one of them. So a crater class is stamped onto the finished terrain after the solve, and its shape comes from the blob you painted: distance inward from its own shoreline, normalised by its widest point, so the same four numbers describe a small crater and a large one.

{ "name": "crater", "rgb": [124,117,111], "uplift_mm_yr": 0.15,
  "crater": { "rim_m": 340, "floor_m": 60, "rim_at": 0.30, "wall_at": 0.62 } }
   sea ~~~~\        /‾‾‾\_________/‾‾‾\        /~~~~ sea
            \      /  rim         rim  \      /
             \____/                     \____/
   t:        0   0.30      0.62 .. 1 floor

rim_at is where the crest sits and wall_at where the inner wall reaches the floor, both as a fraction of the way in from the shore. floor_m above zero keeps the basin dry; the floor is a closed basin, so expect it to pond in the detail passes unless there is a gap in the rim. A crater that straddles the seam is one crater.

The class's uplift_mm_yr still matters a little — it is what the solve does to the flanks before the stamp lands — so keep it low. It is not what makes the crater.

Uplift is the number that matters and the one to turn first. Steady-state slope is U/(K·A^m), applied down to a single cell, so at an 8 m cell and K 5e-5 a rate of 0.03 mm/yr is about a 4° hillslope, 0.08 about 8°, and anything past roughly 0.45 is above the angle of repose and shaped by landsliding rather than by rivers. Raising the rate makes the summits higher; it does not make the ground steeper past that point.

The white problem

Every hand-painted world map draws white twice: the polar caps, and an outline stroke around each island. Exactly one class can own that colour, and it has to be the stroke, because the stroke is the one that must be recognised wherever it appears. What the caps become is then a derived class:

{ "name": "ice",    "derived": true, "rgb": [250, 250, 250], "uplift_mm_yr": 0.05 },
{ "name": "stroke", "rgb": [238, 238, 238], "stroke": true, "edge_class": "ice" }

A white region touching the top or bottom row is a cap and becomes ice; every other white dissolves into whichever real class is nearest, split down its middle rather than given wholly to one side — which is the only answer that does not move the coastline by the width of the artist's brush.

The plan's two counters are how you check it landed: Map3.jpg reports 2,084,442 px rescued at the poles and 0 dissolved, which is the evidence that on this template white is only ever the caps.

The palette

How a preview is drawn is a separate file from what the painting means, and deliberately so: the legend is about the world, the palette is about the picture. Nothing in a palette changes a height — two bakes of the same world under two palettes are the same terrain — so swapping one is cheap and reversible.

# write the defaults out to start from
Tools/Terrain/bin/terrain.exe palette RawContent/World/Templates/mine.palette.json

Then point the planet manifest at it with "palette": "Templates/mine.palette.json". Leaving the key out uses the same numbers, so the file is a starting point rather than a requirement.

It holds the hypsometric ramp (land_stops, sea level at t 0 to the top of the land at t 1), the water (sea_shallow, sea_deep), the rivers, the ice, where the ramp's top is taken from (land_top_percentile — a percentile rather than the maximum, so one high summit cannot push a whole continent into the bottom of the ramp), and the light (sun_azimuth_deg, sun_altitude_deg, ambient, gain). Anything left out keeps the default; a misspelt key is an error rather than a setting that silently does nothing. Keys beginning with an underscore are comments.

Reading a plan

terrain plan writes the maps and plan.json into RawContent/World/Plan/, in about four seconds. In order of what they answer:

  • map_class.png — did the legend read the painting? If this is not the picture you drew, nothing downstream can be. The match report beside it counts pixels no class was near.
  • map_regions.png — how the planet was cut up. Each region is one hue, its land saturated and the ocean margin it carries the same hue dimmed. Check that a landmass straddling the seam is one colour and not two, and that nothing has swallowed the ocean.
  • map_uplift.png — the field everything else is a consequence of, the massif fabric included, so a class with a massif block shows as plain with upland in it rather than as one flat colour across the landmass. The one to read when the ground comes out the same everywhere.
  • map_erodibility.png — where texture inside a range will come from.
  • map_overlay.png — the annotation layer over a dimmed class map, when there is one. A mark means nothing on its own and everything against the coastline or the range it was drawn along.

And the class table above them, which is the four seconds best spent. It prints every class's uplift rate and then two angles, and which of the two you read is the difference between a legend that is right and one that is three times too hot:

  class          share       cells   uplift mm/yr    K     depth m   divide    typical
    lowland       14.4%    10967451          0.080  1.00         -   11.3 deg    3.8 deg  rolling
                 massif over 16% of it; the other 84% is 0.012 mm/yr, 1.7 deg at a divide and 0.6 typical - plain

The same two angles are in World Orogen's class table, beside the inputs that set them (D-67), which is the place to read them while you are still typing the numbers.

divide is the steepest ground the rate can make: steady state is S = U/(K·A^m), A is smallest at the top of a catchment, and that is where the number comes from. typical is the median slope over the class, and it is about a third of the divide angle in tangent — measured on a 600² grid of 8 m cells over a factor of twenty in rate, where the ratio came out 0.34, 0.33, 0.33 and 0.32. Almost none of a map is divide. Read the second column; it is the ground somebody stands on, and reads as is taken from it.

Then the block that says what a re-roll would move: the rock provinces and the fault set, with a trace count per class. And if there is an overlay, how much of the world each mark covers, how many pieces it is in, and what it asked of the coastline.

Then the region table, which is where the bake time goes. The margin is the lever. It decides which landmasses are solved together: two within twice the margin share a box. At 0.5 km this template gives 18 regions; scattered islets on some other template would each cost a whole region, and --margin-km is how to find out before spending the afternoon.

Read the time estimate as a floor. It is calibrated on lowland, and mountains cost about five times as much per cell — steep ground drives the hillslope law to its full sub-step budget every step while a plain leaves it idle. Measured on this template: a 14.0 M cell lowland region took 1014 s and a 4.0 M cell highland region took 1394 s. So raising an uplift_mm_yr does not only change the terrain, it changes the bake time.

The detail bake

terrain tiles runs the detail passes over a geology bake and writes the ground somebody can stand on. It reads the bake's heightmap off disk rather than solving anything, which is what makes it batchable: the geology is hours, a 5 km tile is about twelve seconds, and the islands you care about can be baked first.

# every tile, about forty minutes for a 100 km world. --bake defaults to the newest Bake_NNN.
Tools/Terrain/bin/terrain.exe tiles

# a rectangle of them, by tile index, from a particular bake
Tools/Terrain/bin/terrain.exe tiles --bake RawContent/World/Bake_001 --only 11,7,13,8

# the geology upsampled and nothing else: is what you are looking at the solve or the detail passes?
Tools/Terrain/bin/terrain.exe tiles --only 16,6,16,6 --no-detail

Each tile gets Planet_x11_y07.png (16-bit height, 2500 x 2500 at 2 m), _shade.png (a hillshade at the same resolution, because a 16-bit grey PNG of a hundred metres of relief is a flat grey rectangle to look at), and _flow, _wear and _deposit as 8-bit maps of what the droplets did. tiles.json beside them carries the grid, the cell size, each tile's world origin and its elevation range.

If the planet has an overlay, _overlay.png comes too — 8-bit, one mark index a detail cell — with overlay.json beside tiles.json as its key and the feature list. The mask is sampled through world metres rather than through a tile-local index, so a cell reads the same mark whichever tile reaches it.

The tiles are seamless and that is measured, not asserted: a tile carries a margin of three droplet lifetimes which is discarded afterwards, and every hash and noise lattice in the passes is keyed on absolute world position, so a cell in one tile's interior comes out as it would have in one whole-world run. The margin costs about five per cent of a tile on each side.

What is not there yet: the shore, at detail resolution. The geology bake has a shelf, a surf-cut platform and a beach as of D-60, but the detail passes do not refine any of it: a tile's coastline is the 8 m coastal pass upsampled, with no sand, no boulder field and no plan-view crenulation at 2 m. Everything inland is finished.

What a bake writes

preview.png is relative, and that matters more than it sounds

The hypsometric ramp's top is land_top_percentile of the land in the world being drawn, not a fixed height. So bare rock and snow on a preview mean "the highest ground here" and say nothing at all about scale.

Measured on this template's central lowland: it bakes to 0..47 m with a median hillslope of 0.61°, 4.4 % of it over three degrees and nothing anywhere over eight — a plain by any reading. Drawn against its own 32 m ceiling it comes out with tan uplands and white caps on its 40 m hills, which is exactly what a 2800 m range looks like, and it is the single most misleading thing the generator produces. Redrawn at a fixed 400 m ceiling the same heightmap is flat green with four pale massifs on it.

Every run prints which ceiling its preview used, so read that line before reading the picture:

preview the hypsometric ramp tops out at 32 m - the 99.5% percentile of *this* world's land, so rock and
        snow mean "the highest ground here" and nothing about scale. Set palette.land_top_m for an
        absolute ramp

palette.land_top_m is a height in metres; 0 keeps the percentile. The percentile stays the default on purpose — an absolute ramp over a world with no mountains is a flat green shape with nothing legible on it, and "is there drainage here" is a question the contrast has to answer. When what you want to know is how steep is this ground, read map_slope.png and the plan's typical column, not the tint.

planet_height.png at the geology grid (12500 × 6076 at 8 m for this world), plus quarter and tenth overviews; preview.png with hypsometric tint, hillshade and rivers; the plan's maps again, now over real terrain; map_slope.png and map_flow.png; and meta.json with the resolved manifest, the legend, the match report and every region's statistics.

And the two coastal maps, which are the whole of what the pass did:

  • map_coast.png — what it moved, cool where the surf cut and warm where the sediment landed, on a ±30 m scale. The deep sea floor is masked out of it on purpose: the ocean drops five hundred metres in this one pass and would otherwise set the scale for the few metres the shore processes move, which is the thing the map exists to show.
  • map_exposure.png — how open each stretch of shore is to the sea, drawn only within a kilometre of the water. Not tidiness: exposure is measured on the waterline and carried inland by "the nearest stretch of shore to you", so past a few hundred metres it stops being a coastal quantity and becomes a picture of the continent's medial axis.

The pass is one line in the log and one block in meta.json, and it runs once over the whole cylinder rather than per region — fetch crosses straits, and a sediment budget split in two is not a budget:

coast: 971 km of shoreline, 65% sea, shelf 60% of it; surf planed 3.5 km2 and cut 34.18 Mm3,
       6 river mouths delivered 1.15 Mm3, 35.32 Mm3 laid (0% unplaced) as 4.14 km2 of new beach

0% unplaced is the line to read: it is the sediment the budget could not find a home for, and a figure that is not near zero means the drift ran out of shore before it ran out of sand. On a --only run the backshore figures read 0 m, correctly — most of the planet's land is unsolved and sitting at sea level.

It is also the memory peak of a bake, at about 8.3 GB of working set against the solve's 3.6, and it is worth knowing that before starting one on a smaller machine.

And the verdict block, which a bake prints for the first time as of D-59: the slope distribution, the slope-area fit, the hypsometric integral, the drainage density, and the per-uplift-class breakdown that is the one to read. It is pooled from the regions rather than computed on a grid that never exists - a histogram adds, so summing the pieces and taking a quantile of the sum is exactly what one pass over the whole world would have given.

A run with --only marks itself PARTIAL: its extent is the whole cylinder while its ground statistics cover just the landmasses that were solved, and the two are not comparable.

  field -543..340 m; land 0..9 m (relief 9 m), 36% land, 0.00% clipped
  PARTIAL: the line above is the whole world; everything below is the 3% of its land that
           was actually solved (842682 of 27455176 cells). The two are not comparable.
  slopes: 100% under 15 deg, 100% under 30, 0.0% over 50, median 0.5 deg
  ...
  by uplift class:
    plain     0.00..0.10 mm/yr   100% of land  slope  0.5 deg median,  0.8 P90  relief  3 m/500 m  ...

The polar pad never appears in any of them. It is a few rows of synthetic ocean above and below the painted map, added so that a cap touching the top row has a shore to drain into — without it the solver would treat the map edge as an outlet and freeze the ice at its initial relief.

Limits worth knowing

  • A landmass cannot ring the planet. A region is flattened into a rectangle with water on both sides, because a grid edge is an outlet; one that goes all the way round has no such edges. The run refuses it rather than silently freezing the coast. Break it with a strait.
  • The seed alone no longer names a world. The margin and the minimum landmass size change how the planet is cut up, and the priority-flood's traversal across a flat depends on the box it is flooding. All three are in Planet.json and all three are recorded in meta.json.
  • The elevation range is a hard clip. A run whose clip fraction is above 0.1 % is a failed run, not a rounded one, and painted uplift makes it easy to ask for more relief than the range holds. U/K is the one relief knob.

The same painting in World Orogen

Tools/Orogen/ (D-66) reads exactly these files. Serve the folder with any static server and open the import page's Painted Map source:

npx serve Tools/Orogen          # then http://localhost:3000/import

Choose Map3_002.png as the painting and Map3.legend.json as the legend, press Solve Terrain, and in about half a minute the painting is a globe with rivers, climate and a satellite view, plus class, uplift, erodibility, drainage, slope and basin layers to inspect and export. The table under the legend edits the classes' numbers and Download legend JSON writes them back into a copy of this file with the commentary intact, so a legend tuned there runs here unchanged.

What it is for is the thirty-second question - does this painting make the rivers I meant, is that lowland a plain with massifs in it or a slab - before the two-hour bake answers the eight-metre one. What it is not: the globe is Earth-sized (legend distances are scaled by the ratio of circumferences), the cells are ~44 km, and faults, craters, the plates, the repose clamp and the detail passes are not carried because nothing at that cell size could show them.

It reads this planet's own files now, and the studio serves them. Point it at Planet.json and the planet block, the lithology multipliers, the seed and the geology grid's constants travel without being retyped; it outranks a legend's own planet block, because it is the file the bake reads. The overlay is carried too - as a texture over the globe and the map rather than voted onto the mesh, since a road is a few pixels wide and a cell there is tens of kilometres - and its coast_jitter marks pin or roughen the shore on the sphere exactly as they do here.

Easiest of all, while terrain studio is running, its From terrain studio button loads the painting, both legends and the manifest in one step, including strokes made since the last save, because the studio holds the painting in memory. The studio shares reads and only reads: its CORS header is on GET alone and it answers no preflight, so a browser tab can look at the planet and can never start a bake.

What it still cannot tell you is the ground at eight metres. The bake owns the geology grid, the coast pass, the faults, the craters and the detail tiles, and only they produce ground a player can stand on.