Files
2026-09-25 17:02:24 +03:00

14 KiB
Raw Permalink Blame History

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 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.


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.