code wiki / (root) / nx_procgen_biome.nx

nx_procgen_biome.nx source

↩ module page · 145 lines · 5813 B

1// nx_procgen_biome.nx -- biome map as a CAUSAL DERIVATION from elevation 2// + moisture (PROCGEN arc P2 = EXCEED10 ladder R1; the composed-primitives 3// cardinal: biomes are LAW, not paint). 4// 5// The grader's BIOME_COHERENCE axis (nx_world_quality_grader) was 6// structurally ABSENT (=0) because no generator ever produced a biome map. 7// This module produces one the only honest way: each cell's biome follows 8// from WHERE it is -- 9// 10// elevation band = the SAME quartile law the grader checks 11// (low / mid / high / peak over observed range) 12// moisture = a second perlin-fbm field (independent seed stream) 13// picks the biome WITHIN the band, Whittaker-style 14// 15// Band law (must stay in lockstep with _wqg_biome_expected_band): 16// LOW : DESERT (dry) | TUNDRA (wet) 17// MID : GRASSLAND < BOREAL_FOREST < TEMPERATE_FOREST < TROPICAL_RAINFOREST 18// (driest -> wettest) 19// HIGH : SNOW 20// PEAK : ICE 21// 22// Coherence is lawful BY CONSTRUCTION; the grader is the independent 23// instrument that proves the law holds (and catches any future drift 24// between this table and the grader's). Moisture creates the within-band 25// variety that the render/material rung (B4) will consume as color bands. 26// 27// Deterministic per (heightmap, seed): same inputs -> byte-identical map. 28// license_tier: ORIGINAL 29 30import "nx_syscalls.nx" 31import "nx_tier.nx" 32import "nx_perlin.nx" 33 34// ===== Biome IDs (sealed; values fixed by the grader's expected-band law) 35const NX_BIOME_TUNDRA: nx_int = 0 36const NX_BIOME_BOREAL: nx_int = 1 37const NX_BIOME_GRASSLAND: nx_int = 4 38const NX_BIOME_TEMPERATE: nx_int = 5 39const NX_BIOME_DESERT: nx_int = 8 40const NX_BIOME_TROPICAL: nx_int = 11 41const NX_BIOME_SNOW: nx_int = 12 42const NX_BIOME_ICE: nx_int = 13 43// Histogram length covering all IDs above (grader takes ids 0..13). 44const NX_BIOME_ID_SPAN: nx_int = 14 45 46// Moisture thresholds (Q10, fbm band is ~[-1024, 1024]). 47const NX_BIOME_MOIST_DRY: nx_int = 0 - 256 48const NX_BIOME_MOIST_MID: nx_int = 0 49const NX_BIOME_MOIST_WET: nx_int = 256 50 51// Seed offset for the moisture stream: biome moisture must be independent 52// of the heightmap's noise stream but derived from the SAME world seed 53// (one seed reproduces the whole world). 54const NX_BIOME_MOISTURE_SEED_OFF: nx_int = 9173 55 56// Moisture field sampling (matches the heightmap's cell step so moisture 57// patches have landscape-scale coherence). 58const NX_BIOME_MOIST_STEP_Q10: nx_int = 102 59const NX_BIOME_MOIST_OCTAVES: nx_int = 3 60const NX_BIOME_MOIST_PERSIST: nx_int = 512 61 62func nx_biome_is_valid(b: nx_int) -> nx_int { 63 if b == NX_BIOME_TUNDRA { return 1 } 64 if b == NX_BIOME_BOREAL { return 1 } 65 if b == NX_BIOME_GRASSLAND { return 1 } 66 if b == NX_BIOME_TEMPERATE { return 1 } 67 if b == NX_BIOME_DESERT { return 1 } 68 if b == NX_BIOME_TROPICAL { return 1 } 69 if b == NX_BIOME_SNOW { return 1 } 70 if b == NX_BIOME_ICE { return 1 } 71 return 0 72} 73 74// Elevation band 0..3 -- IDENTICAL comparisons to the grader's 75// _wqg_biome_coherence_score so coherence can never drift by construction. 76func nx_biome_band(v: nx_int, hmin: nx_int, range: nx_int) -> nx_int { 77 let low_max: nx_int = hmin + range / 4 78 let mid_max: nx_int = hmin + (range * 2) / 4 79 let hi_max: nx_int = hmin + (range * 3) / 4 80 var band: nx_int = 0 81 if v > low_max { band = 1 } 82 if v > mid_max { band = 2 } 83 if v > hi_max { band = 3 } 84 return band 85} 86 87// The classification law: (band, moisture) -> biome. Pure, table-shaped, 88// no state -- the Whittaker diagram as a function. 89func nx_biome_pick(band: nx_int, moisture_q10: nx_int) -> nx_int { 90 if band == 0 { 91 if moisture_q10 < NX_BIOME_MOIST_MID { return NX_BIOME_DESERT } 92 return NX_BIOME_TUNDRA 93 } 94 if band == 1 { 95 if moisture_q10 < NX_BIOME_MOIST_DRY { return NX_BIOME_GRASSLAND } 96 if moisture_q10 < NX_BIOME_MOIST_MID { return NX_BIOME_BOREAL } 97 if moisture_q10 < NX_BIOME_MOIST_WET { return NX_BIOME_TEMPERATE } 98 return NX_BIOME_TROPICAL 99 } 100 if band == 2 { return NX_BIOME_SNOW } 101 return NX_BIOME_ICE 102} 103 104// Build the biome map for a heightmap. out_map: w*h i64s, parallel to 105// heightmap. Deterministic per (heightmap contents, seed). 106// Flat map (range 0): every cell is band 0 -> moisture still differentiates 107// DESERT/TUNDRA (the grader skips coherence on range 0; we stay valid). 108func nx_biome_map_build(heightmap: *i64, w: nx_int, h: nx_int, 109 seed: nx_int, out_map: *i64) -> nx_int { 110 if w <= 0 { return 0 - 1 } 111 if h <= 0 { return 0 - 1 } 112 let n: nx_int = w * h 113 114 // pass 1: observed range (the band law is relative, like the grader's) 115 var hmin: nx_int = heightmap[0] 116 var hmax: nx_int = heightmap[0] 117 var i: nx_int = 0 118 while i < n { 119 let v: nx_int = heightmap[i] 120 if v < hmin { hmin = v } 121 if v > hmax { hmax = v } 122 i = i + 1 123 } 124 let range: nx_int = hmax - hmin 125 126 // pass 2: moisture stream + classification 127 let ms: *PerlinState = nx_perlin_alloc(seed + NX_BIOME_MOISTURE_SEED_OFF) 128 var y: nx_int = 0 129 while y < h { 130 var x: nx_int = 0 131 while x < w { 132 let idx: nx_int = y * w + x 133 let m: nx_int = nx_perlin_fbm_2d(ms, 134 x * NX_BIOME_MOIST_STEP_Q10, 135 y * NX_BIOME_MOIST_STEP_Q10, 136 NX_BIOME_MOIST_OCTAVES, 137 NX_BIOME_MOIST_PERSIST) 138 let band: nx_int = nx_biome_band(heightmap[idx], hmin, range) 139 out_map[idx] = nx_biome_pick(band, m) 140 x = x + 1 141 } 142 y = y + 1 143 } 144 return 0 145}