14 KiB
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'slayersblock gainedbiomes(the two sources and the blend width) andpaint, an ordered list of layers each with a rule:slope,altitude,beach,class,climateorremainder.region_manifest.pymodels the list;LAYER_SUFFIXESsurvives as what a manifest with nopaintblock means.mapart biomesclassifies the two sources and writes one blurred greyscale mask per biome.generate_region_tiles.pysamples the masks and composes every enabled layer,--all-layersto 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 theLayerNameon 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_fogalready does this and the reason is inrocky_meadows.py. - Sun angle and colour chosen once and written down, because "the sun is wrong" has already cost one commit
(positional
unreal.Rotatoris(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
LandscapeGrassOutputto the landscape material, fed by the layer weights from Phase 1. - One
ULandscapeGrassTypeper 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
forestmark. - 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.jsonreserves anoverlayblock andregion_manifest.pyhasmarks_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 actualforestblobs 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.