Added: Initial world generation tool
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user