16 KiB
The world pipeline: from a painted map to ground in Unreal
Read this when you want to know what order things happen in. It is the orientation document for everything between "an author paints a world map" and "a player stands on it", and it owns no decisions of its own — every one of them is argued somewhere else and linked from here.
| If you want | Read |
|---|---|
| Why the generator exists and how it works | Terrain.md — the specification and the decision record |
| What is being worked on right now, what looks wrong | Terrain-Next.md — the working brief |
| Every manifest key, explained | RawContent/World/README.md and Scripts/Authoring/region_manifest.py |
| How to paint a world | RawContent/World/Templates/README.md |
| Why a thing is the way it is | Decisions.md — D-47 onwards is all world |
All of this is off the ladder (D-47). No gameplay code reaches into it and nothing here blocks a step in
Steps.md.
The shape of it
RawContent/World/Templates/Map5.png + Map5.legend.json
a painted flat cylindrical world map what each colour means: uplift mm/yr, erodibility
|
| terrain studio paint it, 127.0.0.1:8099
| terrain plan 4 s: cut the planet into regions, solve nothing
v
+----------------+----------------+
| |
terrain bake Tools/Orogen (the browser twin)
~2 h, 8 m geology grid ~12 s, 204K-region sphere mesh
| |
v v
RawContent/World/Bake_NNN/ "Export Map"
planet_height.png RawContent/World/Orogen Gens/
map_flow / class / uplift ... orogen-heightmap-7945.png (8192 x 4096)
overlay.json, meta.json orogen-colormap / satellite / climate ...
| |
| +------------+-------------+
| | |
terrain tiles generate_region_tiles.py Tools/MapArt (Go)
5 km tiles at 2 m cuts the window into tiles `biomes` -> biome masks
| ^ `build` -> 4 map layers
| | |
| | |
X NOT WIRED v v
(this is the RawContent/World/ RawContent/World/MapArt/
future; see RegionTiles/*.png map_relief / colour / satellite / climate
"What is not | |
wired yet") create_region_world.py create_world_map.py
via build_region.sh via build_world_map.sh
| |
v v
Content/Maps/L_World Content/World/Maps/
98 landscapes DA_WorldMap_L_World + 4 textures
71.40 x 35.70 km |
\ /
\________________________/
|
the map view, in game and in the editor
Two generators sit side by side and they are not interchangeable. The Go tool in Tools/Terrain/ is the
one that makes ground a player can stand on: an 8 m geology grid, a coastal pass, faults, craters, detail
tiles. World Orogen in Tools/Orogen/ is a browser twin that reads the same painting and the same legend and
solves them on a sphere mesh in about twelve seconds — for looking at a painting's rivers on a globe and tuning
the legend's numbers before committing two hours to a bake. A 44 km cell cannot show a fault or a crater, and
it never will (D-66).
The ground in L_World today came from Orogen, not from the Go tool. That is a deliberate shortcut: it
put 900 km² of ground in the engine long before the generator's detail passes reach Unreal, and the relief you
see is therefore art rather than a solve. terrain bake makes that same continent a 116 m plain; the Orogen
export makes it 2972 m (D-69).
The three routes
Only one of these is live. Knowing which is which saves an hour of confusion.
| Route | Status | What it builds | How the tiles are made |
|---|---|---|---|
| A. Orogen → Python cutter → Unreal | live; this is what L_World is |
Content/Maps/L_World |
generate_region_tiles.py cuts a window out of a whole-planet PNG. Region.json's source.kind is planet_map and its metres_per_pixel is a number somebody chose |
| B. Orogen → direct tile export → Unreal | built, not the one in use (D-71) | the same level | Orogen's Unreal Landscape… export renders the tiles itself and writes Region.generated.json. source.kind is orogen_render, and the scale is a consequence rather than a guess |
C. terrain bake → terrain tiles → Unreal |
not wired | the same level, eventually | This is the point of the whole generator. terrain tiles already writes 5 km tiles at 2 m; nothing carries them into Unreal yet |
Route B is strictly better than A on the one thing that matters most — the scale stops being invented — and the
only reason A is what is in the level is that A came first. Route C is where this is going, and when it lands,
generate_region_tiles.py is the only thing that changes.
Route A, step by step
This is the sequence that produced what is in the project now.
1. Paint the world
cd Tools/Terrain && go build -o bin/terrain.exe ./cmd/terrain
Tools/Terrain/bin/terrain.exe studio # 127.0.0.1:8099
The brushes are the legend's classes. The panel prints the hillslope angle each uplift rate buys as you type
it — read the typical column, not divide: almost none of a map is divide, and reading the divide angle
as the landscape is how a legend gets set two or three times too hot (D-59). o switches to the overlay sheet,
ctrl+z undoes a stroke, and Re-roll changes the seed, which moves the massifs, the rock, the faults and
the coastline detail without touching a painted pixel (D-58).
Paint the uplift, never the height. A solve handed a painted surface erodes it into something else and throws the drainage network away, which is the reason the generator exists at all.
2. Check the painting before committing to a bake
Tools/Terrain/bin/terrain.exe plan # four seconds, solves nothing
Plan/map_class.png and Plan/map_regions.png are the two pictures that decide whether a bake is worth
starting. plan.json carries the class table.
3. Solve it — in the browser first
Serve the repo and open the import page:
npx serve Tools/Orogen # any static server
# then open /import, choose the Painted Map source, and Load from studio
Load from studio pulls the painting, the legend and Planet.json straight out of the running terrain studio in about four seconds, strokes included (D-67). The studio answers GET and HEAD only and no
preflight, so no browser tab can ever paint, save, plan or bake.
Twelve seconds later there is a globe with rivers on it. Tune the legend's numbers in the table here, not after a two-hour bake.
4. Export the planet
Press Export Map. That writes the whole-planet PNGs into RawContent/World/Orogen Gens/ — the heightmap
(8192 × 4096, a fixed −5000…6000 m ramp), plus colormap, satellite, climate, landmask and the painted-layer
debug maps.
Do not use "Export All" for the heightmap. Its list is
{biome: Satellite, koppen: Climate, landheightmap: Heightmap, landmask: Land Mask}— the entry labelled "Heightmap" is the land variant, whoselandHeightmapColorreturns black forelevation <= 0, so every ocean pixel is 0 m. Import that and the sea floor sits flat at exactly sea level: z-fighting with the sea plane across 64 % of the world, no shelf and no shore. The fileRegion.jsonreads is the absolute heightmap, −5 km to +6 km, which has to be exported as a single layer. Both are 8192 x 4096 greyscale and neither says which it is, so the tell is the filename:orogen-heightmap-*.pngagainstorogen-land-heightmap-*.png.Orogen also numbers each export, not each planet.
orogen-colormap-14733759.pngandorogen-heightmap-7945.pnglook like two different worlds and are one. Nothing inside a PNG says which planet it is, so if you are ever unsure,cd Tools/MapArt && go run . checkmeasures land/sea agreement against the heightmap — 97.5 % is the same planet, 50 % is not.
5. Render the biome masks
The painting's classes and the Köppen climate become one blurred greyscale mask per biome, which is what the paint layers are built from. Go, because the painting is 29 megapixels of RGB and the engine's Python cannot decode it; blurred once globally so a tile can read it without carrying a margin the width of the blend.
cd Tools/MapArt && go run . biomes # a few seconds, into RawContent/World/Biomes/
It prints how far the worst pixel was from any legend colour. On the painting that is 0.0 — a painted map is made of its legend's own colours. A number much above that means the image is a render of a classification rather than the classification itself, which is the difference between data and a picture of data, and is exactly why Orogen's own class export is not the source here.
6. Cut the window into landscape tiles
RawContent/World/Region.json is the contract: which level, how many tiles, what a heightmap value means in
metres, and — the number that matters most — source.metres_per_pixel, because the export carries no scale of
its own.
# measure a window and print what it holds, writing nothing. Try scales here, not by rebuilding.
D:/UE_5.8/.../python.exe Scripts/Authoring/generate_region_tiles.py --scout
D:/UE_5.8/.../python.exe Scripts/Authoring/generate_region_tiles.py # ~2 min, 208 MB, untracked
7. Build the level
Scripts/Authoring/build_region.sh # the whole grid, a few tiles per process
Scripts/Authoring/build_region.sh --append # add whatever is still missing
One process per batch, because a landscape of a hundred components costs about a gigabyte the editor never gives back: thirty-six tiles in one process reached 14.7 GB by the ninth (D-69).
8. Build the map view
Scripts/Authoring/build_world_map.sh # renders the art, imports it, writes the definition
Independent of the level — it reads the same Orogen Gens/ images and Region.json, and writes
Content/World/Maps/. See Spec/UI.md for the map itself; M opens it in game, Window → World
Map in the editor.
What each artefact is, and whether it is tracked
| Path | What | In git? |
|---|---|---|
RawContent/World/Templates/ |
The painting, its legend, its plates. The real source. | tracked |
RawContent/World/Planet.json |
The planet's manifest: circumference, cell size, pipeline constants | tracked |
RawContent/World/Orogen Gens/ |
Orogen's whole-planet exports | tracked — they came out of a browser session and cannot be regenerated headlessly |
RawContent/World/Plan/ |
terrain plan output |
ignored, 4 s to rebuild |
RawContent/World/Bake_NNN/ |
terrain bake output: planet_height.png, the data maps, overlay.json, meta.json |
ignored, gigabytes, ~2 h to rebuild |
RawContent/World/RegionTiles/ |
98 tile sets: height + 3 weightmaps each | ignored, 208 MB, 2 min to rebuild |
RawContent/World/MapArt/ |
layers.json (tracked) and the rendered map layers (ignored, 6 s) |
mixed |
Content/Maps/L_World |
The level: 98 landscapes and ~500 external actor packages | tracked, through LFS |
Content/World/Maps/ |
DA_WorldMap_L_World and four map textures |
tracked, through LFS |
The rule is: an input is tracked, a product is not. The one exception is Orogen Gens/, because a browser
session is not something a script can redo.
Traps
Every one of these has cost real time.
- Never run a level build while the editor holds that level.
--rebuildempties the level first and saves last, so a lock detected late is indistinguishable from data loss — that is exactly what happened on 2026-09-20, leaving twelve of ninety-eight tiles, all of them the polar ocean strip, so the level opened on 71 km of sea and read as corrupted. Bothbuild_region.shandcreate_region_world.pynow probe the file before anything is destroyed (D-71a). - Run the long ones detached.
create_world.pyandterrain bakemust never go under a tool timeout. A killed bake is two hours. - A script's arguments go inside the quoted
-script=value. Anything after it is parsed by the engine and silently never reaches Python — which looks exactly like a script that ignored its arguments. create_world.pyempties whatever level it is handed. This is why the numpy canvas is namedL_Canvas_Protoand notL_World: a manifest still pointing at the latter would replace 98 landscapes with a 14 km square on one run, with no prompt (D-72).- Tile files are named after the level. Change
Region.json'sleveland all 98 tile sets look missing; rename the PNGs or 208 MB regenerates. - The heights out of Orogen are art. Fixed −5000…6000 ramp, land normalised to a browser preview's peak
setting. Only
sea_scalecorrects any of it, and only the sea. - A freshly opened
L_Worldcan look empty. The landscapes are split into world-partition streaming proxies and none is loaded; what you see is the sky, the fog and the sea plane at Z 0, which from above looks convincingly like soft terrain. Load a region, or build HLODs. - Do not trace for the ground in a commandlet. Landscape collision is not reliably there, the sea plane's
is, and a trace that hits the sea returns
0.0rather than failing. Read the heightmap instead.
What is not wired yet
Stated plainly so nobody goes looking for it.
- Route C.
terrain tileswrites 5 km tiles of 2500 samples at 2 m with hillshade, flow, wear and deposit maps beside each one, and nothing carries them into Unreal. This is the path that ends with ground worth standing on;generate_region_tiles.pyis the piece that changes. - The overlay in Unreal.
terrain overlayproposes woodland, settlements and roads, the studio has a Generate marks button, andoverlay.jsoncarries every feature in world metres.Region.jsonreserves anoverlayblock andregion_manifest.pyhas the slot for a per-tile mark map — neither is implemented. No forest, road or settlement is placed from one. It is left unbuilt rather than written blind because a carry that has never carried anything is a guess about a file format. - Overlay marks on the map view. They are in whole-cylinder normalised coordinates, exactly like the map
art, so they transfer by the same
u,v. This is the cheapest useful thing left in the list. - Erosion-derived paint layers in the region. Route A has no erosion pass and therefore no wear, flow or
deposit map, which is why its paint rules are slope and altitude alone.
L_Canvas_Proto— the legacy numpy pipeline — is still the only path that carries those maps into Unreal, and that is the only reason it is kept. - The coastal detail pass, which the shelf and shore platform have just unblocked. See
Terrain-Next.md.