The world's terrain source
This document describes the pipeline as it is built today. Where it is going — a Go core, stream-power erosion, plates and faults, a
Generatededit layer, and a canvas of 7141 vertices at 200 cm — is settled in../../Docs/Terrain.md(D-47). Nothing here is wrong yet; several things in it are scheduled to be replaced, and that document says which and by what.
L_World is a product of three inputs, none of them hand-edited: this folder's World.json (the manifest),
the PNGs in Heightmaps/ that Scripts/Authoring/generate_heightmap.py writes from it, and
Scripts/Authoring/create_world.py, which imports them into the level and dresses it with Elite_RockyMeadows'
kit. Change an input, rerun the two scripts, and the level is rebuilt from scratch.
D:/UE_5.8/Engine/Binaries/ThirdParty/Python3/Win64/python.exe Scripts/Authoring/generate_heightmap.py
D:/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_world.py -AllowCommandletRendering
The first needs numpy in Scripts/Authoring/.pylib (bootstrap-pylib.sh, once per machine) and takes about
five minutes at 4081, most of it erosion (40 s without). The second takes a minute or two (the landscape's
textures are built through the derived-data cache)
and must not be killed part way: run it detached, not under a tool with a timeout. It can run while the editor
is open, as long as the editor does not have L_World loaded at that moment. An existing level is loaded and
emptied rather than deleted, because an editor that has had L_World open keeps its two HLOD layer assets
locked and a recreation would fail to save; the previous build's proxy packages are swept after the save.
The manifest
| Key | Meaning |
|---|---|
vertices_per_side |
Heightmap resolution. 4081 gives 16x16 landscape components of 255 quads, one streaming proxy each; see the layout note below before changing it |
quad_cm |
Metres between vertices, in centimetres. 350 makes 4081 vertices 14.28 km, 204 km² |
elevation_m.min / .max |
What heightmap values 0 and 65535 mean, in metres. The landscape's Z scale and the actor's Z offset follow from these, so that elevation 0 m is world Z 0 |
sea_level_m |
Where the sea plane sits and what the noise source builds its continent around. Keep it 0 unless there is a reason |
spawn_pad_m |
Radius of the flat disc blended into the centre of the map for the player starts |
streaming_grid_components |
Landscape components per world-partition streaming proxy, per side |
source |
Where the height comes from; see below |
layers |
The paint-layer rules: rock by slope (rise over run), high rock by altitude (metres), and a noise break-up so boundaries are not contour lines |
Erosion
Whatever the source, the height then goes through heightmap_erosion.py before the layers are derived,
because fractal noise alone gives pillowy hills and no drainage. The erosion block of the manifest drives
it; every key has a default in heightmap_erosion.DEFAULTS, and "enabled": false skips the whole stage
(for a real DEM, which is already eroded).
| Key | Meaning |
|---|---|
coarse_factor, coarse_droplets, coarse_lifetime |
The first hydraulic pass runs on the map downsampled by the factor, with long-lived droplets (one cell per step, so 120 on 14 m cells is a 1.7 km path): this carves the valleys. Its result is applied to the full map as a delta, so the fine detail survives |
fine_droplets, fine_lifetime |
The second pass at full resolution, short-lived droplets: gullies and rills |
thermal_passes, talus_deg |
Thermal weathering with an angle of repose: where a cell stands above a neighbour by more than the angle allows, half of the largest excess slides down, shared among the lower neighbours. Mass is conserved, so cliffs keep a face and scree builds at their foot |
strata_period_m, strata_contrast |
Rock hardness as horizontal bands with a slow tilt, scaling the hydraulic erosion: hard bands hold shelves and ledges. Contrast 0 is uniform rock |
inertia, capacity, min_slope, deposit_rate, erode_rate, evaporation, gravity |
The droplet constants, in cell units (a slope of 1 is 45°), so they mean the same at both resolutions |
max_change, max_speed, max_load |
Brakes. Droplets step in vectorised batches that share cells; without a cap on what one droplet may cut or fill per step, a crowd in one cell runs away to infinity. The load cap bounds the mound a droplet can leave where it stops |
min_erode_slope, fine_scale |
Below min_erode_slope (rise over run) water deposits but barely cuts, so lowland soil holds and meadows stay smooth; fine_scale runs the full-resolution pass at a fraction of the cutting rate, so it leaves gullies rather than trenches |
Each droplet cuts through a 3x3 brush around its cell, not a single cell: a one-cell footprint leaves every path as a rill one cell wide, which reads as brush strokes across the lowlands. Deposits land on the droplet's own cell, so a pit fills to the brim and the droplets move on; spread through the brush, a pit's rim rises faster than its floor and every droplet draining into it adds to a mound.
The pass also writes four derivative maps next to the weightmaps: L_World_Flow.png (water passed, log scaled),
L_World_Wear.png (bedrock scraped), L_World_Deposit.png (sediment laid down) and L_World_Curvature.png
(128 flat, brighter convex, darker concave). The layer rules use them: scraped bedrock and convex ridges read as
rock, sediment fans and basins read as meadow. Nothing in the landscape material samples them yet; they are
there for the material that will.
Swapping the noise for a real heightmap
The source block decides. Today it is noise:
"source": { "kind": "noise", "seed": 7 }
To use a real heightmap, point it at the file and say what its value range means in metres:
"source": { "kind": "file", "path": "RawContent/World/Sources/my_area.png", "elevation_m": { "min": 0, "max": 2400 } }
Then rerun the two scripts. What happens to the file: it is read (16-bit greyscale PNG, or raw 16-bit
little-endian .r16/.raw with "width" given when it is not square; 8-bit PNGs are accepted and widened),
optionally flipped with "flip_y": true, cropped to a centred square, converted to metres with its own
elevation_m, resampled onto vertices_per_side (box-filtered when shrinking, bilinear otherwise; add
"smooth_passes": 2 to soften a coarse DEM that was scaled up), and re-encoded into the world's
elevation_m range, clipping and reporting anything outside it. The paint layers are derived from the finished
height by the same rules as for noise, so a real heightmap needs no weightmaps of its own, and the spawn pad is
blended in at the centre either way. Widen the world's elevation_m if the file's range does not fit; the
range costs nothing but height precision (65535 steps over the span: 4 cm at 2560 m).
Any DEM tool that writes 16-bit PNG or r16 works: QGIS (gdal_translate -ot UInt16 -scale), World Machine,
Gaea, terrain.party, the engine's own landscape export. A 30 m DEM of a 14 km area is only about 470 samples
across; it will be smooth after resampling, which is what smooth_passes and the layer break-up are for.
--source-file and --source-elevation on generate_heightmap.py try a file for one run without editing
the manifest; --seed does the same for noise.
The component layout
create_world.py hands the PNG to the engine's own importer, which picks the section size the way the
editor's Import button does: the largest of 255, 127, 63, 31, 15, 7 quads that divides vertices_per_side - 1
exactly, preferring one section per component. 4081 - 1 = 16 x 255, so 16x16 components of 255 quads.
The count matters more than the size: every component is a draw call and carries its own height and weight
textures, all built through the derived-data cache on import. Epic's own recommended 4033 would divide only
by 63, giving 64x64 components and a build four times as long for no visible gain. If you change the
resolution, pick 255 x N + 1 (or 127 x N + 1) with N at most 32.
Rocky Meadows' part
The pack contributes the landscape material (M_Landscape_Main_Inst_RockyMeadows02) and its three layer infos,
which is why the weightmaps carry the pack's names. Those names mislead: its Base_Layer samples the rock
textures, Layer_02 the grass, Layer_03 the high rock, so the meadow weightmap is L_World_Layer_02.png; the
sun with the pack's cloud-shadow light function; its skybox dome and sky light; its height fog and post-process
grade. The numbers are copied from the pack's Rocky_Meadows_01 demo map as Scripts/Authoring/dump_level.py
read them, and live at the top of create_world.py. The sea is a plane with the engine's water material,
World_Sea_Proto, until a water body replaces it.