nx_worldpipe_core_relief_t140.nx source
↩ module page · 144 lines · 8645 B
1// nx_worldpipe_core.nx -- THE PURE STAGED-TERRAIN CORE. Extracted from nx_worldpipe.nx on
2// 2026-08-28 for gameengine rung GE14a, so the SHIPPING WASM ENGINE and the NAS-side pipeline
3// cannot disagree about the ground they respectively walk on and bake.
4//
5// WHY THE SPLIT EXISTS, MEASURED (the rung's original wording said "the engine imports
6// nx_worldpipe" and that is NOT IMPLEMENTABLE): nx_wasm_craft carries ZERO sys_mmap /
7// sys_read_file / sys_write -- it is allocator-free and syscall-free BY CONSTRUCTION, which is
8// exactly why it compiles to wasm ("wasm has no mmap; every buffer is an arena offset" is written
9// in its own arena map) -- while nx_worldpipe.wp_hydro_bake allocates 819,200 B and pulls
10// nx_trimesh and nx_terrain_erode_lib into the closure. So the pipeline splits along the line the
11// TARGET draws: everything here is PURE ARITHMETIC over (x,z) with no allocation and no syscall,
12// and every allocating stage (hydrology bake, erosion bake, features, lighting) stays in
13// nx_worldpipe. That is the boundary, and it is a property of the code, not a preference.
14//
15// ONE RULER, TWO CONSUMERS: the function bodies below are the SAME BYTES nx_worldpipe carried,
16// moved not copied -- nx_worldpipe now imports this lib and keeps its call sites unchanged, so a
17// copy cannot drift into a second answer for the same ground. The single source change is that the
18// moved code calls rlf_noise3 DIRECTLY instead of through nx_trimesh's one-line delegate
19// (tm_noise3 is literally `return rlf_noise3(...)`), because nx_relief_lib is 7,310 B of pure
20// arithmetic while nx_trimesh is a 45 KB mesh library the wasm target must not carry.
21// That change makes byte-identity of the rebuilt ELF unavailable as the neutrality proof, so the
22// proof is an EXTERNAL CONSTANT instead: nx_worldpipe_core_gate asserts a KAT of heights and
23// parameters harvested from the PRE-EXTRACTION organ.
24//
25// STAGE 1 params: 0 continentalness, 1 temperature, 2 humidity, 3 erosion, 4 weirdness, 5 river.
26// license_tier: ORIGINAL No hw writes (Rule 26).
27import "nx_syscalls.nx"
28import "nx_relief_lib.nx"
29import "nx_itrig.nx"
30import "nx_genp_allow_lib.nx"
31
32const WP_MAGIC_1400: i64 = 1400
33const WP_MAGIC_3400: i64 = 3400
34const WP_MAGIC_2600: i64 = 2600
35const WP_MAGIC_2100: i64 = 2100
36const WP_MAGIC_1700: i64 = 1700
37const WP_MAGIC_1300: i64 = 1300
38const WP_MAGIC_2400: i64 = 2400
39const WP_MAGIC_9973: i64 = 9973
40const WP_MAGIC_524287: i64 = 524287
41const WP_MAGIC_1024: i64 = 1024
42
43static WP_SEED: i64
44// ★AUTHORING KNOBS (the "describe a world" layer, 2026-07-13): global biases the NL front-end sets. Defaulted in
45// wp_init so every existing gate is byte-unchanged; the authoring path sets them AFTER wp_init.
46static WP_TEMP_BIAS: i64 // added to temperature field (colder = snowier)
47static WP_CONT_BIAS: i64 // added to continentalness (lower = more ocean/islands)
48static WP_MOUNT_SCALE: i64 // relief multiplier (default 1400 = current alpine strength)
49
50func wp_iabs(v: i64) -> i64 { if v < 0 { return 0-v } return v }
51func wp_clamp(v: i64, lo: i64, hi: i64) -> i64 { if v<lo {return lo} if v>hi {return hi} return v }
52
53// ---- STAGE 1: the parameter fields. k: 0 continentalness, 1 temperature, 2 humidity, 3 erosion, 4 weirdness, 5 river.
54// each = value noise on its own (seed,param) y-plane at its own frequency, centered to [-512, 511].
55func wp_param(x: i64, z: i64, k: i64) -> i64 {
56 var f: i64 = WP_MAGIC_3400 // continentalness: the longest wavelength (continents)
57 if k == 1 { f = WP_MAGIC_2600 } // temperature
58 if k == 2 { f = WP_MAGIC_2100 } // humidity
59 if k == 3 { f = WP_MAGIC_1700 } // erosion
60 if k == 4 { f = WP_MAGIC_1300 } // weirdness
61 if k == 5 { f = WP_MAGIC_2400 } // river field
62 let yk: i64 = WP_SEED*WP_MAGIC_9973 + k*WP_MAGIC_524287
63 var bias: i64 = 0
64 if k == 0 { bias = WP_CONT_BIAS } // continentalness bias (islands/continents)
65 if k == 1 { bias = WP_TEMP_BIAS } // temperature bias (snowy/hot)
66 return rlf_noise3(x, yk, z, f) - 512 + bias
67}
68
69// ---- STAGE 2: the continentalness->height SPLINE (piecewise linear over control points) ----
70func wp_spline(c: i64) -> i64 {
71 // knots: (-512,-430) (-220,-160) (-120,-40) (-40,15) (120,90) (300,190) (512,420)
72 if c <= 0-220 { return 0-430 + (c - (0-512))*((0-160) - (0-430))/((0-220) - (0-512)) }
73 if c <= 0-120 { return 0-160 + (c - (0-220))*((0-40) - (0-160))/((0-120) - (0-220)) }
74 if c <= 0-40 { return 0-40 + (c - (0-120))*(15 - (0-40))/((0-40) - (0-120)) }
75 if c <= 120 { return 15 + (c - (0-40))*(90 - 15)/(120 - (0-40)) }
76 if c <= 300 { return 90 + (c - 120)*(190 - 90)/(300 - 120) }
77 return 190 + (c - 300)*(420 - 190)/(512 - 300)
78}
79
80// ---- STAGES 2+3+4: terrain height from the params (relief = ridged * weirdness * (1-erosion), rivers carve) ----
81// exposed with explicit params so gates can prove each factor's effect in isolation.
82func wp_height_core(cont: i64, ero: i64, weird: i64, x: i64, z: i64) -> i64 {
83 let base: i64 = wp_spline(cont)
84 var ridge: i64 = 512 - wp_iabs(rlf_noise3(x, WP_SEED*WP_MAGIC_9973 + 6*WP_MAGIC_524287, z, 900) - 512)*2
85 if ridge < 0 { ridge = 0 }
86 let wamp: i64 = weird + 512 // 0..1023: weirdness scales the mountains
87 let eroF: i64 = 512 - ero // 1..WP_MAGIC_1024: erosion FLATTENS (high ero -> small relief)
88 let mfac: i64 = wp_clamp((cont + 40)*4, 0, WP_MAGIC_1024) // mountains only inland (not in the sea)
89 let mount: i64 = ((ridge*wamp/WP_MAGIC_1024)*eroF/WP_MAGIC_1024)*mfac/WP_MAGIC_1024*WP_MOUNT_SCALE/WP_MAGIC_1024 // relief (authoring-scalable; default WP_MAGIC_1400)
90 return base + mount
91}
92func wp_height_params(cont: i64, ero: i64, weird: i64, x: i64, z: i64) -> i64 {
93 let detail: i64 = (rlf_noise3(x, WP_SEED*WP_MAGIC_9973 + 7*WP_MAGIC_524287, z, 300) - 512)/8
94 return wp_height_core(cont, ero, weird, x, z) + detail
95}
96// the FLOW surface: core only -- water descends the VALLEY-scale terrain, not micro-bumps
97func wp_flow_h(x: i64, z: i64) -> i64 {
98 let cont: i64 = wp_param(x, z, 0)
99 let ero: i64 = wp_param(x, z, 3)
100 let weird: i64 = wp_param(x, z, 4)
101 return wp_height_core(cont, ero, weird, x, z)
102}
103// raw terrain (stages 2+3 only -- what the water flows OVER)
104func wp_height_raw(x: i64, z: i64) -> i64 {
105 let cont: i64 = wp_param(x, z, 0)
106 let ero: i64 = wp_param(x, z, 3)
107 let weird: i64 = wp_param(x, z, 4)
108 return wp_height_params(cont, ero, weird, x, z)
109}
110
111// Dune basis moved from nx_wasm_craft_dunes_t70. Packed recipe is admitted
112// by genp_dune_valid at the public initialization boundary. No allocation.
113// Phase is the engine's existing seed hash in angle units; coordinates are
114// absolute blocks. This is analytic relief, not sediment transport.
115const WP_HEIGHT_Q: i64 = 256
116func wp_dune_wave_q(packed: i64, x: i64, z: i64, phase_seed: i64) -> i64 {
117 if packed == 0 { return 0 }
118 let amplitude: i64 = packed & GA_DUNE_AMP_MASK
119 let wavelength: i64 = (packed >> GA_DUNE_WAVE_SHIFT) & GA_DUNE_WAVE_MASK
120 let heading: i64 = ((packed >> GA_DUNE_HEADING_SHIFT) & GA_DUNE_WAVE_MASK)*2*IT_PI/256
121 let dx: i64 = it_cos4096(heading)
122 let dz: i64 = it_sin4096(heading)
123 let along: i64 = x*dx + z*dz
124 let across: i64 = z*dx - x*dz
125 let bend: i64 = it_sin4096(across*2*IT_PI/(wavelength*4*IT_FX) + phase_seed)*IT_PI2/IT_FX
126 let phase: i64 = along*2*IT_PI/(wavelength*IT_FX) + phase_seed + bend
127 let wave: i64 = 4*it_cos4096(phase) + it_sin4096(2*phase)
128 return amplitude*(5*IT_FX + wave)/(10*IT_FX)
129}
130// Signed inland distance comes from the existing ocean edge owner. Return
131// exactly zero at/on the seaward side; smoothstep fades over one wavelength.
132func wp_dune_coast_q(relief: i64, inland: i64, wavelength: i64) -> i64 {
133 if inland <= 0 { return 0 }
134 if inland >= wavelength { return relief }
135 let f: i64 = inland*WP_HEIGHT_Q/wavelength
136 let fade: i64 = f*f*(3*WP_HEIGHT_Q-2*f)/(WP_HEIGHT_Q*WP_HEIGHT_Q)
137 return relief*fade/WP_HEIGHT_Q
138}
139// Keep the incumbent voxel quantization offset. Only newly crossed integer
140// height boundaries move a column; enabling zero relief cannot re-round it.
141// Both Q heights are already clamped by the existing terrain owner.
142func wp_dune_raw_delta(legacy: i64, base_q: i64, raised_q: i64) -> i64 {
143 return legacy + raised_q/WP_HEIGHT_Q - base_q/WP_HEIGHT_Q
144}