code wiki / (root) / nx_terrain_erode_lib.nx

nx_terrain_erode_lib.nx source

↩ module page · 164 lines · 8548 B

1// nx_terrain_erode_lib.nx -- TERRAIN-SCALE EROSION COMPOSITION + ITS REFEREE (PG20, procgen.plan). 2// 3// WHY THIS LIB EXISTS. nx_water_erosion.nx ships all seven Mei 2007 stages and is gate-proven 4// (nx_water_erosion_gate, PG18) -- and the ranker measured it LIB-UNIMPORTED: nothing in the estate 5// composed it. PG20's contract is that composition onto a terrain heightfield, plus the thermal-talus 6// companion Musgrave 1989 pairs with hydraulic erosion, plus the referee that decides whether the 7// result reads as terrain rather than as blobs. 8// 9// THREE THINGS LIVE HERE AND NOTHING ELSE: 10// 11// 1. te_thermal_pass -- THE ONE TALUS PASS IN THE ESTATE. 12// nx_pets_voxel3d.vp_erode_pass already implemented the voxel form (shed exactly one unit to every 13// 4-neighbour overhung by more than talus, mass-conserving by construction). That is a real 14// lower-layer kernel and it is NOT deleted: this function GENERALISES it by one parameter and 15// REPRODUCES it exactly at flux_q14 == 0, which is what makes retiring the private copy onto this 16// owner provable rather than asserted. flux_q14 > 0 selects the Musgrave proportional law that a 17// continuous heightfield needs, where a fixed one-unit shed would be a voxel quantum applied to a 18// quantity that has no voxels. 19// INDEX ORDER IS NOT A CONVENTION HERE, IT IS A PROVEN PROPERTY. vp_erode_pass indexes x*gn+z and 20// nx_water_erosion indexes y*w+x. On a square grid the four neighbours of cell k are k-1, k+1, 21// k-gn, k+gn under BOTH, the boundary guards map onto each other exactly, and the deltas are 22// accumulated additively from the pre-pass state -- so the two orders produce byte-identical 23// output. The gate asserts that against a reference written in the other order rather than 24// leaving it as a comment nobody can check. 25// 26// 2. te_hypsometric_permil -- THE REFEREE. Strahler's hypsometric integral, 27// (mean - min) / (max - min), in permille. It is the right referee for a procedural world for a 28// reason that is decidable before any measurement is taken: IT IS DIMENSIONLESS. The plan named 29// two published bands, hypsometric curve and DRAINAGE DENSITY -- and drainage density is 30// kilometres of channel per square kilometre, so quoting a published band for it against a world 31// with no metric scale binding would be a number about a subject we do not have. That half is 32// declared open by name in the gate, not half-measured. 33// The integral is invariant under h -> a*h + b for a > 0, which is exactly what makes it 34// non-gameable: an implementation that merely lowers the terrain, or flattens it, or adds noise, 35// moves it NOWHERE. Only moving mass from highs into lows moves it, and that is erosion. 36// A flat field has no hypsometry, so this ABSTAINS with -1 rather than returning a number. 37// 38// 3. te_erode_grid -- the composed loop: seed the Mei state from a caller-owned height grid, tick the 39// full seven-stage pipeline, interleave the talus pass, read the bed back. Every buffer is 40// caller-owned and sized by te_state_bytes / te_grid_bytes, so there is no cap to guess and no 41// allocation inside any loop. 42// 43// SCALE: this lib is SCALE-FREE. nx_water_erosion names its units metres; every quantity that decides 44// behaviour here is a RATIO (slope = height delta / cell size), so a caller passing height and cell 45// size in the same arbitrary world unit gets the same physics with no metre assumption anywhere. 46// 47// license_tier: ORIGINAL 48// genealogy_id: mei_2007_fast_hydraulic_erosion_gpu + musgrave_1989_eroded_fractal_terrains + 49// strahler_1952_hypsometric_analysis 50 51import "nx_syscalls.nx" 52import "nx_tier.nx" 53import "nx_water_erosion.nx" 54 55// Permille scale for the hypsometric integral. Named for its PURPOSE (the estate reports ratios in 56// permille) and used nowhere else in this file. 57const TE_PERMIL: i64 = 1000 58// The abstention value: "this field has no hypsometry", never a height and never a ratio. 59const TE_HYPSO_UNOBSERVABLE: i64 = 0 - 1 60 61// ===== buffer sizing, DERIVED from the composed organ's own stride ===== 62func te_state_bytes(gn: i64) -> i64 { return gn * gn * NX_WATER_STRIDE * NX_SIZEOF_NX_INT } 63func te_grid_bytes(gn: i64) -> i64 { return gn * gn * NX_SIZEOF_NX_INT } 64 65// ===== the talus law, one site ===== 66// Sheds material from src to dst when the overhang exceeds talus. Returns the amount moved (0 = none). 67// flux_q14 == 0 -> the VOXEL law: exactly one unit, reproducing nx_pets_voxel3d.vp_erode_pass. 68// flux_q14 > 0 -> the MUSGRAVE law: a fraction of the excess, halved because the pair of cells shares 69// the excess between them (moving all of it would overshoot past level). 70func te_shed(hg: *i64, delta: *i64, src: i64, dst: i64, h: i64, talus: i64, flux_q14: i64) -> i64 { 71 let dh: i64 = h - hg[dst] 72 if dh <= talus { return 0 } 73 var amt: i64 = 1 74 if flux_q14 > 0 { amt = (dh - talus) * flux_q14 / NX_WATER_Q / 2 } 75 if amt <= 0 { return 0 } 76 delta[src] = delta[src] - amt 77 delta[dst] = delta[dst] + amt 78 return amt 79} 80 81// One thermal-erosion pass over a square gn x gn grid, row-major (idx = y*gn + x). 82// Deltas are computed from the PRE-PASS state and applied afterwards, so the result does not depend 83// on iteration order. Every -amt has a matching +amt, so sum(hg) is invariant EXACTLY. 84// Returns the total material moved, so a caller can tell a real pass from a no-op. 85func te_thermal_pass(hg: *i64, gn: i64, talus: i64, flux_q14: i64, delta: *i64) -> i64 { 86 let n: i64 = gn * gn 87 var i: i64 = 0 88 while i < n { delta[i] = 0; i = i + 1 } 89 var moved: i64 = 0 90 var y: i64 = 0 91 while y < gn { 92 var x: i64 = 0 93 while x < gn { 94 let k: i64 = y * gn + x 95 let h: i64 = hg[k] 96 if x > 0 { moved = moved + te_shed(hg, delta, k, k - 1, h, talus, flux_q14) } 97 if x < gn - 1 { moved = moved + te_shed(hg, delta, k, k + 1, h, talus, flux_q14) } 98 if y > 0 { moved = moved + te_shed(hg, delta, k, k - gn, h, talus, flux_q14) } 99 if y < gn - 1 { moved = moved + te_shed(hg, delta, k, k + gn, h, talus, flux_q14) } 100 x = x + 1 101 } 102 y = y + 1 103 } 104 var j: i64 = 0 105 while j < n { hg[j] = hg[j] + delta[j]; j = j + 1 } 106 return moved 107} 108 109// ===== the referee ===== 110// Strahler's hypsometric integral in permille. Returns TE_HYPSO_UNOBSERVABLE on a flat or empty 111// field: a field with no relief has no hypsometry, and reporting a number there would acquit on a 112// measurement that could not be taken. 113func te_hypsometric_permil(hg: *i64, n: i64) -> i64 { 114 if n <= 0 { return TE_HYPSO_UNOBSERVABLE } 115 var mn: i64 = hg[0] 116 var mx: i64 = hg[0] 117 var sum: i64 = 0 118 var i: i64 = 0 119 while i < n { 120 let v: i64 = hg[i] 121 if v < mn { mn = v } 122 if v > mx { mx = v } 123 sum = sum + v 124 i = i + 1 125 } 126 if mx == mn { return TE_HYPSO_UNOBSERVABLE } 127 let mean: i64 = sum / n 128 return (mean - mn) * TE_PERMIL / (mx - mn) 129} 130 131// ===== state marshalling: only the BED field crosses, water and sediment persist across ticks ===== 132func te_seed_bed(state: *i64, hg: *i64, n: i64) -> i64 { 133 var i: i64 = 0 134 while i < n { state[i * NX_WATER_STRIDE + NX_WATER_OFF_H] = hg[i]; i = i + 1 } 135 return n 136} 137func te_read_bed(state: *i64, hg: *i64, n: i64) -> i64 { 138 var i: i64 = 0 139 while i < n { hg[i] = state[i * NX_WATER_STRIDE + NX_WATER_OFF_H]; i = i + 1 } 140 return n 141} 142 143// ===== the composed loop ===== 144// Every buffer is caller-owned: state (te_state_bytes), scratch and delta (te_grid_bytes each). 145// nx_water_erosion_init runs ONCE, so water, sediment and flux carry across ticks as the pipe model 146// requires; only the bed round-trips each tick so the talus pass can act on it. 147// Returns the total material the talus passes moved over the whole run. 148func te_erode_grid(hg: *i64, gn: i64, ticks: i64, rain_q14: i64, talus: i64, flux_q14: i64, 149 params: *i64, state: *i64, scratch: *i64, delta: *i64) -> i64 { 150 let n: i64 = gn * gn 151 te_seed_bed(state, hg, n) 152 nx_water_erosion_init(state, gn, gn) 153 var moved: i64 = 0 154 var t: i64 = 0 155 while t < ticks { 156 nx_water_erosion_tick(state, scratch, gn, gn, rain_q14, params) 157 te_read_bed(state, hg, n) 158 moved = moved + te_thermal_pass(hg, gn, talus, flux_q14, delta) 159 te_seed_bed(state, hg, n) 160 t = t + 1 161 } 162 te_read_bed(state, hg, n) 163 return moved 164}