Files
UnrealPrototyping/Docs/World-Dressing.md
T
2026-09-25 17:02:24 +03:00

223 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dressing the world: substances, sky, light and things that grow
A plan, not a specification. It covers what has to happen for `L_World` to read as deserts, coasts, tropics and
craters rather than as one grass-and-rock material stretched over 2549 km², and for there to be a sky and trees
worth looking at.
[`World-Pipeline.md`](World-Pipeline.md) is how the ground gets made and imported; this is what it wears
afterwards. Everything here is **off the ladder** (D-47) and blocks no step in [`Steps.md`](Steps.md).
---
## What we already have, which is more than it looks
The single most important fact: **the biomes are already painted.** `Map5.legend.json` is not a set of
erodibility numbers that happen to have names — it is a per-pixel classification of the planet.
| Class | RGB | Is |
| --- | --- | --- |
| `ocean` / `deep` | 91,175,185 / 66,165,180 | sea, two depths |
| `ice` | 250,250,250 | ice cap |
| `lowland` | 150,200,105 | plains |
| `highland` | 71,175,100 | hill and mountain country |
| `desert` | 226,215,145 | arid |
| `crater` | 124,116,111 | impact ground |
That map exists at planet resolution in three forms already registered to the world by the same normalised
`u,v` as everything else: `Plan/map_class.png`, `Bake_NNN/map_class.png`, and
`Orogen Gens/orogen-painted-class-7945.png`. There is also `orogen-climate-7945.png`, Orogen's own climate
bands, which is where **tropical** comes from — it is the one biome you asked for that is *not* a painted class.
The overlay legend already carries `forest` (generation enabled), plus `city`/`town`/`village` and
`road`/`track`. `terrain overlay` and the studio's **Generate marks** button already place them.
And what we do **not** have:
| | |
| --- | --- |
| Substances | **Three.** `Base_Layer` (rock), `Layer_02` (meadow grass), `Layer_03` (high rock) — all from Elite_RockyMeadows, collected into `Content/Terrain/` (D-69a). No sand, no beach, no jungle floor, no ice, no crater regolith. |
| Trees | **None, in any pack.** HouseForge has one mushroom and a cover-plant material; Medieval_Weapons and RPGEnvironmentVFX have no ground cover at all. |
| Grass | **None.** `M_Ground_Landscape` has no `LandscapeGrassOutput` node, so nothing grows anywhere. |
| Sky | Elite_RockyMeadows' skybox mesh, sun with cloud shadows, sky light, exponential fog — scaled for world size in `rocky_meadows.py`. Serviceable; not authored. |
| Biome weightmaps | **Not carried.** `region_manifest.LAYER_SUFFIXES` is a fixed dict of three, and `Region.json`'s `layers` block derives them from slope and altitude alone. |
---
## Five decisions that gate the work
**All five are now taken** (D-74); the reasoning is kept because it is why the phases are shaped as they are.
The two that were a genuine fork went: **dress Route A now** rather than wiring Route C first, and **Fab/Quixel
Megascans** for the substances and the trees.
**1. Where the substances come from. `[DECIDED]` Fab/Quixel Megascans.** Free for Unreal use and has every
surface on the list — sand, shingle, jungle litter, regolith, ice — plus trees for Phase 5. Each goes into
`RawContent/Terrain/ground.json` exactly as the Rocky Meadows set did, so `collect_terrain_assets.py` keeps
owning `Content/Terrain/` and no pack is ever modified. **This is the one thing the plan needs from a person:**
the assets have to be added to the project from Fab before Phase 2 can start. Phase 1 does not wait on them.
**2. The layer budget. `[DECIDED]` eight layers.** This is the real constraint and it is not obvious. Unreal packs **four paint layers per
weightmap texture, per component**. We have three, so one texture each across **9800 components**. A fourth is
free; the fifth doubles it. The shader cost is linear too — every layer is a full material function sampled and
blended whether or not it contributes.
So the honest budget is **six to eight layers**, and that is the whole design:
| Layer | Rule | Substance |
| --- | --- | --- |
| `Base_Layer` | slope | **Layered_Rock_Cliff** (D-76) |
| `Layer_03` | altitude | the pack's high rock |
| `Layer_02` | remainder | the pack's meadow grass |
| `Beach` | near sea level, low slope | **Thai_Beach_Sand** |
| `Sand` | class `desert` | **Bright_Desert_Sand** |
| `Ice` | class `ice` | **Snow** |
| `Regolith` | class `crater` | **Desert_Western_Ground_Gravel_Coarse_04** |
**Seven, with one spare in the budget.** A `Jungle` layer reading Köppen `Af`+`Am`+`Aw` was specified and then
dropped (D-76): the nearest substance to hand is a mossy rocky ground, which is temperate and damp where a
tropical forest floor is leaf litter, and the wrong green over the whole equator is more misleading than no
biome at all. The equator wears the remainder layer instead — generic, but not wrong. Everything needed to
bring it back is still in place; it is one manifest entry and one substance.
**3. Route A or Route C first. `[DECIDED]` Route A, now.** Today's ground came from Orogen with **no erosion
pass**, so there is no wear, flow or deposit map to read — which is why the paint rules are slope and altitude
alone. `terrain tiles` already produces those maps at 2 m. Dressing Route A means sand where the *class* says
desert; dressing Route C means sand where sediment actually accumulated, and beaches where the coast pass
actually laid them.
Route C is better ground and is the point of the generator, but it is a whole world rebuild in front of the
dressing rather than after it, with nothing to look at until it finishes. Route A gives a visible result in
Phase 1 and **nothing authored for it is wasted**: when Route C lands it changes the *inputs* to the layer
rules, not the material, the sky, the grass or the trees. The two costs accepted are that beaches are
approximated from altitude and slope rather than taken from where the coast pass put them, and that no rule can
read sediment or wear.
**4. Whether trees need collision. `[DECIDED]` no, for now.** `LandscapeGrassOutput` is the only thing that scales to 2549 km² without
authoring actors — GPU-instanced, distance-culled, driven straight off the layer weights we are about to add,
and it costs nothing to author once the material has the node. But grass-output instances **have no collision**.
Trees you can walk into mean PCG or foliage actors, which is a different and much larger job at this scale.
Recommendation for a prototype: grass output for everything including trees, and revisit when a step needs to
collide with one.
**5. One sky or many. `[DECIDED]` one.** It is one planet, so it is one sun, one sky light and one skybox. "Desert lighting" and
"tropical lighting" as distinct looks means per-region post-process volumes, which at 98 landscapes is a lot of
actors to place and keep in step. Recommendation: one authored sky, and let the *ground* carry the biome.
---
## The plan
Each phase says what exists afterwards and what proves it. They are ordered so that nothing waits on anything
later.
### Phase 1 · Carry the biome into the tiles — **done**
The class map and the climate map become per-vertex layer weights, sampled at the same global coordinates as the
height, so they are seam-exact for the same reason it is.
- `Region.json`'s `layers` block gained `biomes` (the two sources and the blend width) and `paint`, an ordered
list of layers each with a rule: `slope`, `altitude`, `beach`, `class`, `climate` or `remainder`.
- `region_manifest.py` models the list; `LAYER_SUFFIXES` survives as what a manifest with no `paint` block means.
- **`mapart biomes`** classifies the two sources and writes one blurred greyscale mask per biome.
- `generate_region_tiles.py` samples the masks and composes every enabled layer, `--all-layers` to preview the
ones nothing can import yet.
**Three things it turned up.**
**The class source is the painting, not Orogen's class render.** The render is the legend's colours
double-encoded to sRGB, which puts exported `desert` nearer to legend `ice` than to legend `desert` — nearest
colour matching against it is simply wrong. The painting is made of the legend's own colours and nothing else:
measured, worst nearest-colour distance **0.0** over all 29 M pixels. One is data; the other is a picture of data.
**Tropical is Köppen, not latitude.** `Tools/Orogen/js/koppen.js` has a real classification with exact colours,
and the climate export decodes against it cleanly once the same sRGB encode is applied — every observed colour
within **1.4/255**, and only **0.423 %** of pixels not close to any class, which is the anti-aliased ring at
class boundaries and nothing else. Jungle is `Af`+`Am`+`Aw`, 8.11 % of the planet.
**The registration is measured, not assumed.** The painting is 7738 × 3761 and the heightmap 8192 × 4096; the
two candidate mappings were tested against each other on land/sea agreement and identity won, **98.09 % against
96.14 %**, in every latitude band including the polar ones where a vertical scale error shows first.
**Proved by:** seams **0 differing vertices** across all nine files on a shared column; weights **255..256 and
never under**, so no ground is unpainted; and the default three-layer path still produces the built world —
height, rock and high rock **bit-identical**, meadow differing in **6 of 6 507 601** vertices by 1, which is the
deliberate rounding change and nothing else.
**Left for Phase 2:** `rocky_meadows.LAYER_INFOS` still knows only three layers and now refuses loudly rather
than dropping one silently, which is the hook the substances plug into. Underwater ground gets the remainder
layer, because no rule claims the sea floor; the sea plane covers it, so it costs nothing but it does inflate
the meadow's share in any per-tile number. And the beach rule wants tuning against real coastline once there is
a sand substance to see — it lands at 0.1–0.2 % of a coastal tile today.
### Phase 2 · The project's own landscape material — **all but the layer infos**
Built by `Scripts/Authoring/build_ground_material.py`, which **extends** the pack's material rather than
replacing it: a new layer is a copy of an existing layer function with its six parameters renamed, so it
inherits the camera-distance colour blend D-69a found load-bearing at this scale. Verified: the
`LandscapeLayerBlend` carries seven wired layers, the new functions' parameters are uniquely prefixed so they
do not collide in the instance, and twenty texture parameters point the layers at the Fab substances.
The layer infos are done too. `LayerName` is read-only from Python and there is no LayerInfo factory in the
bindings - duplicate-and-rename produces an asset that silently keeps the name it was copied from - so it goes
through `ULandscapeAuthoringLibrary::CreateLayerInfo`, in the editor module for the same reason
`CreateLandscapeFromHeightmap` is. Verified per layer: layer info, `LayerName`, parameter prefix, substance,
and `weightmap_entries` resolving 7 of 7.
The notes below were the plan and are kept because they are still what the phase is for.
`M_Ground_Landscape` is already our copy (D-69a), so extending it modifies nothing of the pack's.
- Add the five new layers and their material functions, one per substance, following the three that exist.
- Keep the existing distance-blend structure — it is the reason the world does not read as tiling mush from the
air, and `MI_Ground_RockyMeadows`'s far-colour corrections are load-bearing at this scale.
- New layer infos in `Content/Terrain/Layers/`, and the `LayerName` on each must match what the material blends.
**Proves it:** fly the world; deserts are sand, the ice cap is ice, the crater floor is not grass.
### Phase 3 · Sky and light
Authored rather than inherited. One pass, and it is mostly numbers.
- A sky worth having: Sky Atmosphere plus Volumetric Clouds, or keep the pack's skybox mesh if the atmosphere
costs too much at this scale — decide by looking, not in advance.
- Exposure that works **both** standing on the ground and looking down from 25 km, which is the case that has
already bitten twice (the white sea material read as an ice sheet; the fog at demo density was opaque).
- Fog density stays scaled by world size — `scale_fog` already does this and the reason is in `rocky_meadows.py`.
- Sun angle and colour chosen once and written down, because "the sun is wrong" has already cost one commit
(positional `unreal.Rotator` is `(roll, pitch, yaw)`).
**Proves it:** a screenshot from the ground and one from 25 km, both legible, no blown highlights on the sea.
### Phase 4 · Ground cover
- Add a `LandscapeGrassOutput` to the landscape material, fed by the layer weights from Phase 1.
- One `ULandscapeGrassType` per layer that should have cover: grass on lowland, scrub on desert, none on rock,
ice or sea.
- Density and cull distance tuned at this scale, not the demo's.
**Proves it:** ground cover appears where the layer says and stops where it does not, and the frame time at
ground level is still sane.
### Phase 5 · Trees
- Tree meshes from the chosen source (decision 1), as grass types placed by the same output.
- Density driven by the layer weight and, where it exists, by the overlay's `forest` mark.
- Treeline: the overlay generator already takes its treeline from a quantile of the land's own heights, because
metres mean nothing until a world is baked (D-68). Use the same rule here.
**Proves it:** forests are where the overlay says, thin out with altitude, and are absent from desert, ice and
open water.
---
## What this does not cover
- **The overlay carry into Unreal is still unbuilt.** `Region.json` reserves an `overlay` block and
`region_manifest.py` has `marks_path`; nothing writes one and nothing reads one. Phase 5 can work off layer
weights alone without it — that is the cheaper path and the one to take first. Reading actual `forest` blobs
means building the carry.
- **Water.** The sea is `M_Sea_Proto`, a placeholder grey, deliberately. Real water is its own job and the
engine's single-layer water read as a lake shader stretched over a planet.
- **Rivers.** The bake knows where they are (`map_flow.png`); nothing in Unreal does.
- **HLODs.** Needed for any of this to be visible from the air at all, and they have to be rebuilt after the
material changes. See the sculpting note in `Terrain.md` §5.1 before hand-editing anything.