325 lines
24 KiB
Markdown
325 lines
24 KiB
Markdown
# The world's terrain source
|
||
|
||
> **`L_World` is the planet-map region (D-72).** Two manifests live in this folder and they build different
|
||
> levels. `Region.json` builds `L_World`, the world the game uses — skip to
|
||
> [The region](#the-region-900-km-of-land-from-a-planet-map) for it. `World.json`, described first below,
|
||
> builds `L_Canvas_Proto`: the numpy pipeline's square 14.28 km canvas, legacy by D-47 and kept only because
|
||
> it is still the one path that carries the erosion pass's flow, wear and deposit maps into Unreal.
|
||
>
|
||
> The names are apart on purpose. `create_world.py` **empties whatever level it is handed** before rebuilding
|
||
> it, so a manifest still pointing at `L_World` would replace 98 landscapes with a 14 km square on a single
|
||
> run, with no prompt and no warning.
|
||
|
||
> 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_Canvas_Proto` 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_Canvas_Proto` loaded at that moment. An existing level is
|
||
loaded and emptied rather than deleted, because an editor that has had it 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_Canvas_Proto_Flow.png` (water passed, log scaled),
|
||
`L_Canvas_Proto_Wear.png` (bedrock scraped), `L_Canvas_Proto_Deposit.png` (sediment laid down) and `L_Canvas_Proto_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_Canvas_Proto_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, `World_Sea_Proto`, until a water
|
||
body replaces it.
|
||
|
||
It wears a **placeholder grey** (`/Game/World/M_Sea_Proto`, opaque and default-lit) rather than the engine's
|
||
single-layer water. The water material is a lake shader stretched over a whole planet here and reads at every
|
||
scale as something it is not; a plane that is honestly a placeholder is worth more while the ground is being
|
||
looked at than one pretending to be an ocean. `rocky_meadows.SEA_GREY` is the switch — set it `False` and the
|
||
water material comes back, unchanged and still the first thing tried. The material is authored on demand
|
||
rather than picked out of `/Engine`, because nothing there is the right value: `BasicShapeMaterial` is the
|
||
near-white that once read as an ice sheet to the horizon, and `WorldGridMaterial` puts a metre grid on a plane
|
||
seventy kilometres across.
|
||
|
||
`ensure_dressing` only spawns a sea when the level has none — correct, or a rerun would leave a second sun —
|
||
so changing the switch cannot by itself reach a world that already exists. That is what
|
||
`fix_sea_material.py` is for: it repaints the sea in a finished level and saves it, without rebuilding
|
||
anything.
|
||
|
||
```bash
|
||
D:/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe <abs>/Salty.uproject -run=pythonscript \
|
||
-script="<abs>/Scripts/Authoring/fix_sea_material.py --level /Game/Maps/L_World" \
|
||
-AllowCommandletRendering -unattended -nopause -abslog=<abs>/Saved/Logs/sea.log
|
||
```
|
||
|
||
Pass `--level` more than once for several worlds. It probes the `.umap` first and refuses when an editor holds
|
||
it, for the same reason the region builder does.
|
||
|
||
## The region: 900 km² of land from a planet map
|
||
|
||
`L_Canvas_Proto` above is the numpy pipeline's square 14.28 km canvas. **`L_World` is this**, built from a
|
||
different manifest: a **window cut out of a finished planet heightmap** and laid out as a grid of Unreal
|
||
landscapes, so there is ground at the scale the game wants long before the Go generator's detail passes reach
|
||
Unreal. Its contract is `Region.json`; every key in it is explained in `Scripts/Authoring/region_manifest.py`.
|
||
|
||
`Region.json`'s `level` decides which level is built **and what the tile files are called** — `tile_name`
|
||
is the level's last segment plus the tile's coordinates, so `L_World` means `L_World_x0_y0_Height.png`.
|
||
Change `level` and every tile in `RegionTiles/` looks missing; rename the PNGs to match or the generator
|
||
rebuilds all ninety-eight of them. `build_region.sh` reads the level out of the manifest for the same reason
|
||
it reads the grid from there: a name written into the script goes stale the moment the manifest changes.
|
||
|
||
```bash
|
||
cd Tools/MapArt && go run . biomes # the biome masks, if any paint layer reads one
|
||
D:/UE_5.8/Engine/Binaries/ThirdParty/Python3/Win64/python.exe Scripts/Authoring/generate_region_tiles.py --scout
|
||
D:/UE_5.8/Engine/Binaries/ThirdParty/Python3/Win64/python.exe Scripts/Authoring/generate_region_tiles.py
|
||
D:/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe <abs>/Salty.uproject -run=pythonscript \
|
||
-script=<abs>/Scripts/Authoring/create_region_world.py -AllowCommandletRendering -abslog=<abs>/region.log
|
||
```
|
||
|
||
**Do not leave the editor sitting on `L_World` while a build runs.** It holds a write lock on the `.umap`,
|
||
and the save is the *last* thing a batch does while `--rebuild` is the first: the run aborts on
|
||
`MoveFile … (Error Code 32)` having already emptied the level. That happened on 2026-09-20 and left twelve
|
||
of ninety-eight tiles, all of row y=0 — the polar strip, which on this planet is nearly all ocean — so the
|
||
level opened on 71 km of sea and read as a corrupted world. Both `build_region.sh` and
|
||
`create_region_world.py` now probe the file before anything is destroyed and refuse with a message naming
|
||
the cause. Load another level in the editor (or close it) and rerun; `--append` keeps whatever survived.
|
||
|
||
`--scout` measures the window and prints what it holds without writing anything: it is the cheap way to try a
|
||
scale or a position. The tiles take about two minutes and 208 MB (untracked; they are a product of the manifest
|
||
and the source). The level takes about ten minutes and must be run detached, with absolute paths and its own
|
||
`-abslog`: with the editor open and a relative project path the commandlet exits silently having done nothing.
|
||
|
||
### What it is today
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| Source | `Orogen Gens/orogen-heightmap-7945.png`, 8192x4096, the painted planet exported from the browser twin. The **absolute** heightmap (-5..6 km), not `orogen-land-heightmap-*.png`, which Orogen's "Export All" confusingly labels "Heightmap" and which has every ocean pixel at 0 m |
|
||
| Window | **the whole export**, 8192 x 4096 at (0, 0). Not a crop: every pixel of the planet is imported |
|
||
| Scale | 8.7158203125 m a source pixel, resampled to 2 m quads: a 4.36x upsample |
|
||
| Grid | 14 x 7 landscapes of 2551 vertices, 10 x 10 components of 255 quads each: **9800 components** |
|
||
| Extent | 71.40 x 35.70 km, 2549 km² of map holding about **925 km² of land**, elevation -1024..6144 m |
|
||
|
||
14:7 is exactly the export's own 2:1, so the *aspect* is undistorted and both axes come out at the same metres
|
||
per pixel, which is what `--scout` checks.
|
||
|
||
### Three things about it that are not obvious
|
||
|
||
> These three were written when the window was a 1355 px square at latitude -24 — 6 x 6 tiles, 30.60 km a
|
||
> side, 22.583 m a pixel. The window is the whole planet now and two of the numbers below moved with it, but
|
||
> the reasoning is unchanged and is why the manifest looks the way it does. **The second one got worse, not
|
||
> better:** a 1355 px window at latitude -24 stretched a uniform 9.6%, whereas reading the *entire* cylinder
|
||
> flat stretches by 1/cos(latitude) at every row, which is unbounded at the poles. The polar strips of
|
||
> `L_World` are therefore smeared east-west, and that is the price of importing the whole map rather than a
|
||
> patch of it. It is not a defect to fix here; it is the reason a window at middle latitudes was the original
|
||
> shape, and the reason Orogen's own export (below) cosine-corrects at the centre latitude.
|
||
|
||
**The source carries no scale, so the scale is a choice.** Orogen's heightmap export is a cylindrical
|
||
projection with a fixed -5000..6000 m ramp and nothing saying how wide the planet is. `Planet.json` says 100 km
|
||
round, and at 100 km the planet is 31.8 km *across*: a flat 30 km square is bigger than the planet and there is
|
||
no window to cut. `metres_per_pixel` is therefore a manifest number rather than something derived, and it is
|
||
the number that decides how much land a window can hold. 22.583 m a pixel is a 185 km circumference, chosen as
|
||
the finest reading whose best window still clears 900 km² of land. Coarser buys more land and blunter ground.
|
||
|
||
**The map is read flat, not unprojected.** A cylindrical map read flat stretches east-west by 1/cos(latitude);
|
||
this window sits at latitude -24 so its ground is 9.6% wider east-west than Orogen drew it. That is the price
|
||
of not projecting, and it is much cheaper than the alternatives: cos-correcting the crop stretches the window's
|
||
own edges by ±35% across the latitudes it spans, and a proper azimuthal projection of a patch two thirds the
|
||
width of the planet distorts more still. Keep a window at middle latitudes and the flat reading is a few
|
||
per cent. `--scout` prints the stretch it would cause.
|
||
|
||
**The heights are Orogen's, not the generator's.** Orogen normalises land so its 99.5th percentile stands at
|
||
the import page's peak setting, so these are a browser preview's metres. `terrain bake` makes this same
|
||
continent a plain about 116 m tall; the export makes it 2972 m. Treat the relief as art. `sea_scale` is the one
|
||
correction applied, and only to the sea: the export's abyss is 3 km down on a whole-planet ramp, which over a
|
||
30 km window is either a clipped plateau with a cliff at every shore, or an elevation range so wide the land
|
||
loses its precision.
|
||
|
||
### The other way to get the tiles: straight out of Orogen
|
||
|
||
`generate_region_tiles.py` cuts the tiles out of a whole-planet PNG. World Orogen can now write them
|
||
itself, which skips the PNG and, more usefully, **skips inventing the scale**. Open `Tools/Orogen` in a
|
||
browser, press **Export Map** then **Unreal Landscape…**, and point the folder picker at
|
||
`RawContent/World/`. It writes `RegionTiles/` and a `Region.json` beside it, in exactly the shape
|
||
`region_manifest.py` reads, so `create_region_world.py` and `build_region.sh` are unchanged.
|
||
|
||
What that buys, and what it does not:
|
||
|
||
- **The scale stops being a guess.** You give the planet's circumference (this project's is in
|
||
`Planet.json`: 100 km) and the export records `metres_per_pixel`, the window in degrees and the
|
||
projection into the manifest. `metres_per_pixel` in the hand-written manifest above is a number somebody
|
||
chose; here it is a consequence.
|
||
- **The window is a window**, not a crop of a planet-wide raster, so the sampling resolution is spent on
|
||
the ground you are cutting, and the heights come back as float rather than through a 16-bit ramp.
|
||
- **The projection is cosine-corrected** at the window's centre latitude, so the east-west stretch is split
|
||
between the north and south edges instead of landing entirely on one. The panel prints it.
|
||
- **It does not make the ground finer.** The mesh still resolves about 200 m. That is the next section.
|
||
- **It will not overwrite this `Region.json`.** Most of that file is the reasoning behind its numbers, so
|
||
when one is already there the export writes `Region.generated.json` beside it and says so; rename it over
|
||
the old one once you have read the difference. The tiles in `RegionTiles/` *are* overwritten.
|
||
- **The cutter cannot run against that manifest**, and says so. Its `source.kind` is `orogen_render` and it
|
||
names no file, because there is no PNG to re-cut from — the tiles came out of the browser. If a tile file
|
||
goes missing, `create_region_world.py` notices and reaches for `generate_region_tiles.py`, which now
|
||
stops with a message telling you to re-export from Orogen rather than a `KeyError` three frames down.
|
||
Everything else `region_manifest.py` exposes works unchanged, `metres_per_pixel()` included.
|
||
|
||
It also puts a hard number on something this document only implies. **A 100 km circumference is a 3183 km²
|
||
planet.** The 936 km² window above is 29% of its entire surface, which is why it comes out as a rectangle
|
||
110° on a side with 74.7% of east-west stretch at its edge. Nothing is wrong with the tiles that produces —
|
||
they are what a flat reading of most of a small globe looks like — but if you want a window that a sphere
|
||
this size can hold flat, it is a few hundred km², not nine hundred. The panel's default, 4 × 2 tiles, is
|
||
20.4 × 10.2 km and 208 km² at 5.4% stretch.
|
||
|
||
Requires Chrome or Edge on desktop (the File System Access API); the panel says so if the browser lacks it.
|
||
|
||
### What is missing, and where it comes from
|
||
|
||
The source resolves about 200 m — Orogen solves on a 204 K-region sphere mesh — so below that the ground is
|
||
smooth, and an 11x upsample cannot invent what is not there. There is no erosion pass here and therefore no
|
||
wear, flow or deposit map, which is why the paint layers are slope and altitude alone rather than L_World's
|
||
richer rules. None of that is a defect to be fixed here: the detail is the Go generator's job, and
|
||
`terrain tiles` already writes 5 km tiles of 2 m samples over a bake. When those tiles replace the window as
|
||
the source, `generate_region_tiles.py` is what changes and nothing downstream of it does.
|
||
|
||
### Seams
|
||
|
||
Neighbouring tiles share their edge vertices and every vertex is sampled from its **global** position in the
|
||
window, so a shared column is computed twice from the same source coordinates and comes out bit-identical;
|
||
nothing blends or stitches. The paint-layer break-up noise goes through `fbm_at` at global coordinates for the
|
||
same reason. The one thing that did not follow from this was slope: `np.gradient` takes a one-sided difference
|
||
at an array edge, which is not what the neighbour computes for that vertex, and every tile boundary came out as
|
||
a one-vertex line of different paint. Tiles are therefore sampled with one vertex of margin on each side, the
|
||
layers derived over the lot, and the margin cropped off.
|
||
|
||
### The overlay: reserved, not built
|
||
|
||
`Region.json` reserves an `overlay` block for the annotation layer (D-57), and `region_manifest.py` has the
|
||
place a tile's mark map would go (`marks_path`). **Neither is implemented.** No mark map is written, and
|
||
nothing here reads a mark: no forest, no road and no settlement is placed from one, which is deliberate — the
|
||
overlay is a feature this pipeline carries a slot for, not an input to any outcome in it.
|
||
|
||
It is left unbuilt rather than written blind because there is no overlay to run it against yet, and a carry
|
||
that has never carried anything is a guess about a file format. When there is one, this is the shape it should
|
||
take, and it is the shape `terrain tiles` already uses: an 8-bit mark index beside every tile, registered to
|
||
the same window and cut on the same global coordinates as the height, plus the features in world metres, with
|
||
the legend from `RawContent/World/Templates/*.overlay.json`. Until then the key is documentation of intent.
|
||
|
||
### Why the level can look empty, and where the terrain actually is
|
||
|
||
`ChangeGridSize` splits every landscape into world-partition streaming proxies: at
|
||
`streaming_grid_components` 5 over a tile's 10 x 10 components that is four proxies a tile, **144 over the
|
||
window**, and they hold *all* 3600 components. The thirty-six `Landscape` actors left behind are always-loaded
|
||
and carry **none**. So a freshly opened level shows only what is always loaded - the sun, the sky dome, the fog
|
||
and the sea plane - and the sea plane at Z 0, lit through the pack's cloud-shadow light function, looks
|
||
convincingly like soft terrain from above. It is not. `trace_world` straight down at the origin hitting Z 0
|
||
is the quick way to tell.
|
||
|
||
Nothing is lost when this happens: World Partition holds all 190 actor descriptors and reports the right world
|
||
bounds. Three ways to see the ground:
|
||
|
||
- **Load a region.** Window > World Partition, drag a box, right-click > Load Region. No rebuild, and it is
|
||
the intended editor workflow.
|
||
- **Build HLODs.** The World Partition window's Build HLODs button, or the
|
||
`WorldPartitionHLODsBuilder` commandlet. Unloaded ground then draws as proxy meshes, so the whole 936 km²
|
||
is visible from the air. This is the right answer at this size and `L_World` needs it too.
|
||
- **Stop splitting.** `streaming_grid_components: 0` skips `ChangeGridSize` entirely and leaves the components
|
||
on the always-loaded landscape - measured, 100 a tile instead of 0. The level then just opens showing
|
||
everything, at the cost of loading 3600 components at once and giving up streaming.
|
||
|
||
A related trap, and the reason the player starts were once 180 m underground: **do not trace for the ground in
|
||
a commandlet.** The landscape's collision is not reliably present there, the sea plane's is, and a trace that
|
||
hits the sea returns `0.0` rather than failing. Read the height out of the heightmap instead, as
|
||
`create_region_world.pad_height_cm` does.
|