Files
UnrealPrototyping/RawContent/World/README.md
T

117 lines
8.9 KiB
Markdown

# 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 `Generated` edit layer, and a canvas of 7141 vertices at 200 cm — is settled in
> [`../../Docs/Terrain.md`](../../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.
```bash
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:
```json
"source": { "kind": "noise", "seed": 7 }
```
To use a real heightmap, point it at the file and say what its value range means in metres:
```json
"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.