code wiki / (root) / nx_gradient_blend.nx

nx_gradient_blend.nx source

↩ module page · 267 lines · 12366 B

1// nx_gradient_blend.nx -- smooth zone-to-zone transition primitive. 2// 3// Sixth demonstration of the kind-specific-generator cardinal 4// `feedback-kind-specific-generators-not-broad-noise`. Solves the 5// "biome edges are sharp cliffs" problem: when two zones (biomes, 6// elevation bands, climate regions, kind-generators) meet, they 7// SHOULDN'T abruptly snap from one to the other. Real-world biome 8// transitions are gradients over kilometres of varying ecological 9// pressure. This primitive ships the interpolation kernels. 10// 11// Caller pattern: 12// blended = nx_gradient_blend_q14(value_a, value_b, progress, curve) 13// where progress in [0, Q] (0 = pure A, Q = pure B). 14// 15// Sealed-enum easing curves give different aesthetic options: 16// LINEAR -- straight line, equal-weight blend 17// SMOOTHSTEP -- cubic Hermite, smooth at both ends (Perlin's 18// classic fade curve) 19// EASE_IN_OUT -- slow start + slow end (s-curve) 20// SHARP -- nearly-step but with thin transition band 21// CRENULATE -- alternating pattern (used for forest/grassland 22// transitions that look "fingery" rather than a 23// smooth line) 24// 25// V2 follow-ups (queued): 26// - 2D progress (perpendicular + along the transition line) 27// - Noise-perturbed transition for organic edges (composes 28// nx_perlin or nx_worley_noise to roughen the transition line) 29// - Catmull-Rom and bezier easing for N-way blends 30// - Multi-zone blending (3+ neighbours) 31// - "Fingery" forest edge via L-system extension 32// 33// Loss audit: Q14 integer arithmetic; cubic and s-curve evaluation 34// uses 64-bit intermediates to avoid overflow. Clamping at boundary 35// values to keep output bounded. CRENULATE relies on hash for 36// determinism. 37// 38// genealogy_id: perlin_2002_improving_noise + ken_perlin_smoothstep + 39// whittaker_1975_community_ecology 40// lineage_id: nx_gradient_blend_5curve_q14_v1 41 42// nx_safety_envelope: 43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 44// sil_target: SIL1 45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 46// verdict: NOT_YET_EVALUATED 47 48import "nx_syscalls.nx" 49import "nx_tier.nx" 50const NX_MAGIC_2654435761: i64 = 2654435761 51 52// ===== Q14 ========================================================== 53const NX_GB_Q: nx_int = 16384 54 55// Park-Miller LCG (for CRENULATE). 56const NX_GB_LCG_A: nx_int = 48271 57const NX_GB_LCG_M: nx_int = 2147483647 58 59// ===== Curve sealed enum ============================================ 60const NX_GB_CURVE_LINEAR: nx_int = 0 61const NX_GB_CURVE_SMOOTHSTEP: nx_int = 1 62const NX_GB_CURVE_EASE_IN_OUT: nx_int = 2 63const NX_GB_CURVE_SHARP: nx_int = 3 64const NX_GB_CURVE_CRENULATE: nx_int = 4 65 66const NX_GB_CURVE_COUNT: nx_int = 5 67 68// ===== Validity predicate =========================================== 69func nx_gb_curve_is_valid(c: nx_int) -> nx_int { 70 if c == NX_GB_CURVE_LINEAR { return 1 } 71 if c == NX_GB_CURVE_SMOOTHSTEP { return 1 } 72 if c == NX_GB_CURVE_EASE_IN_OUT { return 1 } 73 if c == NX_GB_CURVE_SHARP { return 1 } 74 if c == NX_GB_CURVE_CRENULATE { return 1 } 75 return 0 76} 77 78// ===== Curve transforms ============================================== 79// Each function takes Q14 progress in [0, Q] and returns Q14 weight 80// in [0, Q]. The blend then = a + (b - a) * weight / Q. 81 82// LINEAR: identity. 83func _gb_curve_linear(t: nx_int) -> nx_int { return t } 84 85// SMOOTHSTEP: classic 3t^2 - 2t^3. In Q14: 86// t_q = t in [0, Q] 87// t_norm_sq = t_q * t_q / Q (Q14) 88// t_norm_cube = t_norm_sq * t_q / Q 89// result = 3 * t_norm_sq - 2 * t_norm_cube 90func _gb_curve_smoothstep(t: nx_int) -> nx_int { 91 let q: nx_int = NX_GB_Q 92 if t <= 0 { return 0 } 93 if t >= q { return q } 94 let t_sq: nx_int = t * t / q 95 let t_cube: nx_int = t_sq * t / q 96 return 3 * t_sq - 2 * t_cube 97} 98 99// EASE_IN_OUT: stronger s-curve via 6t^5 - 15t^4 + 10t^3 (Perlin 100// improved fade). 101func _gb_curve_ease_in_out(t: nx_int) -> nx_int { 102 let q: nx_int = NX_GB_Q 103 if t <= 0 { return 0 } 104 if t >= q { return q } 105 let t2: nx_int = t * t / q 106 let t3: nx_int = t2 * t / q 107 let t4: nx_int = t3 * t / q 108 let t5: nx_int = t4 * t / q 109 return 6 * t5 - 15 * t4 + 10 * t3 110} 111 112// SHARP: thin transition band; almost-step. Implemented as a clamped 113// remap centred on t=0.5, width 0.1 (10% of full range). 114func _gb_curve_sharp(t: nx_int) -> nx_int { 115 let q: nx_int = NX_GB_Q 116 // Map t in [0.45Q, 0.55Q] to [0, Q]; below = 0; above = Q. 117 let lo: nx_int = q * 45 / 100 118 let hi: nx_int = q * 55 / 100 119 if t <= lo { return 0 } 120 if t >= hi { return q } 121 return (t - lo) * q / (hi - lo) 122} 123 124// CRENULATE: hash-perturbed transition for "fingery" edges. Caller 125// must supply a SECOND parameter (perpendicular_q14) which is used 126// to hash-jitter the effective progress so the transition isn't a 127// straight line. V1 simplification: uses a sin-like wobble of 128// progress proportional to the perpendicular coordinate. 129// 130// This curve has a SEPARATE entry below (nx_gradient_crenulate_q14) 131// because the standard nx_gradient_blend_q14 doesn't carry the 132// perpendicular param. 133func _gb_curve_crenulate_fallback(t: nx_int) -> nx_int { 134 // Without perpendicular context, fall back to SMOOTHSTEP. 135 return _gb_curve_smoothstep(t) 136} 137 138// ===== Public: standard blend ====================================== 139// Input: a, b (any Q14 value), progress (Q14 [0, Q]), curve enum. 140// Output: a + (b - a) * curve(progress) / Q. Clamped if progress out 141// of band. 142func nx_gradient_blend_q14( 143 a: nx_int, 144 b: nx_int, 145 progress_q14: nx_int, 146 curve: nx_int 147) -> nx_int { 148 if nx_gb_curve_is_valid(curve) == 0 { return a } 149 // Clamp progress. 150 var t: nx_int = progress_q14 151 if t < 0 { t = 0 } 152 if t > NX_GB_Q { t = NX_GB_Q } 153 154 var weight: nx_int = t 155 if curve == NX_GB_CURVE_LINEAR { weight = _gb_curve_linear(t) } 156 if curve == NX_GB_CURVE_SMOOTHSTEP { weight = _gb_curve_smoothstep(t) } 157 if curve == NX_GB_CURVE_EASE_IN_OUT { weight = _gb_curve_ease_in_out(t) } 158 if curve == NX_GB_CURVE_SHARP { weight = _gb_curve_sharp(t) } 159 if curve == NX_GB_CURVE_CRENULATE { weight = _gb_curve_crenulate_fallback(t) } 160 161 return a + (b - a) * weight / NX_GB_Q 162} 163 164// ===== Crenulate blend with perpendicular jitter =================== 165// Hash-perturbs the progress by a fraction of perpendicular_q14 so 166// the transition between A and B forms organic "fingers" rather than 167// a clean line. Useful for forest/grassland edges. 168func nx_gradient_blend_crenulate_q14( 169 a: nx_int, 170 b: nx_int, 171 progress_q14: nx_int, 172 perpendicular_q14: nx_int, 173 finger_amplitude_q14: nx_int, 174 seed: nx_int 175) -> nx_int { 176 // Hash the perpendicular coordinate -> signed jitter in [-Q, Q]. 177 var h: nx_int = seed 178 h = (h * NX_GB_LCG_A + perpendicular_q14 * NX_MAGIC_2654435761) % NX_GB_LCG_M 179 if h < 0 { h = h + NX_GB_LCG_M } 180 let jitter: nx_int = (h % (2 * NX_GB_Q)) - NX_GB_Q 181 182 // Modulate progress by finger amplitude. 183 let progress_adj: nx_int = progress_q14 + jitter * finger_amplitude_q14 / NX_GB_Q 184 185 return nx_gradient_blend_q14(a, b, progress_adj, NX_GB_CURVE_SMOOTHSTEP) 186} 187 188// ===== Self-test ==================================================== 189func main() -> i64 { 190 let q: nx_int = NX_GB_Q 191 192 // T1: Validity predicate. 193 if nx_gb_curve_is_valid(NX_GB_CURVE_LINEAR) != 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 194 if nx_gb_curve_is_valid(NX_GB_CURVE_SMOOTHSTEP) != 1 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 195 if nx_gb_curve_is_valid(NX_GB_CURVE_EASE_IN_OUT) != 1 { return __syscall(93, 3, 0, 0, 0, 0, 0) } 196 if nx_gb_curve_is_valid(NX_GB_CURVE_SHARP) != 1 { return __syscall(93, 4, 0, 0, 0, 0, 0) } 197 if nx_gb_curve_is_valid(NX_GB_CURVE_CRENULATE) != 1 { return __syscall(93, 5, 0, 0, 0, 0, 0) } 198 if nx_gb_curve_is_valid(99) != 0 { return __syscall(93, 6, 0, 0, 0, 0, 0) } 199 200 // T2: LINEAR -- exact identity weight. 201 if nx_gradient_blend_q14(100, 200, 0, NX_GB_CURVE_LINEAR) != 100 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 202 if nx_gradient_blend_q14(100, 200, q, NX_GB_CURVE_LINEAR) != 200 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 203 if nx_gradient_blend_q14(100, 200, q / 2, NX_GB_CURVE_LINEAR) != 150 { return __syscall(93, 12, 0, 0, 0, 0, 0) } 204 205 // T3: SMOOTHSTEP -- weight at t=0.5 is 0.5 (3*0.25 - 2*0.125 = 0.5). 206 let s_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SMOOTHSTEP) 207 // Expected 0.5 * Q = 8192; allow +/- 5 for integer rounding. 208 if s_half < q / 2 - 5 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 209 if s_half > q / 2 + 5 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 210 // SMOOTHSTEP at t=0.25: 3*0.0625 - 2*0.015625 = 0.15625. Less 211 // than linear's 0.25. 212 let s_quarter: nx_int = nx_gradient_blend_q14(0, q, q / 4, NX_GB_CURVE_SMOOTHSTEP) 213 let l_quarter: nx_int = nx_gradient_blend_q14(0, q, q / 4, NX_GB_CURVE_LINEAR) 214 if s_quarter >= l_quarter { return __syscall(93, 22, 0, 0, 0, 0, 0) } 215 // Endpoints exact. 216 if nx_gradient_blend_q14(0, q, 0, NX_GB_CURVE_SMOOTHSTEP) != 0 { return __syscall(93, 23, 0, 0, 0, 0, 0) } 217 if nx_gradient_blend_q14(0, q, q, NX_GB_CURVE_SMOOTHSTEP) != q { return __syscall(93, 24, 0, 0, 0, 0, 0) } 218 219 // T4: EASE_IN_OUT -- at t=0.5, weight = 6*0.03125 - 15*0.0625 + 220 // 10*0.125 = 0.1875 - 0.9375 + 1.25 = 0.5. 221 let e_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_EASE_IN_OUT) 222 if e_half < q / 2 - 5 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 223 if e_half > q / 2 + 5 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 224 // Steeper than SMOOTHSTEP near the middle -- at t=0.6, EASE_IN_OUT 225 // is closer to 1 than SMOOTHSTEP is. 226 let e_06: nx_int = nx_gradient_blend_q14(0, q, q * 6 / 10, NX_GB_CURVE_EASE_IN_OUT) 227 let s_06: nx_int = nx_gradient_blend_q14(0, q, q * 6 / 10, NX_GB_CURVE_SMOOTHSTEP) 228 if e_06 <= s_06 { return __syscall(93, 32, 0, 0, 0, 0, 0) } 229 230 // T5: SHARP -- near-step at t=0.5. 231 // Below 0.45: 0. Above 0.55: Q. At 0.5: 0.5*Q. 232 if nx_gradient_blend_q14(0, q, q * 40 / 100, NX_GB_CURVE_SHARP) != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) } 233 if nx_gradient_blend_q14(0, q, q * 60 / 100, NX_GB_CURVE_SHARP) != q { return __syscall(93, 41, 0, 0, 0, 0, 0) } 234 let sh_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SHARP) 235 if sh_half < q / 2 - 100 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 236 if sh_half > q / 2 + 100 { return __syscall(93, 43, 0, 0, 0, 0, 0) } 237 238 // T6: Clamping out-of-band progress. 239 if nx_gradient_blend_q14(100, 200, 0 - 5 * q, NX_GB_CURVE_LINEAR) != 100 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 240 if nx_gradient_blend_q14(100, 200, 99 * q, NX_GB_CURVE_LINEAR) != 200 { return __syscall(93, 51, 0, 0, 0, 0, 0) } 241 242 // T7: Invalid curve -> returns a (refusal). 243 if nx_gradient_blend_q14(777, 888, q / 2, 99) != 777 { return __syscall(93, 60, 0, 0, 0, 0, 0) } 244 245 // T8: Crenulate -- deterministic (same seed + perp + progress 246 // -> same value). 247 let c_a: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, q / 4, 42) 248 let c_b: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, q / 4, 42) 249 if c_a != c_b { return __syscall(93, 70, 0, 0, 0, 0, 0) } 250 // Different perpendicular -> different blend (the finger effect). 251 let c_c: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 200, q / 4, 42) 252 if c_a == c_c { return __syscall(93, 71, 0, 0, 0, 0, 0) } 253 // Zero finger amplitude -> same as smoothstep at progress. 254 let c_zero: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, 0, 42) 255 let s_for_comp: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SMOOTHSTEP) 256 if c_zero != s_for_comp { return __syscall(93, 72, 0, 0, 0, 0, 0) } 257 258 // T9: Composition example -- blending two biome heights. 259 // forest_height = 200; desert_height = 50. Transition smoothly 260 // via SMOOTHSTEP across a region. At t=0.5, the blend should be 261 // 125 (50 + 75 = 125). 262 let blend_125: nx_int = nx_gradient_blend_q14(50, 200, q / 2, NX_GB_CURVE_SMOOTHSTEP) 263 if blend_125 < 120 { return __syscall(93, 80, 0, 0, 0, 0, 0) } 264 if blend_125 > 130 { return __syscall(93, 81, 0, 0, 0, 0, 0) } 265 266 return 0 267}