223 lines
14 KiB
Markdown
223 lines
14 KiB
Markdown
# 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.
|