Tooling
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:badf0d836e76b1cc8b7973805d05efdd3ac6741c04a0f688bf198eaffde45ff6
|
||||
size 1523788
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ddd2e473db089a11da24150c6b297beeba10339a5a7b28f2868f41ee5bbd5dc9
|
||||
size 10056654
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9d67c2896ad270faf7d86187df5acf47eb8d328a332f9acef565ad8580a8c6b
|
||||
size 694383
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a94885e7d50aaabb1646597e9c110f9ede8486adba67fbe351f38cda454030ac
|
||||
size 1483703
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:afde7495a094787517f6f81acf68d571b88b8ff725fa738eb36cfe49883c49d7
|
||||
size 9898252
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b792f62a059828c8a7e6caa9f30317cfe4429ac90855230d3cf9f58b3666fbbc
|
||||
size 481255
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:282f1d57737e3642a054c519d6995177a5682906c174d334d4659543f525838d
|
||||
size 1724227
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba01a70ca892fcfc04198523dd4c31dd7615847b79250fd4e136e57daa1e6e8e
|
||||
size 9582191
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8b192c33a7a1ca52e34c2063be28ca271b7ebb7bb00e17e1b54928ea8eb316b7
|
||||
size 588546
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c038cd5ac23af04893e82b9ac829a0bc7e3153ef5af70a49386f9296a8fd7a7
|
||||
size 1665074
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee7f6547bd805fa42dd665b4a280ccde6f8fe4c0eab8977683ad2a4574557761
|
||||
size 7966014
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6a7d0221f0acda6a4de585df6ae984a872453f26fc84d0b9d4416752f18ffc55
|
||||
size 714673
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:768bfbebe27ccb75dc8475e690db821977b606f87a6ac38c4593dfc222013ef8
|
||||
size 702044
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e2e1b5123eda98145c3f54c98ebc138d01659d7fa17ababbc6396d01a1575a5a
|
||||
size 5662640
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1cf96738cc3a767bb7d39c899191e1e151042314f8698d62c699759443c67a9
|
||||
size 1658373
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"maps": [
|
||||
"BaseColor",
|
||||
"Normal",
|
||||
"Roughness"
|
||||
],
|
||||
"resolution": 2048,
|
||||
"sets": [
|
||||
{
|
||||
"name": "Rock_Layered",
|
||||
"layer": "Base_Layer",
|
||||
"source": "Layered_Rock_Cliff-a1d10e7b",
|
||||
"physical_m": 2,
|
||||
"written": [
|
||||
"T_Rock_Layered_BaseColor.jpg",
|
||||
"T_Rock_Layered_Normal.png",
|
||||
"T_Rock_Layered_Roughness.jpg"
|
||||
],
|
||||
"from_px": 4096,
|
||||
"to_px": 2048,
|
||||
"seconds": 3.7319375
|
||||
},
|
||||
{
|
||||
"name": "Sand_Beach",
|
||||
"layer": "Beach",
|
||||
"source": "Thai_Beach_Sand-21c8230c",
|
||||
"physical_m": 2,
|
||||
"written": [
|
||||
"T_Sand_Beach_BaseColor.jpg",
|
||||
"T_Sand_Beach_Normal.png",
|
||||
"T_Sand_Beach_Roughness.jpg"
|
||||
],
|
||||
"from_px": 4096,
|
||||
"to_px": 2048,
|
||||
"seconds": 3.9402629
|
||||
},
|
||||
{
|
||||
"name": "Sand_Desert",
|
||||
"layer": "Sand",
|
||||
"source": "Bright_Desert_Sand-fe7bd740",
|
||||
"physical_m": 2,
|
||||
"written": [
|
||||
"T_Sand_Desert_BaseColor.jpg",
|
||||
"T_Sand_Desert_Normal.png",
|
||||
"T_Sand_Desert_Roughness.jpg"
|
||||
],
|
||||
"from_px": 4096,
|
||||
"to_px": 2048,
|
||||
"seconds": 3.8338872
|
||||
},
|
||||
{
|
||||
"name": "Snow",
|
||||
"layer": "Ice",
|
||||
"source": "Snow-2ddf31c1",
|
||||
"physical_m": 0.3,
|
||||
"written": [
|
||||
"T_Snow_BaseColor.jpg",
|
||||
"T_Snow_Normal.png",
|
||||
"T_Snow_Roughness.jpg"
|
||||
],
|
||||
"from_px": 4096,
|
||||
"to_px": 2048,
|
||||
"seconds": 3.1032527
|
||||
},
|
||||
{
|
||||
"name": "Gravel_Coarse",
|
||||
"layer": "Regolith",
|
||||
"source": "Desert_Western_Ground_Gravel_Coarse_04-900b9858",
|
||||
"physical_m": 2,
|
||||
"written": [
|
||||
"T_Gravel_Coarse_BaseColor.jpg",
|
||||
"T_Gravel_Coarse_Normal.png",
|
||||
"T_Gravel_Coarse_Roughness.jpg"
|
||||
],
|
||||
"from_px": 4096,
|
||||
"to_px": 2048,
|
||||
"seconds": 3.7498515
|
||||
}
|
||||
],
|
||||
"when": "2026-09-20T20:10:19Z"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"_comment": "What Content/Terrain/ is made of. Read by Scripts/Authoring/collect_terrain_assets.py, which copies each source into the destination, repoints the copies at each other so the set stands on its own, and reports anything still leaning on a pack. Adding ground is a line in `assets`, and a rerun: the script is idempotent and skips what is already there and current.",
|
||||
|
||||
"_comment_why": "The ground was spread across the asset packs, one folder per vendor, with no way to tell what the project actually uses as terrain from what came along with a pack. This collects it. Copies rather than moves, so a pack stays exactly as it shipped and can be updated or deleted without taking the ground with it; the cost is the bytes, which for the Rocky Meadows set is about 200 MB through LFS.",
|
||||
|
||||
"destination": "/Game/Terrain",
|
||||
|
||||
"_comment_naming": "The project's asset prefixes (CLAUDE.md): T_ textures, M_ master materials, MI_ instances, MF_ material functions. The pack's own names are kept for the textures, which already comply, and for the layer infos, whose asset names are load-bearing nowhere but whose `LayerName` property must keep matching the layer names the master material blends: Base_Layer, Layer_02, Layer_03. Those three names mislead and are the pack's, not ours: Base_Layer is the rock, Layer_02 the meadow grass, Layer_03 the high rock.",
|
||||
|
||||
"assets": [
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Grass_Color_02", "dest": "Textures/T_Grass_Color_02" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Grass_normal", "dest": "Textures/T_Grass_Normal" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Rock_Base_Color", "dest": "Textures/T_Rock_Base_Color" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Rock_Base_Normal", "dest": "Textures/T_Rock_Base_Normal" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Rock_High_Color", "dest": "Textures/T_Rock_High_Color" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Textures/T_Rock_Shade_Variation", "dest": "Textures/T_Rock_Shade_Variation" },
|
||||
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Functions/Base_Layer_function", "dest": "Materials/Functions/MF_Ground_Rock" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Functions/Layer_02_function", "dest": "Materials/Functions/MF_Ground_Meadow" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Functions/Layer_03_function", "dest": "Materials/Functions/MF_Ground_RockHigh" },
|
||||
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/M_Landscape_Main", "dest": "Materials/M_Ground_Landscape" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/M_Landscape_Main_Inst_RockyMeadows02", "dest": "Materials/MI_Ground_RockyMeadows",
|
||||
"_comment_parameters": "The two instances the pack ships share every texture and differ only in these colour multipliers, so which one a world uses decides what its ground *is*. RockyMeadows02, the one copied here, multiplies the meadow layer by (0.185, 0.248, 0.401) - a blue - at every distance, which is why a world painted 78% grass rendered as cold blue-grey rock from the air. These are Inst_Rocky's numbers instead, read from the pack's own Rocky_Meadows_01 demo map, where the meadow's far multiplier is a yellow-green and distant rock keeps its full brightness. They are applied to the copy; the pack is never modified. `Distance Blending Value Layer 02` is 20000 cm, so past 200 m it is the _Far colours alone that are seen - on a world tens of kilometres across that is almost all of it, which is what makes the far pair the ones that matter.",
|
||||
"parameters": {
|
||||
"vector": {
|
||||
"Base Texture Color": [0.406250, 0.353117, 0.304688, 1.0],
|
||||
"Base Texture Color Far": [1.000000, 1.000000, 1.000000, 1.0],
|
||||
"Layer 02 Texture Color": [0.131353, 0.151042, 0.125868, 1.0],
|
||||
"Layer 02 Texture Color_Far": [0.517693, 0.557292, 0.388244, 1.0],
|
||||
"Layer 03 Texture Color": [0.781250, 0.725061, 0.663249, 1.0]
|
||||
}
|
||||
} },
|
||||
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Layers/Base_Layer_LayerInfo", "dest": "Layers/Base_Layer_LayerInfo" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Layers/Layer_02_LayerInfo", "dest": "Layers/Layer_02_LayerInfo" },
|
||||
{ "source": "/Game/Elite_RockyMeadows/Materials/Material_Layers/Layer_03_LayerInfo", "dest": "Layers/Layer_03_LayerInfo" }
|
||||
],
|
||||
|
||||
"_comment_not_here": "Deliberately left in their packs. T_Cloud_Shadows_Tex and M_Cloud_Shadows_Inst02 dress the sun, not the ground, and belong with the sky kit in rocky_meadows.py. HouseForge_01's Textures/Stone is architecture - walls, stairs, a doorway - not terrain, and its Viking_Village_Props grass is a foliage card. Medieval_Weapons and RPGEnvironmentVFX have no ground at all. When a pack with real ground arrives, its substances go in `assets` above and nowhere else.",
|
||||
|
||||
"_comment_substances": "Ground that came from Fab rather than from a pack already in Content/. `assets` above duplicates assets the project already has; these are raw downloads that have to be *imported* first, which is a different job with a different tool. `mapart substances` reads this block, pulls the three maps the landscape material samples out of each set's nine, downsamples them to `resolution`, and writes them to `output_dir`; the import then has a small, clean folder to work from and never sees the 800 MB of 4K originals. Adding ground is a folder under `source_dir`, a line in `sets`, and a rerun.",
|
||||
|
||||
"_comment_resolution": "2048, not the 4096 they were downloaded at. The scans are 2 m across, so 4K is 2048 pixels per metre for ground that is almost always seen at grazing distance across a 71 km world - and a UTexture2D stores its source inside the uasset, so the resolution here is what decides whether this pipeline costs tens of megabytes through LFS or hundreds. Raise it if a substance turns out to be seen close up and looks soft; it is one number and a rerun.",
|
||||
|
||||
"_comment_maps": "BaseColor, Normal, Roughness. The pack's own ground samples colour and normal only, so this is already one map richer than what is there. AO, Cavity, Displacement, Gloss, Specular and Bump are downloaded and unused: AO and Cavity are baked into a scan's own colour at this distance, Displacement needs tessellation nobody has turned on, and Gloss is Roughness inverted.",
|
||||
|
||||
"substances": {
|
||||
"source_dir": "RawContent/Terrain/Source",
|
||||
"output_dir": "RawContent/Terrain/Substances",
|
||||
"resolution": 2048,
|
||||
"jpeg_quality": 92,
|
||||
"maps": ["BaseColor", "Normal", "Roughness"],
|
||||
|
||||
"_comment_sets": "`layer` is the paint layer in RawContent/World/Region.json this substance is the ground for, so the two files agree about what a layer is made of without either importing the other. `physical_m` is left out on purpose: it is read from the set's own Quixel metadata, because it is what the material needs to tile the texture life-size and a number copied by hand goes stale the moment a substance is swapped for one scanned at a different scale.",
|
||||
"sets": [
|
||||
{ "name": "Rock_Layered", "folder": "Layered_Rock_Cliff-a1d10e7b", "layer": "Base_Layer",
|
||||
"note": "Replaces the pack's rock on every steep slope in the world (D-76). A 2 m scan against a tiling texture; MI_Ground_RockyMeadows' colour corrections were tuned to the old one and will want re-reading." },
|
||||
{ "name": "Sand_Beach", "folder": "Thai_Beach_Sand-21c8230c", "layer": "Beach",
|
||||
"note": "Categorised surface/sand/beach in its own metadata, which is exactly what the shore layer wants." },
|
||||
{ "name": "Sand_Desert", "folder": "Bright_Desert_Sand-fe7bd740", "layer": "Sand",
|
||||
"note": "Deliberately a different set from the beach: one substance for both would make every shoreline read as the desert running into the sea." },
|
||||
{ "name": "Snow", "folder": "Snow-2ddf31c1", "layer": "Ice",
|
||||
"note": "Snow rather than the Frozen_Lake or Ice_Cliff sets beside it. An ice cap's surface is snow; blue ice is what a lake or a calving face looks like." },
|
||||
{ "name": "Gravel_Coarse", "folder": "Desert_Western_Ground_Gravel_Coarse_04-900b9858", "layer": "Regolith",
|
||||
"note": "Coarse gravel, which is what impact ground reads as. Nothing here is actually a regolith scan." }
|
||||
],
|
||||
|
||||
"_comment_unused": "Three downloaded sets have no layer and are not extracted. Frozen_Lake and Ice_Cliff are blue ice, which the ice cap does not want; Mossy_Rocky_Ground was the nearest thing to a jungle floor and is temperate rather than tropical, so the Jungle layer was dropped instead of being dressed wrongly (D-76). They stay in Source/ because the download is the expensive part, and Source/ is not tracked."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"_comment": "The world map's art: which planet-wide images become the layers of the in-game and in-editor map view, and how big they are when they get there. Read by Tools/MapArt (which downsamples and renders them into this folder) and by Scripts/Authoring/create_world_map.py (which imports the results and writes the definition asset). Adding a layer is a line here and a rerun of Scripts/Authoring/build_world_map.sh.",
|
||||
|
||||
"_comment_registration": "Every source here must be a whole cylinder of the SAME planet as the heightmap Region.json cuts L_World from, at any resolution. They register because L_World imports the entire export with no crop - Region.json's window is the full 8192x4096 - so normalised u,v on any of these images is normalised u,v on the world. A crop, a different planet or a different seed would misregister silently: there is nothing inside a PNG that says which world it is. `mapart check` is the guard, and it is a land/sea agreement against the heightmap rather than a hash, because the layers are different renders of one planet and no two of them are alike pixel for pixel.",
|
||||
|
||||
"source_dir": "RawContent/World/Orogen Gens",
|
||||
"output_dir": "RawContent/World/MapArt",
|
||||
"region": "RawContent/World/Region.json",
|
||||
|
||||
"_comment_wraps_x": "Does the map's left edge join its right? Only when Region.json's window is the whole width of the source, which is the case here - the export is imported entire. null works it out by comparing the window with the source's own width, which only the mapart report knows, because a PNG's size belongs in no manifest. Set it true or false to decide by hand. Getting it wrong is visible: a map that wraps when it should not lets a pan run off one edge and arrive at the other, and one that does not wrap when it should puts a hard edge down the middle of an ocean.",
|
||||
"wraps_x": null,
|
||||
|
||||
"_comment_output": "4096 x 2048 is 17.4 m a pixel over the 71.40 km world: a quarter of the source's 8192 and still far finer than the couple of hundred metres the Orogen mesh actually resolves, so no real detail is lost. Raising it is a number here and a rerun; the cost is LFS, about 11 MB a layer at this size and four times that at full resolution.",
|
||||
"output": { "width": 4096, "height": 2048 },
|
||||
|
||||
"_comment_package": "Where the imported textures and the definition asset land. The definition is what the game and the editor tab both read, and it carries the projection copied out of Region.json, so nothing at runtime needs RawContent to exist.",
|
||||
"package": "/Game/World/Maps",
|
||||
"definition": "DA_WorldMap_L_World",
|
||||
"level": "/Game/Maps/L_World",
|
||||
|
||||
"_comment_layers": "`render` says how a source becomes a layer. `copy` downsamples the image as it is, averaging in linear light, which is right for anything already coloured. `relief` is the one derived layer: it reads a 16-bit heightmap, shades it from a light in the north-west and tints it by altitude, which is what shows the landform a player will walk over - a colormap shows biome, not shape. `default` is the layer the map opens on.",
|
||||
"layers": [
|
||||
{
|
||||
"id": "relief",
|
||||
"name": "Relief",
|
||||
"file": "orogen-heightmap-7945.png",
|
||||
"render": "relief",
|
||||
"default": true,
|
||||
"note": "Shaded relief and a hypsometric tint off the same heightmap L_World's geometry came from, so the map and the ground are the same shape by construction rather than by agreement."
|
||||
},
|
||||
{
|
||||
"id": "colour",
|
||||
"name": "Colour",
|
||||
"file": "orogen-colormap-7945.png",
|
||||
"render": "copy",
|
||||
"note": "Orogen's own planet render. Was orogen-colormap-14733759.png until the 2026-09-21 re-export, which produced one carrying the planet's own number - the same picture of the same world, with a filename that no longer invites the question. Orogen numbers each export, not each planet, which is why the old one looked like a different world and was not."
|
||||
},
|
||||
{
|
||||
"id": "satellite",
|
||||
"name": "Satellite",
|
||||
"file": "orogen-satellite-7945.png",
|
||||
"render": "copy"
|
||||
},
|
||||
{
|
||||
"id": "climate",
|
||||
"name": "Climate",
|
||||
"file": "orogen-climate-7945.png",
|
||||
"render": "copy",
|
||||
"note": "Biome bands. Nothing in the project reads climate yet; it is here because it costs one line and it is the layer that answers 'why would there be forest here'."
|
||||
}
|
||||
],
|
||||
|
||||
"_comment_relief": "The relief render's knobs. The light is the cartographic convention, north-west, so ridges read as ridges rather than as trenches - a light from the south-east inverts the relief for most people looking at it. `exaggeration` multiplies the slope before shading, because a few hundred metres of relief spread over 17.4 m pixels is nearly flat and an honest hillshade of it is featureless grey. `land_top_m` is where the hypsometric ramp tops out, in metres on Region.json's elevation contract; null measures the 99.5th percentile of the land instead, which is what Orogen's export normalises to anyway.",
|
||||
"relief": {
|
||||
"light_azimuth_deg": 315,
|
||||
"light_altitude_deg": 45,
|
||||
"exaggeration": 4.0,
|
||||
"land_top_m": null,
|
||||
"shade_strength": 0.75
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e49829b06ad7710629c557bcfbd4da573454a3e22e2fffe1ebe8cc6254d1e490
|
||||
size 1002889
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e2cdeff872d6894efb9ff99d431c804a2c7a908adc64458ed67a421ef314266
|
||||
size 4243569
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:255b02e32fa12cda0a3c69f24bcf4aa14a54d385878d3e1132a9253ee11f1018
|
||||
size 5573941
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00c90ff98bfbc66a760d87a863a68f6e72a61274f74804b165dc173fa722d16c
|
||||
size 25318019
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ebb2fae628a7bc74948e92a62c638c574cc544abfef0b7c4d101abd8056df31b
|
||||
size 18476308
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e2882f0ee4f3ab5a63e7752c78583ae452332da23dee1d4d7883eb67b2a62d5
|
||||
size 317942
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9441c24d71c9709528e674379cff325cff54a9882bdcf68948c687aaf4cf1be9
|
||||
size 866718
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6310261284b7cc897a703236171f7c1dcfb4ee10519c8e3642c82dce5d622bd0
|
||||
size 403856
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5b5b1a66890da834db74c2db115c373847b3f5eceba33b3d96fc30b7c6830e4
|
||||
size 4399845
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:34e7f2f9e7ce506c7aa7d82d80d29aadd498192312708d39c94fd8493c36faa6
|
||||
size 560816
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3900b1c9b0d492913091c877a79a72ba044a66ddc1ed9da4de3e9905c80954d6
|
||||
size 553179
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:17808ae6f9233e64207190f7ed76abfd04dc8f682f0aebf5bbe5d90c1e357e13
|
||||
size 2155585
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9fbed3c0cc5e102891733f9c34256299b4f7cbf92935b8ab436399f310045c80
|
||||
size 2387319
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:689cf6b23359bc5916628c45f88a69fff695cb7485db794ff564405dacdf1909
|
||||
size 5372560
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"_comment": "A painted planet: Templates/Map3.jpg plus its legend, turned into terrain by the Go generator in Tools/Terrain. Read by `terrain plan` and `terrain bake` only - World.json next to this file is the square 14 km canvas the numpy pipeline and create_world.py still build, and the two do not share anything but their defaults. See Docs/Terrain.md.",
|
||||
|
||||
"level": "/Game/Maps/L_Planet",
|
||||
"quad_cm": 200,
|
||||
"sea_level_m": 0,
|
||||
|
||||
"_comment_elevation": "Wider than D-48's -512..1536, which is the square canvas's range and is tied to an exact Unreal Z scale of 400. A planet is not one Landscape, so that constraint does not bind here, and the trunk profiles are several times longer than the 14 km world's, which is where relief comes from. Span 3072 m over 16 bits is 4.7 cm a step, invisible at an 8 m cell. The clip fraction per region is the check: above 0.1% it is a failed run, not a rounded one.",
|
||||
"elevation_m": { "min": -1024, "max": 2048 },
|
||||
|
||||
"source": { "kind": "template", "seed": 7945 },
|
||||
|
||||
"planet": {
|
||||
"overlay": "Templates/Map5.overlay.png",
|
||||
"template": "Templates/Map5.png",
|
||||
"legend": "Templates/Map5.legend.json",
|
||||
|
||||
"_comment_overlay": "The annotation layer (D-57): a second painting the same size as the template, and a legend of marks saying what its colours stand for. It is where everything that is *placed on* the world rather than solved goes - forests, settlements, roads - and it carries exactly one instruction the generator reads, `coast_jitter`, which is how a coastline drawn by hand survives the waterline roughening below. Leave both keys out and there is no overlay, which is what every planet had before it. The image is created the first time it is saved from `terrain studio`, and versioned after that the same way the template is.",
|
||||
"overlay_legend": "Templates/Map5.overlay.json",
|
||||
|
||||
"_comment_palette": "How the preview is drawn, and nothing else - no height changes, so two bakes under two palettes are the same terrain. Swap the file to restyle the map. Leave the key out and the generator's own numbers are used, which is what this file holds; `terrain palette <path>` writes a fresh copy to start from.",
|
||||
"palette": "Templates/default.palette.json",
|
||||
|
||||
"_comment_scale": "100 km round at a 2 m detail quad and geology_factor 4 is 12500 x 6076 geology cells of 8 m. The 8 m cell is fixed by D-48 and must not be varied: stream power applied down to a single cell makes every divide steepen as the cell shrinks, so the same painting solved at two cell sizes would be two different worlds.",
|
||||
"circumference_km": 100,
|
||||
|
||||
"_comment_massif": "How big the upland fabric's blocks are - the thing a class's massif block cuts to decide where it is plain and where it stands up. One field for the whole planet, so a highland belt and the hills in the lowland beside it are parts of one structure. 7 km against landmasses of 20 to 45 km puts several blocks across every continent; at 12.5 the right-hand island came out entirely above the cut, which is the defect this exists to fix, only smaller. Rounded to a whole number of lattice cells in the noise period - `terrain plan` prints what it got.",
|
||||
"massif_wavelength_km": 7,
|
||||
|
||||
"_comment_reroll": "The two fields the seed re-rolls that the painting does not fix (D-58). Lithology cuts one planet-wide rock pattern into pipeline.lithology's types and multiplies each class's own k_mult by it, so a range has provinces in it rather than being one rock; the wavelength is how big a province is. The fault grain is only how the traces are *aimed* - which classes are faulted, and how hard, is the legend's `faults` block. Both are rounded to a whole number of lattice cells in the noise period, and `terrain plan` prints what they came to. Set either to 0 to switch that field off.",
|
||||
"lithology_wavelength_km": 8,
|
||||
"fault_grain_km": 45,
|
||||
|
||||
"_comment_plates": "The painted tectonic layer. A colour is a plate and the legend says how it moves; where two of them meet, whether that is a collision, a transform or a rift is worked out from the two motions. The faults block is the deformation zone around every margin - how wide, and how densely broken.",
|
||||
"plates": {
|
||||
"layer": "Templates/Map5.plates.png",
|
||||
"legend": "Templates/Map5.plates.json",
|
||||
"faults": { "zone_km": 6, "per_1000km2": 90, "throw_m": [80, 420], "length_km": [4, 16] }
|
||||
},
|
||||
|
||||
"ocean_margin_km": 0.5,
|
||||
"min_land_cells": 16,
|
||||
"uplift_variation": 0.30,
|
||||
"_comment_coast": "A drawn shore is a smooth curve and a coastline is not - that is the Richardson paradox, and projected straight the painting's own smoothness survives all the way to the heightmap. The waterline is roughened before projection by adding fractal noise to the signed distance from it: land juts out where the noise is positive and the sea reaches in where it is negative, so bays come out about wavelength wide and up to amplitude deep. Both are in template pixels, which are 12.92 m here. The amplitude is capped per cell at two thirds of the widest land within reach, or a wavelength far larger than an islet would take the whole islet at once and an archipelago would vanish between two runs. Tune it in `terrain studio`; --coast-jitter, --coast-wavelength, --coast-octaves and --coast-gain override it for one run.",
|
||||
"coast_jitter_px": 68,
|
||||
"coast_jitter_wavelength_px": 384,
|
||||
"coast_jitter_octaves": 5,
|
||||
"coast_jitter_gain": 0.65
|
||||
},
|
||||
|
||||
"pipeline": {
|
||||
"geology_factor": 4,
|
||||
|
||||
"_comment_lithology": "The rock types the lithology field is cut into, equal shares of the planet each. The generator's own defaults are [0.5, 1.0, 3.0], a six-fold span, which was chosen for the square canvas where lithology was the *only* thing varying K; here it multiplies a class's k_mult on top of everything else the legend says, so a narrower span is what reads as one range made of several rocks rather than as two different worlds meeting inside it.",
|
||||
"lithology": { "types": 3, "k_multipliers": [0.6, 1.0, 1.8] }
|
||||
}
|
||||
}
|
||||
+216
-8
@@ -1,11 +1,21 @@
|
||||
# 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_World` is a product of three inputs, none of them hand-edited: this folder's `World.json` (the manifest),
|
||||
`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.
|
||||
@@ -19,8 +29,8 @@ The first needs numpy in `Scripts/Authoring/.pylib` (`bootstrap-pylib.sh`, once
|
||||
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_World` loaded at that moment. An existing level is loaded and
|
||||
emptied rather than deleted, because an editor that has had `L_World` open keeps its two HLOD layer assets
|
||||
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
|
||||
@@ -58,8 +68,8 @@ path as a rill one cell wide, which reads as brush strokes across the lowlands.
|
||||
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_World_Flow.png` (water passed, log scaled),
|
||||
`L_World_Wear.png` (bedrock scraped), `L_World_Deposit.png` (sediment laid down) and `L_World_Curvature.png`
|
||||
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.
|
||||
@@ -109,8 +119,206 @@ resolution, pick `255 x N + 1` (or `127 x N + 1`) with N at most 32.
|
||||
|
||||
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_World_Layer_02.png`; the
|
||||
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 with the engine's water material,
|
||||
`World_Sea_Proto`, until a water body replaces it.
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"_comment": "The whole Orogen planet export, scaled so it holds about 900 km2 of land, cut into Unreal landscapes. Read by Scripts/Authoring/generate_region_tiles.py and create_region_world.py through region_manifest.py, which is where every key is explained. World.json beside this file is the square 14.28 km canvas the numpy pipeline builds and shares nothing with this but the height contract.",
|
||||
|
||||
"_comment_level": "L_World: there is one world level and this builds it. The square 14.28 km canvas that used to own this name is the numpy pipeline's and is legacy (World.json beside this file now points at L_Canvas_Proto). The tiles are named after the level's last segment, so they are L_World_x0_y0_Height.png and the like.",
|
||||
"level": "/Game/Maps/L_World",
|
||||
|
||||
"_comment_tiles": "14 x 7 landscapes of 2551 vertices. 2550 quads is 10 x 255, so the engine gives each tile 10 x 10 components of 255 quads: 100 a tile, 9800 over the window. Neighbours share their edge vertices, so the grid is 35701 x 17851 vertices, 71.40 x 35.70 km, 2549 km2 of map holding about 925 km2 of land. 14:7 is exactly the export's own 2:1, so the ground is not stretched. 637 M vertices is not an import any single Landscape actor will take; ninety-eight of 6.5 M each is the same work in ninety-eight resumable pieces.",
|
||||
"tiles": { "columns": 14, "rows": 7, "vertices": 2551 },
|
||||
|
||||
"quad_cm": 200,
|
||||
"sea_level_m": 0,
|
||||
"spawn_pad_m": 150,
|
||||
|
||||
"_comment_streaming": "Components per world-partition streaming proxy, per side. 5 over a tile's 10 x 10 gives four proxies a tile, 2.55 km each.",
|
||||
"streaming_grid_components": 5,
|
||||
|
||||
"_comment_elevation": "The whole export spans -5000..5988 m on Orogen's fixed ramp; after sea_scale that is -850..5988. -1024..6144 spans 7168 m, an exact Z scale of 1400, and 10.9 cm a step over 16 bits. Nothing clips at either end.",
|
||||
"elevation_m": { "min": -1024, "max": 6144 },
|
||||
|
||||
"source": {
|
||||
"_comment": "A cylindrical (2:1 equirectangular) planet heightmap, read flat. Orogen writes 16-bit greyscale on a fixed ramp: 0 is -5000 m and 65535 is +6000 m, whatever the planet, because the ramp is absolute so the same shade always means the same height. Its land is normalised before that, scaled so the 99.5th percentile stands at the import page's peak setting, so these metres are Orogen's and not the Go generator's: the same painting baked by `terrain bake` makes its largest continent a plain about 116 m tall. Treat the heights as art.",
|
||||
"kind": "planet_map",
|
||||
"path": "RawContent/World/Orogen Gens/orogen-heightmap-7945.png",
|
||||
"elevation_m": { "min": -5000, "max": 6000 },
|
||||
|
||||
"_comment_scale": "What one source pixel is worth on the ground. The file carries no scale of its own, so this is a choice, and it is the choice that decides how much land the world holds. 8.7158 m a pixel puts the whole 8192 x 4096 export on 71.40 x 35.70 km: 2549 km2 of map and, at the export's 36.3% land, about 925 km2 of land. Both axes come out at the same metres per pixel, which is what `--scout` checks: the export is 2:1 and so is a 14 x 7 grid of square tiles, so nothing is stretched.",
|
||||
"metres_per_pixel": 8.7158203125,
|
||||
|
||||
"_comment_window": "The rectangle of source pixels the world is cut from. This is the whole export - every pixel of the planet is imported, not a crop of it.",
|
||||
"window": { "x": 0, "y": 0, "width": 8192, "height": 4096 },
|
||||
|
||||
"_comment_sea_scale": "Multiplies everything below sea level. Orogen's abyss is 5 km down on a ramp built for a whole planet, and 64% of this map is ocean; 0.17 brings the deepest water to -850 m, which fits the elevation range with nothing clipped and leaves the shore a slope instead of the cliff a clamp would cut. Land is untouched.",
|
||||
"sea_scale": 0.17
|
||||
},
|
||||
|
||||
"_comment_overlay": "Reserved for the annotation layer (D-57) and NOT IMPLEMENTED. There is a place for a per-tile mark map in region_manifest.py and nothing writes one; no pass here reads a mark, and no forest, road or settlement is placed from one. That is deliberate - the overlay is a feature this pipeline keeps a slot for, not an input to any outcome in it - and it is left unbuilt rather than written blind because there is no overlay to run it against yet. The README next to this file says the shape it should take when there is one.",
|
||||
|
||||
"_comment_layers": "The paint-layer rules. Three of them read the height alone - rock by slope (rise over run), high rock by altitude in metres, and a noise break-up so no boundary is a contour line - because there is no erosion pass on this route and therefore no wear, flow or deposit map to read (D-74). The rest read the *biome*, which is a different kind of input and is described under `biomes` below.",
|
||||
"layers": {
|
||||
"rock_slope_start": 0.55,
|
||||
"rock_slope_full": 1.05,
|
||||
"high_altitude_start_m": 1400,
|
||||
"high_altitude_full_m": 2000,
|
||||
"breakup_m": 18,
|
||||
"breakup_cells": 24,
|
||||
"breakup_seed": 7,
|
||||
|
||||
"_comment_beach": "The shore layer, from height and slope alone. `above_sea_m` is full strength up to that height, fading out by `fade_m`, and only where the ground is flatter than `max_slope` so a cliff foot is rock and not sand. This is the one biome that is an approximation rather than a reading: the bake's coast pass knows where beaches actually are (it reports beach_km2 and planed_km2) and this route does not carry that, which D-74 accepted as the price of dressing Route A now.",
|
||||
"beach": { "above_sea_m": 12, "fade_m": 25, "max_slope": 0.18 },
|
||||
|
||||
"_comment_biomes": "Where the biome layers come from. Two categorical maps of the whole planet, both read by identity in normalised u,v - the same coordinates the height is sampled at, so a biome and the ground under it cannot drift apart. That identity is measured, not assumed: the painting is 7738x3761 and the heightmap 8192x4096, and the two candidate registrations were tested against each other on land/sea agreement, where identity scored 98.09% against 96.14% and won in every latitude band including the polar ones.",
|
||||
"_comment_class_source": "The *painting*, not Orogen's class render. A painted map is made of its legend's own colours and nothing else - measured, worst nearest-colour distance 0.0 - whereas the render is those colours double-encoded to sRGB, which puts exported `desert` nearer to legend `ice` than to legend `desert`. One is data; the other is a picture of data.",
|
||||
"_comment_climate_source": "Koppen classes, read from Orogen's climate export against the palette in Tools/Orogen/js/koppen.js. The palette is parsed out of the JavaScript rather than copied beside it, because that is where it is used and two copies is how one goes stale. Encoding the table's linear colours to sRGB reproduces every observed colour to within 1.4/255.",
|
||||
"biomes": {
|
||||
"class_image": "RawContent/World/Templates/Map5.png",
|
||||
"class_legend": "RawContent/World/Templates/Map5.legend.json",
|
||||
"climate_image": "RawContent/World/Orogen Gens/orogen-climate-7945.png",
|
||||
"koppen_js": "Tools/Orogen/js/koppen.js",
|
||||
"masks_dir": "RawContent/World/Biomes",
|
||||
"_comment_blend_m": "How wide a biome boundary is on the ground. Blurred once, globally, in the source's own pixels by `mapart biomes`, so a tile reads an already-smooth field and two tiles agree at a shared vertex without either carrying a margin the width of the blend.",
|
||||
"blend_m": 400
|
||||
},
|
||||
|
||||
"_comment_paint": "The landscape's paint layers, in order. `name` is what the material blends and what the weightmap file is called. Eight is the budget and the budget is the design (D-74): Unreal packs four layers per weightmap texture per component and there are 9800 components, so the fourth layer is free and the fifth doubles the weightmap memory. `enabled` false is written by no one and imported by no one - the five biome layers stay off until Phase 2 lands a substance and a layer info for each, so today's build is unchanged. `mapart biomes` still writes their masks either way, which is what makes them inspectable before anything is imported.",
|
||||
"_comment_rules": "slope: rock, by gradient. altitude: high rock, by metres. beach: the shore rule above. class: the painted classes named. climate: the Koppen codes named. remainder: whatever is left, which exactly one layer must claim.",
|
||||
"paint": [
|
||||
{ "name": "Base_Layer", "rule": "slope", "enabled": true, "note": "the rock. The pack's name; it is not a base." },
|
||||
{ "name": "Layer_03", "rule": "altitude", "enabled": true, "note": "high rock." },
|
||||
{ "name": "Layer_02", "rule": "remainder", "enabled": true, "note": "the meadow grass, and whatever no other layer claimed." },
|
||||
|
||||
{ "name": "Beach", "rule": "beach", "enabled": true, "note": "Thai_Beach_Sand. The one biome that is an approximation rather than a reading; see _comment_beach." },
|
||||
{ "name": "Sand", "rule": "class", "enabled": true, "classes": ["desert"] },
|
||||
{ "name": "Ice", "rule": "class", "enabled": true, "classes": ["ice"], "note": "Snow, not blue ice: an ice cap's surface is snow. Its scan is 0.30 m where every other substance is 2 m, so it tiles nearly seven times more densely and the material must not use one tiling number for every layer." },
|
||||
{ "name": "Regolith", "rule": "class", "enabled": true, "classes": ["crater"] }
|
||||
],
|
||||
|
||||
"_comment_no_jungle": "There was a Jungle layer here reading Koppen Af+Am+Aw, and it was dropped rather than dressed wrongly (D-76): the nearest substance available 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 - meadow grass - which is generic but not wrong. Everything needed to bring it back is still here: `mapart biomes` still knows the rule, layers.biomes still names the climate source and the Koppen table, and reinstating it is this entry restored plus a substance."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60ddbf49da40e2ab2e1b237d2059716e7c3570723046b98b24f1e332b9824a4f
|
||||
size 2015822
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f55009607fcb2a5ea782300de4ea81d34ea2ef3d955d5529434b8e85f59122fd
|
||||
size 537370
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c567c9c46586f7276441bffeff9f367f2e91cb3c86a0dfe6fe7f8fc4c386b81
|
||||
size 2263576
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"_comment": "What each painted colour in Map3.jpg means. See README.md next to this file. Rates are rock uplift in mm/yr, and the number to think in is not the rate but the hillslope angle it fixes: at an 8 m cell and K 5e-5, tan(angle) = 2.5 x the rate, so 0.08 is 11 degrees on every divide and 0.25 is 32. Do not read internal/stats' \"plain below 0.1 mm/yr\" as terrain - it is a reporting bucket, and 0.1 mm/yr is hill country. `terrain plan` prints the angle and what it reads as for every class here.",
|
||||
"image": "Map3.jpg",
|
||||
"warn_distance": 60,
|
||||
"classes": [
|
||||
{ "name": "ocean", "rgb": [ 91, 175, 185], "sea": true, "depth_m": 512 },
|
||||
{ "name": "deep", "rgb": [ 65, 165, 180], "sea": true, "depth_m": 512 },
|
||||
{ "name": "shelf", "rgb": [153, 204, 221], "sea": true, "depth_m": 120 },
|
||||
{ "name": "surf", "rgb": [221, 238, 238], "sea": true, "depth_m": 20 },
|
||||
|
||||
{ "_comment_ice": "snow is display and material only - it changes no height and no pass reads it, it just stops the polar caps rendering as meadow. What makes the cap read as ice in the terrain is the detail block: an ice sheet has meltwater channels but not a dendritic bedrock gully network, so it gets almost no droplets. Uplift is low and the cap is still solved like any other land; a proper ice dome would be a stamp after the solve, the way the crater is.",
|
||||
"name": "ice", "derived": true, "rgb": [250, 250, 250], "uplift_mm_yr": 0.05, "k_mult": 1.0,
|
||||
"snow": true, "lithology_mix": 0,
|
||||
"detail": { "droplets_per_cell": 0.02, "strata_contrast": 0.15, "amplitude_m": [1, 3] } },
|
||||
|
||||
{ "_comment_plain": "coastal_plain_km puts the range inland. For n=1 the uplift rate alone fixes the hillslope angle, so a uniformly painted island sits at the angle of repose right down to the water; this ramps the rate up from coastal_floor_mm_yr over the first few km so there is a plain in front of the range. It is the opt-in, explicit version of the taper D-52 removed - that one was hidden and went to zero at the waterline.",
|
||||
"_comment_massif": "The massif is what makes this a landscape rather than a landmass. 0.08 mm/yr is an 11 degree hillslope, and before this block it was 11 degrees on every divide from the waterline to the summit, so the whole continent came out as continuous hill country with no flat ground anywhere on it. Now 0.08 is the rate a *hill mass* reaches and 0.012 - 1.7 degrees, a plain - is the ground between them, with a sixth of it standing up. That is the North European Plain with the Harz and the Ardennes in it, which is what a lowland is.",
|
||||
"name": "lowland", "rgb": [153, 204, 102], "uplift_mm_yr": 0.08, "k_mult": 1.0,
|
||||
"massif": { "floor_mm_yr": 0.012, "fraction": 0.16 },
|
||||
"coastal_plain_km": 1.0, "coastal_floor_mm_yr": 0.012 },
|
||||
|
||||
{ "_comment_faults": "A fault is a difference in uplift rate across a line - steep on one side, gentle on the other - which erosion then carves into a scarp. It is what puts an escarpment and a straight fifteen-kilometre valley inside a range, and neither is something the painting can draw. throw_m is the whole step across the fault over the run - how much higher the upthrown side would stand than the downthrown one if nothing eroded either, which since D-62 is the step and not a full throw on each flank, so a number written before that asks for half what it used to get. A fault is a range front several kilometres wide, not a line: the rate crosses over about a kilometre and the upthrown flank reaches six, which is what lets erosion cut valleys into it instead of printing the profile on the surface. Where several of them overlap - which at that width is most of a faulted class, and they are sub-parallel by design - the total saturates at 1.6 times the strongest single fault rather than adding up (D-63), so raising per_1000km2 past the overlap point buys texture and not height; per_1000km2 is a density over *this class*, which on this template is 549 km2 of highland, so 25 is about fourteen traces before the long ones step into segments. Drop the block to have none, which is what a plain should have.",
|
||||
"name": "highland", "rgb": [ 68, 170, 102], "uplift_mm_yr": 0.25, "k_mult": 1.0,
|
||||
"faults": { "per_1000km2": 25, "throw_m": [120, 400], "length_km": [6, 18] },
|
||||
"_comment_clamped": "0.90 mm/yr is 66 degrees at a divide against a 35 degree angle of repose, so the repose clamp shapes this class rather than erosion does and the ground bakes out as flat polygonal facets. The ceiling is U = tan(talus)*K*cell = 0.280 mm/yr at an 8 m cell; `terrain plan` prints the angle for every class. Dropping to about 0.25 gives erosion-shaped mountains at roughly 350 m on a 20 km island instead of 695 m of talus - relief and steepness are the same knob at a fixed cell, so that is the trade.",
|
||||
"_comment_massif": "A range is a belt, not a dome. Painted solid, this class used to raise every cell of an island to 32 degrees, which is why the first planet had two islands that were nothing but mountain. 0.25 is now the rate the belt reaches and 0.045 - 6.4 degrees, rolling - is its foreland, with three tenths of it standing up. It cuts the same fabric as the lowland beside it, so the hills in the neighbouring lowland are that orogen's outliers rather than an unrelated noise.",
|
||||
"massif": { "floor_mm_yr": 0.045, "fraction": 0.30 },
|
||||
"coastal_plain_km": 4.0, "coastal_floor_mm_yr": 0.03 },
|
||||
|
||||
{ "_comment_desert": "A desert is not a low uplift rate - a wet lowland has one of those too - so at the geology grid it is only k_mult, which being below 1 means less water doing less work and therefore steeper, more angular ground held further from being worn down. What makes it read as desert is the detail block, at two metres: a fifth of the running water, so the dendritic gully network thins out to isolated wadis; twice the strata contrast, so mesas and ledges keep their edges because nothing is rounding them off; and several times the noise amplitude, which on flat ground is dune.",
|
||||
"_comment_massif": "k_mult 0.5 doubles the angle a rate makes - tan(angle) = 2.5 x rate / k_mult - so 0.10 here is 27 degrees, not 14, and painted solid it made a plateau of mountain. 0.10 is the rate a mesa block reaches and 0.015 - 4.3 degrees - is the sand sea between them.",
|
||||
"name": "desert", "rgb": [238, 221, 153], "uplift_mm_yr": 0.10, "k_mult": 0.5,
|
||||
"faults": { "per_1000km2": 10, "throw_m": [60, 220], "length_km": [5, 14] },
|
||||
"massif": { "floor_mm_yr": 0.015, "fraction": 0.14 },
|
||||
"coastal_plain_km": 1.5, "coastal_floor_mm_yr": 0.015,
|
||||
"detail": { "droplets_per_cell": 0.035, "strata_contrast": 0.92, "amplitude_m": [7, 14] } },
|
||||
|
||||
{ "_comment_crater": "An impact is an event, not a rate, so it is stamped onto the finished terrain after the solve rather than painted as uplift - a closed basin built out of negative uplift would be filled in by the priority-flood within a hundred steps. The shape comes from the painted blob: distance in from its own shoreline, normalised by its widest point, so rim_at and wall_at describe every crater whatever size it was drawn. The uplift rate here is only what the solve does to the flanks before the stamp lands, so it is low.",
|
||||
"name": "crater", "rgb": [124, 117, 111], "uplift_mm_yr": 0.15, "k_mult": 1.5,
|
||||
"lithology_mix": 0,
|
||||
"crater": { "rim_m": 340, "floor_m": 60, "rim_at": 0.30, "wall_at": 0.62 } },
|
||||
|
||||
{ "_comment": "White is painted twice: the polar caps and the outline stroke around every island. The stroke owns the colour, because it is the one that has to be recognised wherever it appears; a white region touching the top or bottom row of the map is a cap and becomes ice instead. Everything else white dissolves into whichever real class is nearest.",
|
||||
"name": "stroke", "rgb": [238, 238, 238], "stroke": true, "edge_class": "ice" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f76becfa11e4e6282caa01521a18802e6564f5311cde2c0cf98597d6deab4887
|
||||
size 187973
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"_comment": "A painted tectonic layer: one colour per plate, and how that plate is moving. heading_deg is a compass bearing over the map - 0 points at the top of the image, 90 to the right, 180 to the bottom. speed_cm_yr is drift; what happens at a margin is the difference between the two plates either side of it, so two plates drifting the same way are a boundary doing nothing. spin_deg_myr turns a plate about its own centre, positive clockwise, and it is worth setting on at least one: without it every margin is the same all the way along, and with it one end collides while the other slides. Paint the plates, not the mountains - where two of these meet, the collision, the belt and its faults are worked out from the motions. Repaint the blobs freely; only the colours have to keep matching this file.",
|
||||
"image": "Map3.plates.png",
|
||||
"warn_distance": 60,
|
||||
"plates": [
|
||||
{
|
||||
"name": "plate_0",
|
||||
"rgb": [219, 83, 83],
|
||||
"speed_cm_yr": 3,
|
||||
"heading_deg": 341,
|
||||
"spin_deg_myr": -22.91
|
||||
},
|
||||
{
|
||||
"name": "plate_1",
|
||||
"rgb": [83, 123, 219],
|
||||
"speed_cm_yr": 2.1,
|
||||
"heading_deg": 207,
|
||||
"spin_deg_myr": 15.62
|
||||
},
|
||||
{
|
||||
"name": "plate_2",
|
||||
"rgb": [163, 219, 83],
|
||||
"speed_cm_yr": 5.8,
|
||||
"heading_deg": 192,
|
||||
"spin_deg_myr": 44.51
|
||||
},
|
||||
{
|
||||
"name": "plate_3",
|
||||
"rgb": [219, 83, 202],
|
||||
"speed_cm_yr": 5.9,
|
||||
"heading_deg": 212,
|
||||
"spin_deg_myr": -44.77
|
||||
},
|
||||
{
|
||||
"name": "plate_4",
|
||||
"rgb": [83, 219, 197],
|
||||
"speed_cm_yr": 4.4,
|
||||
"heading_deg": 285,
|
||||
"spin_deg_myr": -33.44
|
||||
},
|
||||
{
|
||||
"name": "plate_5",
|
||||
"rgb": [219, 157, 83],
|
||||
"speed_cm_yr": 3.5,
|
||||
"heading_deg": 321,
|
||||
"spin_deg_myr": -26.46
|
||||
},
|
||||
{
|
||||
"name": "plate_6",
|
||||
"rgb": [117, 83, 219],
|
||||
"speed_cm_yr": 1.6,
|
||||
"heading_deg": 281,
|
||||
"spin_deg_myr": 12.3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36abeaf5151710e163008efacd9d2b17bfcb0371cefba5eb040425290d5428ae
|
||||
size 19083
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"_comment": "What each painted colour in Map3.jpg means. See README.md next to this file. Rates are rock uplift in mm/yr, and the number to think in is not the rate but the hillslope angle it fixes: at an 8 m cell and K 5e-5, tan(angle) = 2.5 x the rate, so 0.08 is 11 degrees on every divide and 0.25 is 32. Do not read internal/stats' \"plain below 0.1 mm/yr\" as terrain - it is a reporting bucket, and 0.1 mm/yr is hill country. `terrain plan` prints the angle and what it reads as for every class here.",
|
||||
"image": "Map3.jpg",
|
||||
"warn_distance": 60,
|
||||
"classes": [
|
||||
{ "name": "ocean", "rgb": [ 91, 175, 185], "sea": true, "depth_m": 512 },
|
||||
{ "name": "deep", "rgb": [ 65, 165, 180], "sea": true, "depth_m": 512 },
|
||||
{ "name": "shelf", "rgb": [153, 204, 221], "sea": true, "depth_m": 120 },
|
||||
{ "name": "surf", "rgb": [221, 238, 238], "sea": true, "depth_m": 20 },
|
||||
|
||||
{ "_comment_ice": "snow is display and material only - it changes no height and no pass reads it, it just stops the polar caps rendering as meadow. What makes the cap read as ice in the terrain is the detail block: an ice sheet has meltwater channels but not a dendritic bedrock gully network, so it gets almost no droplets. Uplift is low and the cap is still solved like any other land; a proper ice dome would be a stamp after the solve, the way the crater is.",
|
||||
"name": "ice", "derived": true, "rgb": [250, 250, 250], "uplift_mm_yr": 0.05, "k_mult": 1.0,
|
||||
"snow": true, "lithology_mix": 0,
|
||||
"detail": { "droplets_per_cell": 0.02, "strata_contrast": 0.15, "amplitude_m": [1, 3] } },
|
||||
|
||||
{ "_comment_plain": "coastal_plain_km puts the range inland. For n=1 the uplift rate alone fixes the hillslope angle, so a uniformly painted island sits at the angle of repose right down to the water; this ramps the rate up from coastal_floor_mm_yr over the first few km so there is a plain in front of the range. It is the opt-in, explicit version of the taper D-52 removed - that one was hidden and went to zero at the waterline.",
|
||||
"_comment_massif": "The massif is what makes this a landscape rather than a landmass. 0.08 mm/yr is an 11 degree hillslope, and before this block it was 11 degrees on every divide from the waterline to the summit, so the whole continent came out as continuous hill country with no flat ground anywhere on it. Now 0.08 is the rate a *hill mass* reaches and 0.012 - 1.7 degrees, a plain - is the ground between them, with a sixth of it standing up. That is the North European Plain with the Harz and the Ardennes in it, which is what a lowland is.",
|
||||
"name": "lowland", "rgb": [153, 204, 102], "uplift_mm_yr": 0.08, "k_mult": 1.0,
|
||||
"massif": { "floor_mm_yr": 0.012, "fraction": 0.16 },
|
||||
"coastal_plain_km": 1.0, "coastal_floor_mm_yr": 0.012 },
|
||||
|
||||
{ "_comment_faults": "A fault is a difference in uplift rate across a line - steep on one side, gentle on the other - which erosion then carves into a scarp. It is what puts an escarpment and a straight fifteen-kilometre valley inside a range, and neither is something the painting can draw. throw_m is the total displacement over the whole run, so it is the height of the scarp the fault would build if nothing eroded it; per_1000km2 is a density over *this class*, which on this template is 549 km2 of highland, so 25 is about fourteen traces before the long ones step into segments. Drop the block to have none, which is what a plain should have.",
|
||||
"name": "highland", "rgb": [ 68, 170, 102], "uplift_mm_yr": 0.25, "k_mult": 1.0,
|
||||
"faults": { "per_1000km2": 25, "throw_m": [120, 400], "length_km": [6, 18] },
|
||||
"_comment_clamped": "0.90 mm/yr is 66 degrees at a divide against a 35 degree angle of repose, so the repose clamp shapes this class rather than erosion does and the ground bakes out as flat polygonal facets. The ceiling is U = tan(talus)*K*cell = 0.280 mm/yr at an 8 m cell; `terrain plan` prints the angle for every class. Dropping to about 0.25 gives erosion-shaped mountains at roughly 350 m on a 20 km island instead of 695 m of talus - relief and steepness are the same knob at a fixed cell, so that is the trade.",
|
||||
"_comment_massif": "A range is a belt, not a dome. Painted solid, this class used to raise every cell of an island to 32 degrees, which is why the first planet had two islands that were nothing but mountain. 0.25 is now the rate the belt reaches and 0.045 - 6.4 degrees, rolling - is its foreland, with three tenths of it standing up. It cuts the same fabric as the lowland beside it, so the hills in the neighbouring lowland are that orogen's outliers rather than an unrelated noise.",
|
||||
"massif": { "floor_mm_yr": 0.045, "fraction": 0.30 },
|
||||
"coastal_plain_km": 4.0, "coastal_floor_mm_yr": 0.03 },
|
||||
|
||||
{ "_comment_desert": "A desert is not a low uplift rate - a wet lowland has one of those too - so at the geology grid it is only k_mult, which being below 1 means less water doing less work and therefore steeper, more angular ground held further from being worn down. What makes it read as desert is the detail block, at two metres: a fifth of the running water, so the dendritic gully network thins out to isolated wadis; twice the strata contrast, so mesas and ledges keep their edges because nothing is rounding them off; and several times the noise amplitude, which on flat ground is dune.",
|
||||
"_comment_massif": "k_mult 0.5 doubles the angle a rate makes - tan(angle) = 2.5 x rate / k_mult - so 0.10 here is 27 degrees, not 14, and painted solid it made a plateau of mountain. 0.10 is the rate a mesa block reaches and 0.015 - 4.3 degrees - is the sand sea between them.",
|
||||
"name": "desert", "rgb": [238, 221, 153], "uplift_mm_yr": 0.10, "k_mult": 0.5,
|
||||
"faults": { "per_1000km2": 10, "throw_m": [60, 220], "length_km": [5, 14] },
|
||||
"massif": { "floor_mm_yr": 0.015, "fraction": 0.14 },
|
||||
"coastal_plain_km": 0, "coastal_floor_mm_yr": 0.1,
|
||||
"detail": { "droplets_per_cell": 0.035, "strata_contrast": 0.92, "amplitude_m": [7, 14] } },
|
||||
|
||||
{ "_comment_crater": "An impact is an event, not a rate, so it is stamped onto the finished terrain after the solve rather than painted as uplift - a closed basin built out of negative uplift would be filled in by the priority-flood within a hundred steps. The shape comes from the painted blob: distance in from its own shoreline, normalised by its widest point, so rim_at and wall_at describe every crater whatever size it was drawn. The uplift rate here is only what the solve does to the flanks before the stamp lands, so it is low.",
|
||||
"name": "crater", "rgb": [124, 117, 111], "uplift_mm_yr": 0.15, "k_mult": 1.5,
|
||||
"lithology_mix": 0,
|
||||
"crater": { "rim_m": 340, "floor_m": 60, "rim_at": 0.30, "wall_at": 0.62 } },
|
||||
|
||||
{ "_comment": "White is painted twice: the polar caps and the outline stroke around every island. The stroke owns the colour, because it is the one that has to be recognised wherever it appears; a white region touching the top or bottom row of the map is a cap and becomes ice instead. Everything else white dissolves into whichever real class is nearest.",
|
||||
"name": "stroke", "rgb": [238, 238, 238], "stroke": true, "edge_class": "ice" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d76d7c0e968f8ec52eff15d07ff5c227368203d7e754f6bef6288333042cb423
|
||||
size 1681659
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"_comment": "What each painted colour in Map5 means. See README.md next to this file. Rates are rock uplift in mm/yr, and the number to think in is not the rate but the hillslope angle it fixes: at an 8 m cell and K 5e-5, tan(angle) = 2.5 x rate / k_mult, so 0.08 is 11 degrees on every divide and 0.25 is 32. Read the *typical* column `terrain plan` prints, not the divide one - a divide is the steepest ground a rate can make and almost none of a map is divide, so the median comes out at about a third of it in tangent. Reading the divide angle as the landscape is how a legend gets set two or three times too hot.",
|
||||
|
||||
"_comment_colours": "Every rgb below is the *measured modal colour* of Map5.jpg, not a guess at it: the painting was sampled, its colours clustered, and the mode of each cluster taken. Map5.png beside it is the same painting with every pixel snapped to exactly these seven colours, which is what the classifier would do anyway and which removes the JPEG halo along every painted edge for good. The manifest points at the PNG; the JPEG is kept as the original.",
|
||||
|
||||
"image": "Map5.png",
|
||||
"warn_distance": 60,
|
||||
|
||||
"classes": [
|
||||
{ "_comment_ocean": "The open sea. depth_m is the painted sea floor, which internal/coast reads per cell as the abyss it builds a shelf and a continental slope down to; the shelf break itself is pipeline.coast.break_m and defaults to 130 m on a planet (D-64), so it is never deeper than the water it is a break in.",
|
||||
"name": "ocean", "rgb": [ 91, 175, 185], "sea": true, "depth_m": 512 },
|
||||
|
||||
{ "_comment_deep": "The second, darker blue the author painted - the big offshore lobe on the right, the band through the north-west islands, and the channels between the island groups. Taken as deeper water rather than as a shallow shelf, because it is darker than the open ocean and not lighter. If a shelf was meant instead, set depth_m to about 120 and rename it; nothing else in the legend has to change. It is 9% of the world, so it will show.",
|
||||
"name": "deep", "rgb": [ 66, 165, 180], "sea": true, "depth_m": 900 },
|
||||
|
||||
{ "_comment_ice": "The polar caps, painted white at the top and bottom of the map and nowhere else - so unlike Map3's white this is a class in its own right rather than an outline stroke rescued at the poles, and it needs no stroke/edge_class pair. snow is display and material only: it changes no height and no pass reads it, it just stops the caps rendering as meadow. What makes a cap read as ice in the terrain is the detail block - an ice sheet has meltwater channels but not a dendritic bedrock gully network, so it gets almost no droplets.",
|
||||
"name": "ice", "rgb": [250, 250, 250], "uplift_mm_yr": 0.05, "k_mult": 1.0,
|
||||
"snow": true, "lithology_mix": 0,
|
||||
"detail": { "droplets_per_cell": 0.02, "strata_contrast": 0.15, "amplitude_m": [1, 3] } },
|
||||
|
||||
{ "_comment_plain": "coastal_plain_km puts the range inland. For n=1 the uplift rate alone fixes the hillslope angle, so a uniformly painted island sits at the same angle right down to the water; this ramps the rate up from coastal_floor_mm_yr over the first kilometre so there is a plain in front of the ground behind it.",
|
||||
"_comment_massif": "The massif block is what makes this a landscape rather than a landmass. 0.08 mm/yr is an 11 degree divide, and without this block it would be 11 degrees from the waterline to the summit, so the whole continent would come out as continuous hill country with no flat ground anywhere on it. 0.08 is the rate a *hill mass* reaches and 0.012 - 1.7 degrees at a divide, a plain - is the ground between them, with a sixth of it standing up. That is the North European Plain with the Harz in it, which is what a lowland is. The threshold is a quantile of the planet, never of this class, so the hills here and the ones in the highland next door are parts of one structure.",
|
||||
"name": "lowland", "rgb": [150, 200, 105], "uplift_mm_yr": 0.08, "k_mult": 1.0,
|
||||
"massif": { "floor_mm_yr": 0.012, "fraction": 0.16 },
|
||||
"coastal_plain_km": 1.0, "coastal_floor_mm_yr": 0.012 },
|
||||
|
||||
{ "_comment_faults": "A fault is a difference in uplift rate across a line - steep one side, gentle the other - which erosion then carves into a scarp. It is what puts an escarpment and a straight fifteen-kilometre valley inside a range, and neither is a shape a brush can draw. throw_m is the *step across* the fault over the run (D-62), not a full throw on each flank. A fault is a range front several kilometres wide rather than a line: the rate crosses over about a kilometre and the upthrown flank reaches six, which is what lets erosion cut valleys into it instead of printing the profile on the surface. Where several overlap - which at that width is most of a faulted class - the total saturates at 1.6x the strongest single fault rather than adding up (D-63), so raising per_1000km2 past the overlap point buys texture and not height. per_1000km2 is a density over *this class*: Map5 has about 554 km2 of highland, so 25 is roughly fourteen traces before the long ones step into segments.",
|
||||
"_comment_massif": "A range is a belt, not a dome. Painted solid, 0.25 would raise every cell of these continents to a 32 degree divide, which is how a planet ends up with islands that are nothing but mountain. 0.25 is the rate the belt reaches and 0.045 - 6.4 degrees, rolling - is its foreland, with three tenths standing up.",
|
||||
"_comment_ceiling": "The repose clamp takes over above U = tan(35 deg) x K x cell = 0.280 mm/yr at an 8 m cell, and past it the ground bakes out as flat polygonal facets rather than as erosion-shaped mountains. 0.25 sits just under it deliberately. Relief and steepness are the same knob at a fixed cell, so going higher buys facets, not mountains.",
|
||||
"name": "highland", "rgb": [ 71, 175, 100], "uplift_mm_yr": 0.25, "k_mult": 1.0,
|
||||
"faults": { "per_1000km2": 25, "throw_m": [120, 400], "length_km": [6, 18] },
|
||||
"massif": { "floor_mm_yr": 0.045, "fraction": 0.30 },
|
||||
"coastal_plain_km": 4.0, "coastal_floor_mm_yr": 0.03 },
|
||||
|
||||
{ "_comment_desert": "A desert is not a low uplift rate - a wet lowland has one of those too - so at the geology grid it is only k_mult, which being below 1 means less water doing less work and therefore steeper, more angular ground held further from being worn down. What makes it read as desert is the detail block at two metres: a fifth of the running water, so the gully network thins to isolated wadis; twice the strata contrast, so mesas and ledges keep their edges; and several times the noise amplitude, which on flat ground is dune.",
|
||||
"_comment_massif": "k_mult 0.5 doubles the angle a rate makes - tan(angle) = 2.5 x rate / k_mult - so 0.10 here is a 27 degree divide, not 14, and painted solid it would be a plateau of mountain. 0.10 is the rate a mesa block reaches and 0.015 - 4.3 degrees - is the sand sea between them.",
|
||||
"name": "desert", "rgb": [226, 215, 145], "uplift_mm_yr": 0.10, "k_mult": 0.5,
|
||||
"faults": { "per_1000km2": 10, "throw_m": [60, 220], "length_km": [5, 14] },
|
||||
"massif": { "floor_mm_yr": 0.015, "fraction": 0.14 },
|
||||
"coastal_plain_km": 1.5, "coastal_floor_mm_yr": 0.015,
|
||||
"detail": { "droplets_per_cell": 0.035, "strata_contrast": 0.92, "amplitude_m": [7, 14] } },
|
||||
|
||||
{ "_comment_crater": "The two grey blobs on the seam, at the far left and far right of the painting - one crater, drawn across the meridian, which the generator treats as a single feature because X wraps. An impact is an event and not a rate, so it is stamped onto the finished terrain after the solve rather than painted as uplift: a closed basin built out of negative uplift would be filled in by the priority-flood within a hundred steps. The shape comes from the painted blob - distance in from its own shoreline, normalised by its widest point - so rim_at and wall_at describe a crater of any size. The uplift rate here is only what the solve does to the flanks before the stamp lands, so it is low.",
|
||||
"name": "crater", "rgb": [124, 116, 111], "uplift_mm_yr": 0.15, "k_mult": 1.5,
|
||||
"lithology_mix": 0,
|
||||
"crater": { "rim_m": 340, "floor_m": 60, "rim_at": 0.30, "wall_at": 0.62 } }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"_comment": "The annotation layer for Map5: a second painting, the same size as the template and registered to it, saying what is *placed on* the finished world rather than what the rock is doing. Paint it in `terrain studio` under the `overlay` tab (or press o), or let `terrain overlay` propose a first draft from a bake. See README.md next to this file, and Docs/Terrain.md.",
|
||||
|
||||
"_comment_blank": "Most of this sheet is nothing, and nothing is alpha rather than a colour: an unpainted pixel is transparent, so no colour has to be spent on the background and an export with a white matte behind it does not turn the world into whatever mark white is nearest. An opaque pixel further than match_distance from every mark below is dropped and counted - `terrain plan` says how many, which is the only way a colour the legend forgot ever shows.",
|
||||
|
||||
"_comment_image": "There is no image yet, and that is the normal way to start: the legend says what the marks mean and the sheet stays empty until the first save from the studio, or until `terrain overlay` writes one. The manifest's planet.overlay is what points at it once it exists.",
|
||||
|
||||
"image": "Map5.overlay.png",
|
||||
"match_distance": 40,
|
||||
"min_area_px": 24,
|
||||
|
||||
"marks": [
|
||||
{ "_comment": "The one thing on this layer the generator reads. The waterline roughening exists because a drawn shore is a smooth curve and a real coast is fractal - which is true of a coast nobody thought about and false of one traced off a map on purpose. Paint over the shore you drew by hand and it stays exactly where you put it while the rest of the world is still roughened. Either side of the waterline is enough; a brush stroke along it covers both.",
|
||||
"name": "drawn_coast", "rgb": [255, 0, 255], "coast_jitter": 0,
|
||||
"note": "Coastline as painted. No jitter." },
|
||||
|
||||
{ "_comment": "The same knob pointed the other way. A fjord or a ria coast wants more than the planet's own amplitude, not less; at 2.5 the bays are two and a half times as deep and just as wide, which is what makes them fjords rather than scallops.",
|
||||
"_comment_generate": "This mark can also be generated - add \"generate\": { \"kind\": \"coast\", \"coast_km\": 2.5 } and `terrain overlay` will band every shore with it. It is left off deliberately: coast_jitter is the one overlay property a pass reads, so generating this mark changes the next bake everywhere, and that is a decision to take on purpose rather than to inherit from a default.",
|
||||
"name": "wild_coast", "rgb": [255, 128, 0], "coast_jitter": 2.5,
|
||||
"note": "Chew this shore harder than the rest of the world." },
|
||||
|
||||
{ "_comment_inert": "Everything below is inert. No pass reads it, no height changes, and two bakes with and without it are the same terrain to the bit. What they do is travel: each one comes out as an index in the per-tile mask beside every heightmap, and as a feature in world metres in overlay.json - a centre, an area, an extent for an area; an ordered polyline for a path. That is what the engine places things from.",
|
||||
|
||||
"_comment_generate": "The generate block lets `terrain overlay` propose this mark from a baked world. not_classes keeps it off ground painted as ice or desert, which matters more than it sounds: height and slope cannot tell an ice cap from a meadow, and without it woodland grows across both poles. The treeline is derived from the land's own heights, because a number in metres means nothing until the world is baked.",
|
||||
"name": "forest", "rgb": [0, 128, 0],
|
||||
"note": "Where trees are scattered. The mask is the volume bound.",
|
||||
"generate": { "kind": "forest", "cover": 0.45, "wavelength_km": 6, "max_slope_deg": 22,
|
||||
"not_classes": ["ice", "desert"] } },
|
||||
|
||||
{ "_comment_settlements": "Three tiers, listed largest first, which is the order the generator places them in. They share one spacing rule - the largest min_spacing_km any of them sets - so a village never lands inside a city. Sites are scored on the three things the terrain actually knows: drainage, flat ground and distance to the sea. Everything else about where a town is belongs to you, which is why these are proposals in a sheet you edit.",
|
||||
"name": "city", "rgb": [220, 30, 30], "min_area_px": 400,
|
||||
"note": "One settlement per painted blob. overlay.json gives its centre and radius.",
|
||||
"generate": { "kind": "settlement", "count": 4, "min_spacing_km": 9, "not_classes": ["ice"] } },
|
||||
|
||||
{ "name": "town", "rgb": [255, 200, 0], "min_area_px": 120,
|
||||
"generate": { "kind": "settlement", "count": 12, "not_classes": ["ice"] } },
|
||||
|
||||
{ "name": "village", "rgb": [150, 90, 200], "min_area_px": 24,
|
||||
"generate": { "kind": "settlement", "count": 30, "not_classes": ["ice"] } },
|
||||
|
||||
{ "_comment_path": "A path is a stroke whose width is not the point: it is thinned to its centreline and comes out as an ordered polyline, because the thing built from it on the other side is a spline. width_m is what the road really is on the ground and is carried rather than used. One stroke is one path - a fork reports its two longest arms as one line and drops the third - so paint each run separately and check the piece count in `terrain plan`.",
|
||||
"_comment_generate": "Generated roads are a minimum spanning tree over the settlements along least-cost paths, not every pair, so there is exactly enough road to reach everywhere. Water is impassable, so each landmass gets its own network and a bridge stays a deliberate act.",
|
||||
"name": "road", "rgb": [90, 60, 30], "kind": "path", "width_m": 8,
|
||||
"generate": { "kind": "road", "max_slope_deg": 20 } },
|
||||
|
||||
{ "name": "track", "rgb": [200, 160, 110], "kind": "path", "width_m": 3 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:92464c0fb278db9b33e5a1ca06f5b41db23826fdb1004572377a72d061da15a9
|
||||
size 205450
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"_comment": "A painted tectonic layer: one colour per plate, and how that plate is moving. heading_deg is a compass bearing over the map - 0 points at the top of the image, 90 to the right, 180 to the bottom. speed_cm_yr is drift; what happens at a margin is the difference between the two plates either side of it, so two plates drifting the same way are a boundary doing nothing. spin_deg_myr turns a plate about its own centre, positive clockwise, and it is worth setting on at least one: without it every margin is the same all the way along, and with it one end collides while the other slides. Paint the plates, not the mountains - where two of these meet, the collision, the belt and its faults are worked out from the motions. Repaint the blobs freely; only the colours have to keep matching this file.",
|
||||
"image": "Map5.plates.png",
|
||||
"warn_distance": 60,
|
||||
"plates": [
|
||||
{
|
||||
"name": "plate_0",
|
||||
"rgb": [219, 83, 83],
|
||||
"speed_cm_yr": 3,
|
||||
"heading_deg": 38,
|
||||
"spin_deg_myr": -22.76
|
||||
},
|
||||
{
|
||||
"name": "plate_1",
|
||||
"rgb": [83, 123, 219],
|
||||
"speed_cm_yr": 5.3,
|
||||
"heading_deg": 108,
|
||||
"spin_deg_myr": -40.21
|
||||
},
|
||||
{
|
||||
"name": "plate_2",
|
||||
"rgb": [163, 219, 83],
|
||||
"speed_cm_yr": 2.8,
|
||||
"heading_deg": 333,
|
||||
"spin_deg_myr": 21.11
|
||||
},
|
||||
{
|
||||
"name": "plate_3",
|
||||
"rgb": [219, 83, 202],
|
||||
"speed_cm_yr": 2.7,
|
||||
"heading_deg": 7,
|
||||
"spin_deg_myr": -20.61
|
||||
},
|
||||
{
|
||||
"name": "plate_4",
|
||||
"rgb": [83, 219, 197],
|
||||
"speed_cm_yr": 5.4,
|
||||
"heading_deg": 142,
|
||||
"spin_deg_myr": -40.89
|
||||
},
|
||||
{
|
||||
"name": "plate_5",
|
||||
"rgb": [219, 157, 83],
|
||||
"speed_cm_yr": 1.5,
|
||||
"heading_deg": 152,
|
||||
"spin_deg_myr": 11.35
|
||||
},
|
||||
{
|
||||
"name": "plate_6",
|
||||
"rgb": [117, 83, 219],
|
||||
"speed_cm_yr": 4.3,
|
||||
"heading_deg": 207,
|
||||
"spin_deg_myr": -32.87
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5ee213080c00e00a7e0c93e757d41e6b9ef559c25c480d028657013e4111f0d1
|
||||
size 14509
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0cdaa91f191731bbdbf19fedcaae57e21e5a2d02e5d44cbbaad07744d0b4abc3
|
||||
size 198624
|
||||
@@ -0,0 +1,937 @@
|
||||
# Painted templates
|
||||
|
||||
A template is a picture of a world and a legend saying what its colours mean. The generator turns the two
|
||||
into terrain: `Tools/Terrain/bin/terrain.exe plan` reads them and says what a bake would involve, and
|
||||
`terrain bake` solves it. The contract between them is [`../Planet.json`](../Planet.json).
|
||||
|
||||
`World.json` next to it is a different world — the square 14.28 km canvas the numpy pipeline and
|
||||
`create_world.py` still build. The two share nothing but their defaults, and nothing here touches that path.
|
||||
|
||||
```bash
|
||||
# a minute: read the painting, cut the planet into regions, solve nothing
|
||||
Tools/Terrain/bin/terrain.exe plan
|
||||
|
||||
# an hour or two: solve every region and composite the world. Run it detached.
|
||||
# The output goes into the next free RawContent/World/Bake_NNN, so a re-bake never
|
||||
# destroys the one before it - the interesting question is almost always "what did
|
||||
# that change", and answering it needs both.
|
||||
Tools/Terrain/bin/terrain.exe bake
|
||||
|
||||
# one landmass, short, for tuning the legend
|
||||
Tools/Terrain/bin/terrain.exe bake --only 11 --steps 200 --out /tmp/try
|
||||
|
||||
# the same painting as a different world: massifs, rock, faults and coastline detail all re-rolled
|
||||
Tools/Terrain/bin/terrain.exe plan --seed 9342
|
||||
|
||||
# the upland fabric at another size, without editing the manifest
|
||||
Tools/Terrain/bin/terrain.exe plan --massif-km 5
|
||||
|
||||
# the painting tool: brushes that carry the legend's numbers, and Plan as a button
|
||||
Tools/Terrain/bin/terrain.exe studio # http://127.0.0.1:8099
|
||||
```
|
||||
|
||||
## The studio
|
||||
|
||||
`terrain studio` is the tool to reach for first. It exists because the two halves of a painted world used to
|
||||
live in different programs: the shapes in an image editor that knows nothing about uplift rates, the meanings
|
||||
in this legend, which cannot show you where they land.
|
||||
|
||||
The brushes **are** the classes. Picking `highland` is picking 0.25 mm/yr, and the panel tells you that is
|
||||
11.7° of ground and reads as hill country while you are painting it — with the 32° divide angle beside it in
|
||||
grey, because that is where the number comes from and what the repose clamp eventually binds against. So the
|
||||
table in the section above stops being something to look up. Change a number and every swatch re-reads
|
||||
instantly; nothing is written until you press a Save button.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| paint | left-drag. `[` and `]` change the brush size |
|
||||
| pan | shift-drag, middle-drag or right-drag |
|
||||
| zoom | wheel |
|
||||
| Plan | runs the plan **against what is on screen**, not what is on disk, and prints the class table, the seam check and the region cuts |
|
||||
| the map buttons | draw `class`, `uplift`, `regions`, `erodibility` or `overlay` over the canvas, each with a key under it. Press again, or Escape, to go back to painting |
|
||||
| layers | the two tabs at the top, or press **o**. `classes` is the geology; `overlay` is the annotation layer, where the brushes are its marks and the last one is an eraser. See **The overlay** |
|
||||
|
||||
**Plan is seven seconds the first time and about a third of a second after that**, as long as all you changed
|
||||
was a number. That is the tuning loop, and it is worth knowing why it works: the classification, the despeckle,
|
||||
the coast mask, the projection and the region cuts depend on the painting and on the class *colours* only — an
|
||||
uplift rate decides how the maps are coloured in and nothing else — so a plan that differs from the last one
|
||||
only in the legend's numbers reuses the whole of it. Touch the painting, or any of the coast or margin
|
||||
settings, and the next plan is a full one again. The report says which you got.
|
||||
|
||||
Rendering the maps smaller does **not** make it quicker, which is worth stating because it is the obvious
|
||||
guess. Measured on the 100 km template, prepare is flat at about 6.5 s from a 400 px map to a 2400 px one, and
|
||||
it breaks down as classify 0.09 s, dissolve strokes 0.93, despeckle 1.52, the coast mask 1.62, project 0.05
|
||||
and `region.Build` **2.68** — and the last one works on the 76-million-cell planet grid, which the size of the
|
||||
painting and the size of the maps both have nothing to do with.
|
||||
|
||||
Three things about it are deliberate:
|
||||
|
||||
- **The brush is hard-edged and writes exact colours.** No antialiasing and no soft edges, because a blended
|
||||
pixel is not a colour between two classes — it is a pixel that classifies as whichever *third* class happens
|
||||
to sit near the midpoint. See the note on the coastline below; that defect is real and this is where not to
|
||||
create it.
|
||||
- **The canvas wraps.** Paint off the left edge and it arrives at the right, because the map is a cylinder.
|
||||
The first template disagrees with itself on 9.4 % of its rows and this is how that gets fixed.
|
||||
- **There are two sheets, and only one of them is geology.** The class painting is what the solve reads; the
|
||||
overlay rides on top of it and, `coast_jitter` aside, changes no height anywhere. It is dimmed while the
|
||||
brush is on the classes so that a road can never be mistaken for something the solve will act on.
|
||||
- **It never overwrites anything, and the base map least of all.** Saves go to the next free `Map3_001.png`,
|
||||
`Map3_002.png`, ... beside the template, and the manifest is repointed at the new one — the same convention
|
||||
as `Bake_001`, for the same reason: the interesting question is almost always "what did that change", and
|
||||
answering it needs both. Rolling back is pointing `planet.template` at an earlier one. A painting is
|
||||
something you made by hand and there is no undo for it outside the studio, and writing back over a *JPEG*
|
||||
would additionally re-create the blended boundary pixels the despeckle pass exists to remove, compounding
|
||||
them on every save.
|
||||
|
||||
Legend and manifest saves **patch the text** rather than rewriting the file, so the commentary in both
|
||||
survives and a change still shows up as a small diff.
|
||||
|
||||
### Baking from the studio
|
||||
|
||||
The **Bake** panel runs the real solve on the painting as it is on screen, and you can watch it. Because the
|
||||
geology is decomposed per landmass, each one comes out whole — so the preview redraws every time a region
|
||||
lands and the world fills in a continent at a time. The first one out tells you whether the numbers are right;
|
||||
the other seventeen do not have to finish for you to know.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| regions | blank bakes everything (~2 h on the 100 km template). A few ids — `11,13` — is the quick version, and the ids are the ones in the regions key |
|
||||
| steps | 0 is the manifest's full count. A few hundred is under-eroded but enough to see the shape |
|
||||
| jobs | how many landmasses solve at once. The wall time is set by the largest single region, which runs at about one core, so this helps least on exactly the template that needs it most |
|
||||
| Cancel | stops it. Regions in flight stop at the end of their current step, so the wait is one step of the biggest one |
|
||||
|
||||
A finished bake goes to the next `Bake_NNN`. A **cancelled** one goes to `Partial_NNN` instead — the regions
|
||||
that finished are complete and worth keeping, everything else in it is still at sea level, and the separate
|
||||
name is so that nothing downstream (`tiles --bake` takes the newest `Bake_NNN`) can mistake one for the other.
|
||||
|
||||
## The picture
|
||||
|
||||
Flat cylindrical: **X wraps** — the left and right edges are the same meridian, so a landmass may straddle
|
||||
them and comes out whole — and **Y does not**: the top and bottom rows are the poles. Any aspect works; the
|
||||
paint is never stretched to 2:1.
|
||||
|
||||
**The painting has to wrap too, and this is the thing to check first.** The left and right edges are the same
|
||||
meridian, so anything that does not continue from one to the other is a real discontinuity down one line of
|
||||
the world — and it is the one defect you cannot see by looking at the picture, because the two edges are as
|
||||
far apart on screen as they can be. `terrain plan` measures it:
|
||||
|
||||
```
|
||||
wrap the left and right edges are the same meridian: they disagree on 355 of 3761 rows (9.4%),
|
||||
261 of those land against water, and 48 px in the outermost columns match no class.
|
||||
THAT IS A VISIBLE SEAM. The generator wraps; the painting has to as well.
|
||||
```
|
||||
|
||||
That is `Map3.jpg` as it stands. The crater island crosses the seam perfectly — sampled at the geology grid,
|
||||
the heights run continuously from the last column into the first — but the islets near the left edge were
|
||||
drawn touching `x = 0` with nothing to meet them at `x = W-1`, so the world has a 400 m cliff down the seam
|
||||
wherever that happens. Painting round the edge, or nudging those islets inwards, fixes it.
|
||||
|
||||
PNG is better than JPEG, and the seam is where it shows. JPEG bleeds colour across every class boundary, and
|
||||
two legend colours closer together than the bleed will swap pixels where they meet — survivable in the middle
|
||||
of the map, but on the outermost columns a lossy encoder leaves a halo, and a halo on the seam is a stripe of
|
||||
the wrong class down the one line of the world that cannot hide it. `Map3.jpg`'s first two columns are
|
||||
lighter than the ocean behind them, which the classifier reads as shelf: a 400 m ledge two pixels wide,
|
||||
running the height of the map.
|
||||
|
||||
The paint does not have to match the grid. `Map3.jpg` is 7738 px across a 100 km world, so a pixel is 12.9 m
|
||||
against an 8 m geology cell: the painting is slightly *coarser* than the simulation, which is the right way
|
||||
round. The rule from `Docs/Terrain-Next.md` §3.2 is that the painted map owns the wavelengths above its pixel
|
||||
size and noise owns those below.
|
||||
|
||||
## Paint the uplift, never the height
|
||||
|
||||
This is the one thing to understand before painting anything. A class does not say how high the ground is; it
|
||||
says **how fast the rock is rising** and **how easily it erodes**. The simulation then produces the terrain.
|
||||
|
||||
Painting a heightmap instead does not work and the reason is worth knowing: a stream-power solve handed a
|
||||
painted surface erodes it into something else within a few hundred steps, and what comes out has no
|
||||
relationship to what was drawn — while the drainage network, which is the entire reason this generator exists,
|
||||
is thrown away. Paint intent; get rivers.
|
||||
|
||||
Rivers cannot be painted either. A river is an *output* of the drainage solve. What does work is biasing:
|
||||
raise `k_mult` along a line so the water finds the soft rock, and the solve chooses to put a river there for
|
||||
its own reasons — and the result is still a coherent network.
|
||||
|
||||
## The legend
|
||||
|
||||
One JSON file beside the image. Keys beginning with an underscore are comments; any other unknown key is an
|
||||
error, because a misspelt key silently ignored is a class quietly running on the default.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"image": "Map3.jpg",
|
||||
"warn_distance": 60, // how far in RGB a pixel may sit from every class before the run says so
|
||||
"classes": [
|
||||
{ "name": "ocean", "rgb": [91, 175, 185], "sea": true, "depth_m": 512 },
|
||||
{ "name": "lowland", "rgb": [153, 204, 102], "uplift_mm_yr": 0.08, "k_mult": 1.0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| key | meaning |
|
||||
| --- | --- |
|
||||
| `name` | what it is called in the reports and the maps |
|
||||
| `rgb` | the painted colour. Every pixel takes the nearest one, so there is no unclassified pixel |
|
||||
| `sea` | water. The solve holds every sea cell at base level for its whole run |
|
||||
| `depth_m` | sea only: how deep the open water is, in metres below sea level, positive |
|
||||
| `uplift_mm_yr` | land only: rock uplift, and **the hillslope angle**. See below: `tan(divide) = 2.5 × rate ÷ k_mult` at an 8 m cell, and the *ground* is a third of that in tangent — so 0.08 is 3.8° of median hillslope with 11.3° at its divides, and 0.25 is 11.7° with 32° |
|
||||
| `k_mult` | land only: the multiplier on stream-power erodibility. Soft rock above 1, hard below |
|
||||
| `stroke` | decoration rather than data — dissolved into whichever real class is nearest |
|
||||
| `edge_class` | a stroke touching the top or bottom row of the map is not a stroke; it becomes this class |
|
||||
| `derived` | never painted: no colour matching, exists only as something else's `edge_class`. Its `rgb`, if any, is for the diagnostic maps |
|
||||
| `coastal_plain_km` | land only: how far inland the rate ramps up to its full value, so the range sits behind a plain |
|
||||
| `coastal_floor_mm_yr` | land only: the rate at the waterline. Defaults to 0.02, never raised above the class rate |
|
||||
| `massif` | land only: `{floor_mm_yr, fraction}` — the class breaks into plain and upland instead of holding one rate everywhere. **Read the next section before writing a legend without one** |
|
||||
| `faults` | land only: `{per_1000km2, throw_m, length_km}` — traces placed in this class's ground. Absent means none, which is right for a plain. See **One painting, many worlds** |
|
||||
| `lithology_mix` | land only: 0 to 1, how much of the planet's rock field shows through here. Default 1; 0 is uniform rock, which is what an ice cap or a crater floor should be |
|
||||
| `crater` | land only: `{rim_m, floor_m, rim_at, wall_at}`, stamped on the finished terrain |
|
||||
| `snow` | land only: permanently under ice. **Display and material only** — no height moves, no pass reads it, and it is drawn with the palette's `ice` colour. What makes a polar cap *terrain* is its uplift rate and its `detail` block like any other class |
|
||||
| `detail` | land only: `{droplets_per_cell, strata_contrast, amplitude_m}`, what the detail passes do differently on this ground |
|
||||
|
||||
### A rate is an angle, and one rate is one landscape
|
||||
|
||||
This is the second thing to understand, and it is the one that caught the first painted planet out.
|
||||
|
||||
For `n = 1` the steady-state slope is `S = U / (K·A^m)`, and with `critical_area_m2` at 0 that law reaches down
|
||||
to a single cell — so at a drainage divide `A` is one cell and the uplift rate, on its own, fixes the hillslope
|
||||
angle. At the 8 m geology cell and `K` 5e-5 it works out at:
|
||||
|
||||
```
|
||||
tan(angle) = 2.5 × uplift_mm_yr ÷ k_mult
|
||||
```
|
||||
|
||||
**But a divide is the steepest place in a catchment, and almost none of a map is divide.** `A` is smallest at
|
||||
the top of a hillslope and grows all the way down, so the divide angle is the *ceiling* on a class and not its
|
||||
landscape. Measured on a 600² grid of 8 m cells at 1000 steps with the manifest's own constants, the median
|
||||
slope comes out at a third of it in tangent, and the ratio barely moves over a factor of twenty in rate
|
||||
(0.34, 0.33, 0.33, 0.32). So there are two columns, and **the one to steer by is the second**:
|
||||
|
||||
| `uplift_mm_yr` | divide | typical ground | reads as |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.012 | 1.7° | 0.6° | plain |
|
||||
| 0.02 | 2.9° | 0.9° | plain |
|
||||
| 0.04 | 5.7° | 1.9° | plain |
|
||||
| 0.08 | 11.3° | 3.8° | rolling |
|
||||
| 0.10 | 14.0° | 4.7° | rolling |
|
||||
| 0.25 | 32.0° | 11.7° | hill country |
|
||||
| 0.28 | 35° | 12.9° | the angle of repose at a divide — above this the clamp shapes the ground, not erosion |
|
||||
|
||||
`terrain plan` prints both for every class in the legend, along with the word in the right-hand column, and so
|
||||
does the studio panel as you type. Read it before starting a bake; it is four seconds against two hours.
|
||||
|
||||
**Reading the divide column as the landscape is how a legend ends up two or three times too hot everywhere**,
|
||||
and it is the same mistake as the one in the paragraph below, one level up.
|
||||
|
||||
**Do not read `internal/stats`' "plain below 0.1 mm/yr" as a description of terrain.** It is a reporting bucket
|
||||
calibrated for the old procedural path, and 0.1 mm/yr is a fourteen-degree hillslope: hill country wherever it
|
||||
is painted. That sentence is how the first planet's `lowland` ended up ten times too hot.
|
||||
|
||||
### Flat ground, which needs two rates and not one
|
||||
|
||||
Because a class was one rate over every cell an author painted with it, a landmass painted one colour came out
|
||||
**uniformly** dissected — from the waterline to the summit, at whatever angle the rate named, with no flat
|
||||
ground anywhere on it. That is not what a continent looks like. Europe away from the Alps is a plain at a
|
||||
fraction of a degree with isolated massifs standing out of it, and what separates the two is not the rate, it
|
||||
is that the rate is not the same everywhere.
|
||||
|
||||
So a class carries a `massif` block: the class rate is re-read as the rate a *massif* reaches, `floor_mm_yr` is
|
||||
the plain between them, and `fraction` is how much of the class stands above the midpoint of the two.
|
||||
|
||||
```jsonc
|
||||
{ "name": "lowland", "rgb": [153,204,102], "uplift_mm_yr": 0.08,
|
||||
"massif": { "floor_mm_yr": 0.012, "fraction": 0.16 } }
|
||||
```
|
||||
|
||||
A sixth of it is rolling ground at 3.8° (11.3° at its divides); the rest is a 0.6° plain that tops out about
|
||||
30 m above the sea over a whole continent. Half the fraction again — a twelfth — reaches 0.08 outright, and
|
||||
half again above that is off the plain at all.
|
||||
|
||||
**So "the lowlands come out too hilly" is almost always one of two numbers, and neither of them is the floor.**
|
||||
It is `fraction`, because the ramp opens at `1 - 1.5 × fraction` in probability, so 0.16 leaves about a
|
||||
*quarter* of the class off the plain rather than a sixth; or it is `uplift_mm_yr`, which is what those raised
|
||||
parts climb to. The floor itself has been measured baking out at 0.58° median with 4 % of it over three
|
||||
degrees, which is a plain by any reading.
|
||||
|
||||
Three things worth knowing about it:
|
||||
|
||||
- **The floor wants to be about a tenth of the rate.** It is the number that decides whether the class has flat
|
||||
ground at all. If a bake comes out too busy, though, drop `fraction` first: the floor is already a plain and
|
||||
lowering it further only flattens ground that was flat.
|
||||
- **`fraction` is a share of the planet's surface, so it is only the *expected* share of any one island.** A
|
||||
small island may get all of a massif or none of it, exactly as it would if it were a real island that
|
||||
happened to sit on or off an orogen. That variance is the point; normalising it per landmass would hand
|
||||
every island its quota of hills, which is the thing this exists to stop.
|
||||
- **There is one fabric for the whole planet**, `planet.massif_wavelength_km` in `Planet.json`, and every class
|
||||
cuts the same one at a different level. So a highland belt and the hills in the lowland beside it come out as
|
||||
the high and low parts of one structure — a foreland and its outliers — rather than as two unrelated noises
|
||||
meeting at a painted edge. Set the wavelength well below the size of a landmass: at 12.5 km against islands
|
||||
of 20–45 km one island came out entirely above the cut, which is the original defect over again, only
|
||||
smaller. 7 km puts several blocks across every continent.
|
||||
|
||||
A class with no `massif` block is one rate all over, which is what every class was before this existed. That is
|
||||
still the right answer for a small class, for a crater's flanks, and for anything an author would rather paint
|
||||
by hand.
|
||||
|
||||
### Coastlines, which are not drawn lines
|
||||
|
||||
A shore drawn with a bezier tool is smooth, and a real coast is not: it has bays inside bays inside bays, and
|
||||
how long it is depends on the ruler you measure it with. Projected straight, the painting's own smoothness
|
||||
survives all the way to the heightmap and the result reads as what it is — a shape somebody drew.
|
||||
|
||||
So the waterline is roughened before the painting is projected. Fractal noise is added to the signed distance
|
||||
from the shore and the sign is read again, so land juts out where the noise is positive and the sea reaches in
|
||||
where it is negative. Nothing away from the shore moves.
|
||||
|
||||
```jsonc
|
||||
// in Planet.json, not the legend: it is about the world, not about one colour
|
||||
"coast_jitter_px": 48, // how far the shore may move, template px
|
||||
"coast_jitter_wavelength_px": 384, // how wide the biggest bay is
|
||||
"coast_jitter_octaves": 5,
|
||||
"coast_jitter_gain": 0.55 // near 0.5: each scale as prominent as the last
|
||||
```
|
||||
|
||||
At 12.92 m a pixel that is bays about 5 km wide and up to 620 m deep. **The ratio of the two is the knob that
|
||||
matters** — amplitude far below wavelength gives a rough line, amplitude approaching half the wavelength gives
|
||||
a fjord coast. Tune it in the studio, which prints both in kilometres as you type. `0` projects the painting
|
||||
exactly as drawn.
|
||||
|
||||
Small islands are safe: the amplitude is capped per cell at two thirds of the widest land within reach, so an
|
||||
islet gets a ragged edge instead of being swallowed by a wavelength ten times its size.
|
||||
|
||||
**And a coastline you drew on purpose is exempt.** The argument above is true of a shore nobody thought about
|
||||
and false of one traced off a real map, so the amplitude is a *field* rather than a number: paint a
|
||||
`coast_jitter` mark from the overlay over the stretch you want kept and it stays exactly where you put it
|
||||
while the rest of the world is still roughened. Zero pins it; above one chews it harder, which is how a fjord
|
||||
coast is made without turning the whole planet into one. See **The overlay** below.
|
||||
|
||||
### A note on JPEG, which is not a style preference
|
||||
|
||||
The README says export PNG. Here is what it costs not to. Classification gives every pixel its nearest class
|
||||
in RGB, and a lossy codec blends across every boundary. On this template the blend of `surf` (221,238,238) and
|
||||
`lowland` (153,204,102) comes out at about (186,219,174) — whose distance to `desert` (238,221,153) is **53.8**
|
||||
against **77.9** to either of the colours it was actually mixed from. So the antialiased edge between the sea
|
||||
and the shore classifies as *desert*, and the map gains a one-pixel ribbon of it along every temperate coast:
|
||||
1607 pixels on this one.
|
||||
|
||||
One pixel wide that is invisible. It stopped being invisible the moment the coast mask arrived, because a
|
||||
stray pixel is still **land** — so it became the nearest land to a stretch of open water, and every cell the
|
||||
mask turned into shore inherited its class. An eleven-pixel band of desert appeared along a green continent.
|
||||
|
||||
A despeckle pass now removes any class that holds a one-pixel line with two others either side of it, which
|
||||
costs 0.068 % of the map and takes that count to zero. It is a safety net and not a licence: paint in the
|
||||
studio or export PNG, and none of this happens.
|
||||
|
||||
## The overlay
|
||||
|
||||
A second painting, the same size as the template and registered to it, for everything that is *placed on* the
|
||||
finished world rather than solved: where the forest is, where the village is, what a road follows, and which
|
||||
coastlines to leave alone.
|
||||
|
||||
It is a separate image rather than more colours on the template because the two answer different questions.
|
||||
Every colour on the template is geology — an uplift rate the solve answers for — and there is no uplift rate
|
||||
for a town. A mark has to be able to sit on top of any class without changing it.
|
||||
|
||||
```jsonc
|
||||
// Planet.json
|
||||
"overlay_legend": "Templates/Map3.overlay.json",
|
||||
"overlay": "Templates/Map3.overlay_001.png", // written by the studio; leave it out to begin
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// Map3.overlay.json
|
||||
{
|
||||
"image": "Map3.overlay.png",
|
||||
"match_distance": 40, // an opaque pixel further than this from every mark is dropped
|
||||
"min_area_px": 24, // below this a blob is a speck and is not reported
|
||||
"marks": [
|
||||
{ "name": "drawn_coast", "rgb": [255, 0, 255], "coast_jitter": 0 },
|
||||
{ "name": "wild_coast", "rgb": [255, 128, 0], "coast_jitter": 2.5 },
|
||||
{ "name": "forest", "rgb": [ 0, 128, 0] },
|
||||
{ "name": "city", "rgb": [220, 30, 30], "min_area_px": 400 },
|
||||
{ "name": "road", "rgb": [ 90, 60, 30], "kind": "path", "width_m": 8 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Nothing is a colour.** An unpainted pixel is transparent, not a background colour, so no colour has to be
|
||||
spent on emptiness and an export with a white matte behind it does not turn the world into whatever mark white
|
||||
is nearest. An opaque pixel that matches no mark inside `match_distance` is dropped and counted — `terrain
|
||||
plan` says how many, which is the only way a colour the legend forgot ever shows.
|
||||
|
||||
### What the generator reads, which is one thing
|
||||
|
||||
`coast_jitter` and nothing else. It scales how far the waterline roughening may move the shore inside the
|
||||
mark: `0` pins it exactly as painted, `2.5` chews it two and a half times as hard as the rest of the world.
|
||||
Painting either side of the waterline is enough — the roughening already knows, for every cell it might move,
|
||||
which cell on the far side it would take its class from — so a brush stroke drawn along the shore covers both
|
||||
and nothing has to be traced precisely.
|
||||
|
||||
The check is exact: an overlay painted `coast_jitter: 0` over every pixel produces a class raster byte for
|
||||
byte identical to a run with `coast_jitter_px` at zero.
|
||||
|
||||
### What travels, which is everything else
|
||||
|
||||
No pass reads a `forest` or a `city` or a `road`. Two bakes with and without them are the same terrain to the
|
||||
bit. What those marks do is come out the other end in two shapes:
|
||||
|
||||
- **A mask per tile.** `Planet_x11_y07_overlay.png` beside every heightmap: 8-bit, one mark index a detail
|
||||
cell, `0` for nothing. Marks cannot overlap — the overlay is one painting and a pixel is one colour — so
|
||||
254 of them fit in the file one would have taken.
|
||||
- **Features in world metres.** `overlay.json` beside `tiles.json`, and beside the maps of every plan and
|
||||
bake. An **area** mark gives a centre, an area, a radius and an extent per connected blob; a **path** mark
|
||||
is thinned to its centreline and gives an ordered polyline and a length, because the thing built from it on
|
||||
the other side is a spline. Both are in metres east of the seam and metres south of the top painted row.
|
||||
|
||||
A blob that crosses the seam is one feature, not two, and its centre lands on the blob rather than on the far
|
||||
side of the world. A road across the seam comes out as one polyline whose X runs past the circumference,
|
||||
which a consumer can wrap back knowing how wide the world is.
|
||||
|
||||
One stroke is one path. A fork reports its two longest arms as a single line and drops the third, so paint
|
||||
each run separately — `terrain plan` prints how many pieces each mark has, which is where a fork shows.
|
||||
|
||||
### Generating a first draft
|
||||
|
||||
The overlay starts blank, and the three things most worth putting on it - woodland, settlements and the roads
|
||||
between them - all depend on ground you cannot see while painting: the classes are painted before the solve
|
||||
exists, and once it does exist it is a seventy-six-million-pixel heightmap. So there is a command that reads a
|
||||
finished bake and proposes them.
|
||||
|
||||
```bash
|
||||
terrain overlay # the newest Bake_NNN, writes the next Map3_NNN.overlay.png
|
||||
terrain overlay --no-save # say what it would place and write nothing
|
||||
terrain overlay --bake RawContent/World/Bake_022
|
||||
terrain overlay --replace # start from a blank sheet instead of filling in around what is painted
|
||||
```
|
||||
|
||||
**In the studio it is a button.** Switch to the `overlay` tab and press **Generate marks**. That is the loop
|
||||
this is really for: press it, look at where the towns landed, press it again, keep the third one. Two things
|
||||
differ from the command, and both follow from a button being pressed repeatedly rather than once.
|
||||
|
||||
- **Every press is a new seed**, so it is a re-roll by construction. The painting fixes where the land is;
|
||||
the seed decides everything it does not - which patch of eligible ground becomes woodland, and which of
|
||||
several equally good sites gets the town.
|
||||
- **Each press replaces the last draft** instead of piling on top of it. The studio remembers exactly which
|
||||
pixels the previous generation put down and clears those, and only those, first. Anything you painted is
|
||||
never in that set.
|
||||
|
||||
The sheet is replaced wholesale rather than stroked, so **ctrl+z will not walk back over a generation** and
|
||||
the undo history is dropped when you press it. The status line says so. Nothing you painted is lost either
|
||||
way, because generation only fills blanks.
|
||||
|
||||
**It works before the first bake**, on the painting alone, and says which of the two ran. That matters: with
|
||||
no solve there are no rivers to sit on and no slope for a road to bend around, so a draft made that way is a
|
||||
sketch that respects land, sea and classes and nothing else. Bake, then press again for one that reads the
|
||||
terrain. A bake made from a *different painting* is ignored rather than used - two paintings of the same
|
||||
planet encode their heightmaps identically, so nothing else would catch it and the marks would be placed
|
||||
against terrain from another world.
|
||||
|
||||
**It never touches a pixel you painted.** The sheet on disk is loaded first and generation fills around it, so
|
||||
running this against a half-painted overlay adds to it, and running it twice is safe. That is what makes the
|
||||
round trip work in both directions: generate a draft, move the towns where you want them in the studio,
|
||||
regenerate the roads around your edits. `--replace` is the explicit way to throw a generation away.
|
||||
|
||||
**It is opt-in per mark.** A mark generates only if it carries a `generate` block, so a legend written before
|
||||
this existed produces the blank sheet it always did, and a mark you want to own completely simply says
|
||||
nothing.
|
||||
|
||||
```jsonc
|
||||
{ "name": "forest", "rgb": [0,128,0],
|
||||
"generate": { "kind": "forest", "cover": 0.45, "wavelength_km": 6,
|
||||
"max_slope_deg": 22, "not_classes": ["ice", "desert"] } },
|
||||
|
||||
{ "name": "city", "rgb": [220,30,30], "min_area_px": 400,
|
||||
"generate": { "kind": "settlement", "count": 3, "min_spacing_km": 9, "not_classes": ["ice"] } },
|
||||
|
||||
{ "name": "road", "rgb": [90,60,30], "kind": "path", "width_m": 8,
|
||||
"generate": { "kind": "road", "max_slope_deg": 20 } }
|
||||
```
|
||||
|
||||
Four kinds:
|
||||
|
||||
- **`forest`** fills ground that is shallow enough and below the treeline, broken up by a noise field so it
|
||||
reads as woodland rather than as a contour band. `cover` is the share of eligible ground it takes and
|
||||
`wavelength_km` how big the patches are. The treeline defaults to a quantile of the land's own heights,
|
||||
because a number in metres means nothing until the world is baked - the same legend has to put trees on a
|
||||
47 m plain and on a 2800 m range.
|
||||
- **`settlement`** scores the land on three things the terrain actually knows - drainage, flat ground and
|
||||
distance to the sea - and takes the best sites with a minimum spacing. Several marks may use it and they
|
||||
share one spacing rule, largest first, so a village never lands inside a city. Everything else about where
|
||||
a town is - trade, history, who won a war - is yours, which is why these are proposals in a sheet you edit.
|
||||
- **`road`** joins the settlements along least-cost paths, as a minimum spanning tree rather than every pair,
|
||||
so there is exactly enough road to reach everywhere. **Water is impassable**, so each landmass gets its own
|
||||
network and a bridge or a ferry stays a deliberate act.
|
||||
- **`coast`** bands the waterline. **It is not enabled in the shipped legend on purpose**: `coast_jitter` is
|
||||
the one overlay property a pass reads, so generating that mark changes the next bake's coastline
|
||||
everywhere, and that is a decision to take deliberately rather than to inherit from a default.
|
||||
|
||||
**`only_classes` and `not_classes`** keep a mark off ground you painted a particular way. They matter more
|
||||
than they sound: height and slope cannot tell an ice cap from a meadow, and the first run of this generator
|
||||
grew woodland across both polar caps, which are flat, below the treeline, and no business of anybody's. A
|
||||
class name that is not in the class legend is an error rather than an empty filter.
|
||||
|
||||
**Read the run's report.** It says what it kept, what it painted, and - the useful one - when it placed fewer
|
||||
settlements than you asked for, which means the spacing or the flat ground ran out. On this template asking
|
||||
for 26 villages at a 9 km spacing gets 17.
|
||||
|
||||
### Painting it
|
||||
|
||||
Press **o** in the studio, or use the `overlay` tab. The brushes become the legend's marks, the last one is an
|
||||
eraser, and the sheet rides over the class painting — full strength while you are on it, dimmed while you are
|
||||
painting classes, because a mark means nothing except against the coastline it was drawn along. **Save
|
||||
overlay** writes the next numbered PNG and points the manifest at it, exactly as the class painting does.
|
||||
|
||||
A planet with `overlay_legend` set and no image yet is the normal way to start: the legend says what the marks
|
||||
mean and the sheet is empty until the first save.
|
||||
|
||||
## One painting, many worlds
|
||||
|
||||
A painting is a **composition**: where the continents are, where the ranges run, which coast you drew on
|
||||
purpose. It is not the whole world. Everything inside it that nobody drew comes from the seed, and changing
|
||||
the seed re-rolls all of it while the painting stays exactly as painted.
|
||||
|
||||
```bash
|
||||
Tools/Terrain/bin/terrain.exe plan --seed 9342 # four seconds; the maps say what moved
|
||||
Tools/Terrain/bin/terrain.exe bake --seed 9342 --out RawContent/World/Bake
|
||||
Tools/Terrain/bin/terrain.exe tiles --seed 9342 --bake RawContent/World/Bake
|
||||
```
|
||||
|
||||
Or in the studio: the **seed** box and the **Re-roll** button, then Save planet and Plan.
|
||||
|
||||
`--seed` is on `tiles` as well, and that is not a convenience: the detail passes hash the seed into every
|
||||
droplet, so a tile run has to be told the seed its heightmap was baked under. `CheckBake` refuses a mismatch
|
||||
rather than giving you gullies from a different world.
|
||||
|
||||
### What moves
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| the upland fabric | where the massifs stand inside every class that has one |
|
||||
| the rock | which lithology province is where, and so which flank of a range is gullied and which is benched |
|
||||
| the faults | where every trace is, which way it runs, and how much it throws |
|
||||
| the regional swell | the long-wavelength warp that puts divides on a plain |
|
||||
| the initial relief | the small symmetry-breaking the solve starts from |
|
||||
| the coastline detail | the bays and headlands the waterline roughening cuts, except where the overlay pins it |
|
||||
|
||||
Measured on this template, seed 7 against 9342: **13.8 %** of `map_uplift.png` and **24.9 %** of
|
||||
`map_erodibility.png` change, against 2.1 % of `map_class.png` — and that 2.1 % is only the coastline.
|
||||
|
||||
### Rock
|
||||
|
||||
One low-frequency pattern over the whole planet, cut into `pipeline.lithology`'s types at **quantiles of the
|
||||
planet** so every region agrees, multiplying each class's own `k_mult`:
|
||||
|
||||
```jsonc
|
||||
// Planet.json
|
||||
"lithology_wavelength_km": 9, // how big a province is. 0 switches it off
|
||||
"pipeline": { "lithology": { "types": 3, "k_multipliers": [0.6, 1.0, 1.8] } }
|
||||
```
|
||||
|
||||
Set the wavelength well *below* a range or a range is one rock and you have gained nothing: 9 km against
|
||||
belts of 15–25 km puts two or three provinces across each of them. The bands are equal-area, so every type
|
||||
appears whatever the seed did — a seed that produced no hard rock anywhere would be a seed that quietly
|
||||
removed a process.
|
||||
|
||||
Boundaries are graded rather than blurred, and pointwise: the width of a transition on the ground follows the
|
||||
field's own gradient, sharp where the rock changes fast. That is not a style choice. A blur is a neighbourhood
|
||||
operation, and one near a region's edge would read cells a different decomposition of the planet would not
|
||||
have given it — the same rule that makes the cut a planet quantile rather than a regional percentile.
|
||||
|
||||
### Faults
|
||||
|
||||
A fault is **a difference in uplift rate across a line**, steep on one side and gentle on the other, which
|
||||
erosion carves into a scarp. That is why it is not something to paint: painted as terrain the solve erodes it
|
||||
away, applied as a rate it maintains itself. It is what puts an escarpment and a straight fifteen-kilometre
|
||||
valley inside a range, and neither is a shape a brush can draw.
|
||||
|
||||
```jsonc
|
||||
{ "name": "highland", "uplift_mm_yr": 0.25,
|
||||
"faults": { "per_1000km2": 25, "throw_m": [120, 400], "length_km": [6, 18] } }
|
||||
```
|
||||
|
||||
- **`per_1000km2` is a density over this class**, not a count, because a class covers whatever you painted
|
||||
with it. This template has 549 km² of highland, so 25 is about fourteen traces before the long ones step.
|
||||
- **`throw_m` is the whole step across the fault over the run**, not a rate — how much higher the upthrown
|
||||
side would stand than the downthrown one if nothing eroded either, which is a number you can picture. It
|
||||
is the *step*, not the height of each side: before D-62 the profile put a full throw on each flank and so
|
||||
built two, and a legend written against that number is now asking for half what it used to get.
|
||||
- **Put the block on the classes that are orogens.** A plain should have none. The *influence* is not
|
||||
restricted to the class, because a range-front fault runs along the edge of a range by definition and its
|
||||
scarp faces the lowland.
|
||||
|
||||
`planet.fault_grain_km` is only how they are **aimed**: a grain field, so traces near each other come out
|
||||
sub-parallel and the set swings gradually across the world rather than running as corduroy.
|
||||
|
||||
**A fault is kilometres wide, not a line.** The rate crosses from the downthrown side to the upthrown one
|
||||
over about a kilometre and the upthrown flank reaches six, so what a fault makes is a *range front* — a belt
|
||||
of ground with its own valleys cut into it, its crest a kilometre or so back from the trace. It used to be a
|
||||
step across one cell inside a 600 m welt, which erosion could not touch, so it came out as a smooth ruled
|
||||
ridge; that is D-62. Two consequences for you: a fault influences ground several kilometres from where you
|
||||
would draw the line, and it needs that much room, so faulting a class painted in strips two kilometres wide
|
||||
will not give you what the picture in your head has.
|
||||
|
||||
**And a set of them saturates rather than stacking** (D-63). Faults near each other are sub-parallel on
|
||||
purpose — that is what `fault_grain_km` is for — so once each one is kilometres wide they overlap, and
|
||||
overlapping faults that all push the same way used to add. Where several reach the same ground the total is
|
||||
now bent onto at most 1.6 times whatever the strongest single fault there asked for. One fault is untouched
|
||||
by that bound, so `throw_m` still means exactly what it says; what it stops is five parallel faults
|
||||
delivering five throws. The practical consequence for you: **raising `per_1000km2` past the point where
|
||||
faults overlap buys texture, not height.** If you want a belt to stand higher, raise `throw_m`.
|
||||
|
||||
Watch `fault_grain_km` against the size of your landmasses. It is the wavelength of the field the strikes
|
||||
are drawn from, so a landmass much smaller than it sits inside one grain cell and *every* fault on it comes
|
||||
out near-parallel. The shipped 45 km against a 22 km landmass is exactly that case.
|
||||
|
||||
Traces over twelve kilometres are drawn as two or three overlapping en-echelon segments, the throw rises and
|
||||
falls along strike as a bell rather than holding flat through the middle, and the only ceiling is the repose
|
||||
angle — or whatever your own numbers asked for, whichever is higher. A bake's per-region line says what share of the land that bound
|
||||
touched, and says nothing when it touched none; a large share means the throws are big for the class they are
|
||||
sitting in.
|
||||
|
||||
**Expect erosion to take most of the throw.** Measured on this template, region 15 baked whole: five traces
|
||||
with throws of 139 to 399 m left scarps of 2.7 to 50 m, all five facing the side the fault raises. That is
|
||||
the point of applying a fault as a *rate* — a painted 400 m step would have been eroded to nothing in a
|
||||
thousand steps, while a rate difference keeps rebuilding the scarp as fast as the rivers cut it down. (Those
|
||||
five were measured 600 m either side of the trace and under the pre-D-62 profile, so they straddle the old
|
||||
welt rather than spanning a range front; treat them as the right order of magnitude and not as a table to
|
||||
tune against.)
|
||||
|
||||
They are drawn on `map_uplift.png` in cyan — the line rather than the rate it contributes, because a scarp is
|
||||
a couple of hundred metres wide and that map is a hundred kilometres across. The geometry is in the bake's
|
||||
`meta.json`, so "which fault made that valley" is answerable afterwards.
|
||||
|
||||
### Making a desert
|
||||
|
||||
A desert is not a low uplift rate — a wet lowland has one of those too. At the geology grid the only lever is
|
||||
`k_mult`, and below 1 it means less water doing less work, so the ground comes out steeper and more angular
|
||||
and further from being worn down. That is right, and it is nowhere near enough: at eight metres a desert and
|
||||
a meadow are the same picture.
|
||||
|
||||
The difference is at two metres, in the `detail` block:
|
||||
|
||||
```jsonc
|
||||
{ "name": "desert", "rgb": [238,221,153], "uplift_mm_yr": 0.10, "k_mult": 0.5,
|
||||
"detail": { "droplets_per_cell": 0.035, "strata_contrast": 0.92, "amplitude_m": [7, 14] } }
|
||||
```
|
||||
|
||||
- **`droplets_per_cell`** is the load-bearing one. A fifth of the running water turns a dendritic gully
|
||||
network into a few isolated wadis, which is most of what "arid" looks like from the air.
|
||||
- **`strata_contrast`** near 1 keeps the ledges sharp: mesas and benches survive because nothing is rounding
|
||||
them off.
|
||||
- **`amplitude_m`** raised on flat ground is dune. Two metres on a plain is a plain; ten is a sand sea.
|
||||
|
||||
Anything left out keeps the manifest's own number, and a legend that overrides nothing costs nothing — the
|
||||
class raster is not even carried through the detail passes.
|
||||
|
||||
### Putting the range inland
|
||||
|
||||
A uniformly painted island comes out at the angle of repose right down to the water, and that is arithmetic
|
||||
rather than a tuning miss: for `n = 1` the uplift rate alone fixes the hillslope angle, so the rivers cut down
|
||||
to sea level but the ground between them has no idea how far from the coast it is. Fjords, end to end.
|
||||
|
||||
`coastal_plain_km` ramps the rate up from the waterline, so there is a plain in front of the range:
|
||||
|
||||
```jsonc
|
||||
{ "name": "highland", "rgb": [68,170,102], "uplift_mm_yr": 0.25,
|
||||
"massif": { "floor_mm_yr": 0.045, "fraction": 0.30 },
|
||||
"coastal_plain_km": 4.0, "coastal_floor_mm_yr": 0.03 }
|
||||
```
|
||||
|
||||
```
|
||||
distance inland 0km 1km 2km 3km 4km 5km+
|
||||
uplift mm/yr 0.03 0.06 0.13 0.20 0.24 0.25
|
||||
at a divide 4° 9° 18° 27° 31° 32°
|
||||
typical ground 1° 3° 6° 9° 11° 12°
|
||||
```
|
||||
|
||||
(on a massif cell — the ramp runs up to whatever rate the massif field left there, and it only ever ramps
|
||||
*down* towards the sea, so a cell of plain between two massifs keeps its floor rather than being raised into a
|
||||
rim of hills round the edge of the continent.)
|
||||
|
||||
It never goes to zero at the water, and that matters: an earlier version of the generator tapered the uplift
|
||||
to nothing at the shore as a hidden side effect, which flattened the hundred-metre strip the surf works in and
|
||||
moved every cliff inland (D-52). This is you saying where the range starts.
|
||||
|
||||
### Craters
|
||||
|
||||
An impact is an event, not a rate, and it cannot be painted as one — a closed basin built from negative uplift
|
||||
is filled in by the priority-flood within a hundred steps, because the flood raises every depression to its
|
||||
spill level on every one of them. So a crater class is **stamped onto the finished terrain after the solve**,
|
||||
and its shape comes from the blob you painted: distance inward from its own shoreline, normalised by its
|
||||
widest point, so the same four numbers describe a small crater and a large one.
|
||||
|
||||
```jsonc
|
||||
{ "name": "crater", "rgb": [124,117,111], "uplift_mm_yr": 0.15,
|
||||
"crater": { "rim_m": 340, "floor_m": 60, "rim_at": 0.30, "wall_at": 0.62 } }
|
||||
```
|
||||
|
||||
```
|
||||
sea ~~~~\ /‾‾‾\_________/‾‾‾\ /~~~~ sea
|
||||
\ / rim rim \ /
|
||||
\____/ \____/
|
||||
t: 0 0.30 0.62 .. 1 floor
|
||||
```
|
||||
|
||||
`rim_at` is where the crest sits and `wall_at` where the inner wall reaches the floor, both as a fraction of
|
||||
the way in from the shore. `floor_m` above zero keeps the basin dry; the floor is a closed basin, so expect it
|
||||
to pond in the detail passes unless there is a gap in the rim. A crater that straddles the seam is one crater.
|
||||
|
||||
The class's `uplift_mm_yr` still matters a little — it is what the solve does to the flanks before the stamp
|
||||
lands — so keep it low. It is not what makes the crater.
|
||||
|
||||
**Uplift is the number that matters and the one to turn first.** Steady-state slope is `U/(K·A^m)`, applied
|
||||
down to a single cell, so at an 8 m cell and `K` 5e-5 a rate of 0.03 mm/yr is about a 4° hillslope, 0.08 about
|
||||
8°, and anything past roughly 0.45 is above the angle of repose and shaped by landsliding rather than by
|
||||
rivers. Raising the rate makes the summits *higher*; it does not make the ground steeper past that point.
|
||||
|
||||
### The white problem
|
||||
|
||||
Every hand-painted world map draws white twice: the polar caps, and an outline stroke around each island.
|
||||
Exactly one class can own that colour, and it has to be the stroke, because the stroke is the one that must be
|
||||
recognised wherever it appears. What the caps become is then a `derived` class:
|
||||
|
||||
```jsonc
|
||||
{ "name": "ice", "derived": true, "rgb": [250, 250, 250], "uplift_mm_yr": 0.05 },
|
||||
{ "name": "stroke", "rgb": [238, 238, 238], "stroke": true, "edge_class": "ice" }
|
||||
```
|
||||
|
||||
A white region touching the top or bottom row is a cap and becomes `ice`; every other white dissolves into
|
||||
whichever real class is nearest, split down its middle rather than given wholly to one side — which is the
|
||||
only answer that does not move the coastline by the width of the artist's brush.
|
||||
|
||||
The plan's two counters are how you check it landed: `Map3.jpg` reports **2,084,442 px rescued at the poles
|
||||
and 0 dissolved**, which is the evidence that on this template white is only ever the caps.
|
||||
|
||||
## The palette
|
||||
|
||||
How a preview is *drawn* is a separate file from what the painting *means*, and deliberately so: the legend is
|
||||
about the world, the palette is about the picture. Nothing in a palette changes a height — two bakes of the
|
||||
same world under two palettes are the same terrain — so swapping one is cheap and reversible.
|
||||
|
||||
```bash
|
||||
# write the defaults out to start from
|
||||
Tools/Terrain/bin/terrain.exe palette RawContent/World/Templates/mine.palette.json
|
||||
```
|
||||
|
||||
Then point the planet manifest at it with `"palette": "Templates/mine.palette.json"`. Leaving the key out
|
||||
uses the same numbers, so the file is a starting point rather than a requirement.
|
||||
|
||||
It holds the hypsometric ramp (`land_stops`, sea level at `t` 0 to the top of the land at `t` 1), the water
|
||||
(`sea_shallow`, `sea_deep`), the rivers, the ice, where the ramp's top is taken from
|
||||
(`land_top_percentile` — a percentile rather than the maximum, so one high summit cannot push a whole
|
||||
continent into the bottom of the ramp), and the light (`sun_azimuth_deg`, `sun_altitude_deg`, `ambient`,
|
||||
`gain`). Anything left out keeps the default; a misspelt key is an error rather than a setting that silently
|
||||
does nothing. Keys beginning with an underscore are comments.
|
||||
|
||||
## Reading a plan
|
||||
|
||||
`terrain plan` writes the maps and `plan.json` into `RawContent/World/Plan/`, in about four seconds. In
|
||||
order of what they answer:
|
||||
|
||||
- **`map_class.png`** — did the legend read the painting? If this is not the picture you drew, nothing
|
||||
downstream can be. The match report beside it counts pixels no class was near.
|
||||
- **`map_regions.png`** — how the planet was cut up. Each region is one hue, its land saturated and the ocean
|
||||
margin it carries the same hue dimmed. Check that a landmass straddling the seam is one colour and not two,
|
||||
and that nothing has swallowed the ocean.
|
||||
- **`map_uplift.png`** — the field everything else is a consequence of, the massif fabric included, so a class
|
||||
with a `massif` block shows as plain with upland in it rather than as one flat colour across the landmass.
|
||||
The one to read when the ground comes out the same everywhere.
|
||||
- **`map_erodibility.png`** — where texture inside a range will come from.
|
||||
- **`map_overlay.png`** — the annotation layer over a dimmed class map, when there is one. A mark means
|
||||
nothing on its own and everything against the coastline or the range it was drawn along.
|
||||
|
||||
And the class table above them, which is the four seconds best spent. It prints every class's uplift rate and
|
||||
then **two** angles, and which of the two you read is the difference between a legend that is right and one
|
||||
that is three times too hot:
|
||||
|
||||
```
|
||||
class share cells uplift mm/yr K depth m divide typical
|
||||
lowland 14.4% 10967451 0.080 1.00 - 11.3 deg 3.8 deg rolling
|
||||
massif over 16% of it; the other 84% is 0.012 mm/yr, 1.7 deg at a divide and 0.6 typical - plain
|
||||
```
|
||||
|
||||
The same two angles are in World Orogen's class table, beside the inputs that set them (D-67), which is the
|
||||
place to read them while you are still typing the numbers.
|
||||
|
||||
`divide` is the steepest ground the rate can make: steady state is `S = U/(K·A^m)`, `A` is smallest at the top
|
||||
of a catchment, and that is where the number comes from. `typical` is the median slope over the class, and it
|
||||
is about **a third** of the divide angle in tangent — measured on a 600² grid of 8 m cells over a factor of
|
||||
twenty in rate, where the ratio came out 0.34, 0.33, 0.33 and 0.32. Almost none of a map is divide. Read the
|
||||
second column; it is the ground somebody stands on, and `reads as` is taken from it.
|
||||
|
||||
Then the block that says what a re-roll would move: the rock provinces and the fault set, with a trace count
|
||||
per class. And if there is an overlay, how much of the world each mark covers, how many pieces it is in, and
|
||||
what it asked of the coastline.
|
||||
|
||||
Then the region table, which is where the bake time goes. **The margin is the lever.** It decides which
|
||||
landmasses are solved together: two within twice the margin share a box. At 0.5 km this template gives 18
|
||||
regions; scattered islets on some other template would each cost a whole region, and `--margin-km` is how to
|
||||
find out before spending the afternoon.
|
||||
|
||||
**Read the time estimate as a floor.** It is calibrated on lowland, and mountains cost about five times as
|
||||
much per cell — steep ground drives the hillslope law to its full sub-step budget every step while a plain
|
||||
leaves it idle. Measured on this template: a 14.0 M cell lowland region took 1014 s and a 4.0 M cell highland
|
||||
region took 1394 s. So raising an `uplift_mm_yr` does not only change the terrain, it changes the bake time.
|
||||
|
||||
## The detail bake
|
||||
|
||||
`terrain tiles` runs the detail passes over a geology bake and writes the ground somebody can stand on. It
|
||||
reads the bake's heightmap off disk rather than solving anything, which is what makes it batchable: the
|
||||
geology is hours, a 5 km tile is about twelve seconds, and the islands you care about can be baked first.
|
||||
|
||||
```bash
|
||||
# every tile, about forty minutes for a 100 km world. --bake defaults to the newest Bake_NNN.
|
||||
Tools/Terrain/bin/terrain.exe tiles
|
||||
|
||||
# a rectangle of them, by tile index, from a particular bake
|
||||
Tools/Terrain/bin/terrain.exe tiles --bake RawContent/World/Bake_001 --only 11,7,13,8
|
||||
|
||||
# the geology upsampled and nothing else: is what you are looking at the solve or the detail passes?
|
||||
Tools/Terrain/bin/terrain.exe tiles --only 16,6,16,6 --no-detail
|
||||
```
|
||||
|
||||
Each tile gets `Planet_x11_y07.png` (16-bit height, 2500 x 2500 at 2 m), `_shade.png` (a hillshade at the same
|
||||
resolution, because a 16-bit grey PNG of a hundred metres of relief is a flat grey rectangle to look at), and
|
||||
`_flow`, `_wear` and `_deposit` as 8-bit maps of what the droplets did. `tiles.json` beside them carries the
|
||||
grid, the cell size, each tile's world origin and its elevation range.
|
||||
|
||||
If the planet has an overlay, `_overlay.png` comes too — 8-bit, one mark index a detail cell — with
|
||||
`overlay.json` beside `tiles.json` as its key and the feature list. The mask is sampled through world metres
|
||||
rather than through a tile-local index, so a cell reads the same mark whichever tile reaches it.
|
||||
|
||||
The tiles are seamless and that is measured, not asserted: a tile carries a margin of three droplet lifetimes
|
||||
which is discarded afterwards, and every hash and noise lattice in the passes is keyed on absolute world
|
||||
position, so a cell in one tile's interior comes out as it would have in one whole-world run. The margin costs
|
||||
about five per cent of a tile on each side.
|
||||
|
||||
**What is not there yet:** the shore, at *detail* resolution. The geology bake has a shelf, a surf-cut
|
||||
platform and a beach as of D-60, but the detail passes do not refine any of it: a tile's coastline is the
|
||||
8 m coastal pass upsampled, with no sand, no boulder field and no plan-view crenulation at 2 m. Everything
|
||||
inland is finished.
|
||||
|
||||
## What a bake writes
|
||||
|
||||
### `preview.png` is relative, and that matters more than it sounds
|
||||
|
||||
The hypsometric ramp's top is `land_top_percentile` of **the land in the world being drawn**, not a fixed
|
||||
height. So bare rock and snow on a preview mean "the highest ground here" and say nothing at all about scale.
|
||||
|
||||
Measured on this template's central lowland: it bakes to **0..47 m** with a median hillslope of **0.61°**,
|
||||
4.4 % of it over three degrees and nothing anywhere over eight — a plain by any reading. Drawn against its own
|
||||
32 m ceiling it comes out with tan uplands and white caps on its 40 m hills, which is exactly what a 2800 m
|
||||
range looks like, and it is the single most misleading thing the generator produces. Redrawn at a fixed 400 m
|
||||
ceiling the same heightmap is flat green with four pale massifs on it.
|
||||
|
||||
Every run prints which ceiling its preview used, so read that line before reading the picture:
|
||||
|
||||
```
|
||||
preview the hypsometric ramp tops out at 32 m - the 99.5% percentile of *this* world's land, so rock and
|
||||
snow mean "the highest ground here" and nothing about scale. Set palette.land_top_m for an
|
||||
absolute ramp
|
||||
```
|
||||
|
||||
`palette.land_top_m` is a height in metres; `0` keeps the percentile. The percentile stays the default on
|
||||
purpose — an absolute ramp over a world with no mountains is a flat green shape with nothing legible on it,
|
||||
and "is there drainage here" is a question the contrast has to answer. When what you want to know is *how
|
||||
steep is this ground*, read `map_slope.png` and the plan's `typical` column, not the tint.
|
||||
|
||||
`planet_height.png` at the geology grid (12500 × 6076 at 8 m for this world), plus quarter and tenth
|
||||
overviews; `preview.png` with hypsometric tint, hillshade and rivers; the plan's maps again, now over real
|
||||
terrain; `map_slope.png` and `map_flow.png`; and `meta.json` with the resolved manifest, the legend, the
|
||||
match report and every region's statistics.
|
||||
|
||||
And **the two coastal maps**, which are the whole of what the pass did:
|
||||
|
||||
- **`map_coast.png`** — what it moved, cool where the surf cut and warm where the sediment landed, on a
|
||||
±30 m scale. The deep sea floor is masked out of it on purpose: the ocean drops five hundred metres in
|
||||
this one pass and would otherwise set the scale for the few metres the shore processes move, which is the
|
||||
thing the map exists to show.
|
||||
- **`map_exposure.png`** — how open each stretch of shore is to the sea, drawn only within a kilometre of
|
||||
the water. Not tidiness: exposure is measured *on* the waterline and carried inland by "the nearest stretch
|
||||
of shore to you", so past a few hundred metres it stops being a coastal quantity and becomes a picture of
|
||||
the continent's medial axis.
|
||||
|
||||
The pass is one line in the log and one block in `meta.json`, and it runs **once over the whole cylinder**
|
||||
rather than per region — fetch crosses straits, and a sediment budget split in two is not a budget:
|
||||
|
||||
```
|
||||
coast: 971 km of shoreline, 65% sea, shelf 60% of it; surf planed 3.5 km2 and cut 34.18 Mm3,
|
||||
6 river mouths delivered 1.15 Mm3, 35.32 Mm3 laid (0% unplaced) as 4.14 km2 of new beach
|
||||
```
|
||||
|
||||
`0% unplaced` is the line to read: it is the sediment the budget could not find a home for, and a figure that
|
||||
is not near zero means the drift ran out of shore before it ran out of sand. On a `--only` run the backshore
|
||||
figures read 0 m, correctly — most of the planet's land is unsolved and sitting at sea level.
|
||||
|
||||
It is also the memory peak of a bake, at about **8.3 GB** of working set against the solve's 3.6, and it is
|
||||
worth knowing that before starting one on a smaller machine.
|
||||
|
||||
And **the verdict block**, which a bake prints for the first time as of D-59: the slope distribution, the
|
||||
slope-area fit, the hypsometric integral, the drainage density, and the per-uplift-class breakdown that is
|
||||
the one to read. It is pooled from the regions rather than computed on a grid that never exists - a histogram
|
||||
adds, so summing the pieces and taking a quantile of the sum is exactly what one pass over the whole world
|
||||
would have given.
|
||||
|
||||
A run with `--only` marks itself **PARTIAL**: its extent is the whole cylinder while its ground statistics
|
||||
cover just the landmasses that were solved, and the two are not comparable.
|
||||
|
||||
```
|
||||
field -543..340 m; land 0..9 m (relief 9 m), 36% land, 0.00% clipped
|
||||
PARTIAL: the line above is the whole world; everything below is the 3% of its land that
|
||||
was actually solved (842682 of 27455176 cells). The two are not comparable.
|
||||
slopes: 100% under 15 deg, 100% under 30, 0.0% over 50, median 0.5 deg
|
||||
...
|
||||
by uplift class:
|
||||
plain 0.00..0.10 mm/yr 100% of land slope 0.5 deg median, 0.8 P90 relief 3 m/500 m ...
|
||||
```
|
||||
|
||||
The polar pad never appears in any of them. It is a few rows of synthetic ocean above and below the painted
|
||||
map, added so that a cap touching the top row has a shore to drain into — without it the solver would treat
|
||||
the map edge as an outlet and freeze the ice at its initial relief.
|
||||
|
||||
## Limits worth knowing
|
||||
|
||||
- **A landmass cannot ring the planet.** A region is flattened into a rectangle with water on both sides,
|
||||
because a grid edge is an outlet; one that goes all the way round has no such edges. The run refuses it
|
||||
rather than silently freezing the coast. Break it with a strait.
|
||||
- **The seed alone no longer names a world.** The margin and the minimum landmass size change how the planet
|
||||
is cut up, and the priority-flood's traversal across a flat depends on the box it is flooding. All three are
|
||||
in `Planet.json` and all three are recorded in `meta.json`.
|
||||
- **The elevation range is a hard clip.** A run whose clip fraction is above 0.1 % is a failed run, not a
|
||||
rounded one, and painted uplift makes it easy to ask for more relief than the range holds. `U/K` is the one
|
||||
relief knob.
|
||||
|
||||
## The same painting in World Orogen
|
||||
|
||||
`Tools/Orogen/` (D-66) reads exactly these files. Serve the folder with any static server and open the
|
||||
import page's **Painted Map** source:
|
||||
|
||||
```bash
|
||||
npx serve Tools/Orogen # then http://localhost:3000/import
|
||||
```
|
||||
|
||||
Choose `Map3_002.png` as the painting and `Map3.legend.json` as the legend, press **Solve Terrain**, and in
|
||||
about half a minute the painting is a globe with rivers, climate and a satellite view, plus class, uplift,
|
||||
erodibility, drainage, slope and basin layers to inspect and export. The table under the legend edits the
|
||||
classes' numbers and **Download legend JSON** writes them back into a copy of this file with the commentary
|
||||
intact, so a legend tuned there runs here unchanged.
|
||||
|
||||
What it is for is the thirty-second question - does this painting make the rivers I meant, is that lowland
|
||||
a plain with massifs in it or a slab - before the two-hour bake answers the eight-metre one. What it is not:
|
||||
the globe is Earth-sized (legend distances are scaled by the ratio of circumferences), the cells are ~44 km,
|
||||
and faults, craters, the plates, the repose clamp and the detail passes are not carried because nothing at
|
||||
that cell size could show them.
|
||||
|
||||
**It reads this planet's own files now, and the studio serves them.** Point it at `Planet.json` and the
|
||||
planet block, the lithology multipliers, the seed and the geology grid's constants travel without being
|
||||
retyped; it outranks a legend's own `planet` block, because it is the file the bake reads. The **overlay**
|
||||
is carried too - as a texture over the globe and the map rather than voted onto the mesh, since a road is
|
||||
a few pixels wide and a cell there is tens of kilometres - and its `coast_jitter` marks pin or roughen the
|
||||
shore on the sphere exactly as they do here.
|
||||
|
||||
Easiest of all, while `terrain studio` is running, its **From terrain studio** button loads the painting,
|
||||
both legends and the manifest in one step, including strokes made since the last save, because the studio
|
||||
holds the painting in memory. The studio shares reads and only reads: its CORS header is on GET alone and it
|
||||
answers no preflight, so a browser tab can look at the planet and can never start a bake.
|
||||
|
||||
**What it still cannot tell you** is the ground at eight metres. The bake owns the geology grid, the coast
|
||||
pass, the faults, the craters and the detail tiles, and only they produce ground a player can stand on.
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"_comment": "How a preview is drawn. Nothing here changes a height - two bakes of the same world under two palettes are the same terrain. Point a planet manifest at this file with \"palette\": \"<path relative to the manifest>\"; leave it out and these numbers are used anyway. Keys beginning with an underscore are comments.",
|
||||
|
||||
"_comment_land_stops": "The hypsometric ramp: sea level at t 0 to the top of the land at t 1. The top is a percentile rather than the maximum, so one high summit cannot push a whole continent into the bottom of the ramp.",
|
||||
"land_stops": [
|
||||
{ "t": 0, "rgb": [72, 106, 68] },
|
||||
{ "t": 0.08, "rgb": [104, 132, 74] },
|
||||
{ "t": 0.2, "rgb": [142, 152, 88] },
|
||||
{ "t": 0.38, "rgb": [164, 148, 104] },
|
||||
{ "t": 0.58, "rgb": [150, 128, 106] },
|
||||
{ "t": 0.75, "rgb": [138, 130, 128] },
|
||||
{ "t": 0.88, "rgb": [176, 174, 174] },
|
||||
{ "t": 1, "rgb": [246, 246, 250] }
|
||||
],
|
||||
|
||||
"sea_shallow": [56, 104, 136],
|
||||
"sea_deep": [18, 40, 72],
|
||||
"river": [70, 132, 180],
|
||||
"_comment_ice": "Drawn wherever a class is marked snow, whatever height the ground stands at. Not pure white: white has nowhere left to go under the hillshade, so an ice sheet comes out as a flat cut-out with no shape in it at all.",
|
||||
"ice": [232, 238, 245],
|
||||
|
||||
"_comment_top": "Where the top of the hypsometric ramp sits. The percentile is relative to the world being drawn, which is the only way a low continent is legible at all and is also a picture that lies about scale - measured on this planet's central landmass, a 47 m plain whose median slope is 0.6 degrees comes out with the same bare rock and white caps a 2800 m range would, because 40 m is the top of its own ramp. Set land_top_m to a height in metres for an absolute ramp instead; 0 keeps the percentile, and either way the run summary says which ceiling the preview was drawn against.",
|
||||
"land_top_percentile": 99.5,
|
||||
"land_top_m": 0,
|
||||
|
||||
"_comment_light": "Azimuth is degrees clockwise from north and altitude degrees above the horizon; north-west at 45 is what every DEM hillshade uses. Ambient is how lit the shaded side is - at zero a shadow is a hole - and gain how much the lit side brightens.",
|
||||
"sun_azimuth_deg": 315,
|
||||
"sun_altitude_deg": 45,
|
||||
"ambient": 0.45,
|
||||
"gain": 0.75
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3d3b0f70b6932b36974a8423bf6007e5e48020a46c6a2e9cc92ec7bf578f8b26
|
||||
size 2433944
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6996dd53fc49134b7f557579da0207eaf6896ebb426782dac66d31e976c5658
|
||||
size 81648
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:69215c65621f0a9d1fb30e5e5bf89e55bc6d11ca732c33e90fc5b0b7b3caa2b4
|
||||
size 40881
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2671e9e5a603e400db6b37fa5041e42ff6bf4683128265fe743dbbb16a2a9135
|
||||
size 3219685
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1f87aa648e33fdcc18b7bdae4ac3641af0b9a807986f8fdde8f60d7c5648fc2
|
||||
size 61095
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c3cb1bfc9994ca075e63928c50ecfff3ac33c45e4e2870c396433c4eb636ac3f
|
||||
size 2927709
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5677f8e3ea797425ad61d5b21be046c41d4c6131ec366779c06a898207836efd
|
||||
size 41778
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user