code wiki / (root) / nx_silhouette_quality.nx

nx_silhouette_quality.nx source

↩ module page · 382 lines · 14378 B

1// nx_silhouette_quality.nx -- horizon/skyline quality grader. 2// 3// Per the honest audit 2026-05-16: heightmap statistical graders 4// (nx_world_quality_grader) can return S-class for terrain that 5// nonetheless produces a FLAT-LOOKING HORIZON. This primitive 6// grades the SILHOUETTE -- what you see when you look toward the 7// horizon -- on 5 axes designed to catch "high-stats, low-vista" 8// worlds. 9// 10// Method: from a camera viewpoint, sample N azimuth rays (default 11// 64) around the horizon. For each ray, march outward across the 12// heightmap and find the maximum ANGULAR ELEVATION (apparent height 13// above the camera, projected to a small-angle proxy: 14// elev_proxy = (max_h - cam_y) / dist 15// scaled to Q14). This produces a silhouette of N samples. 16// 17// AXES (Q14 [0, Q]; higher = better skyline): 18// 19// 0. ELEVATION_RANGE: max - min angular elevation across silhouette. 20// Worlds with all peaks at the same apparent height = 0. 21// 1. PEAK_COUNT: local-maxima count; target 3-8 distinct peaks per 22// half-horizon. Catches uniform-tooth-saw silhouettes. 23// 2. SMOOTHNESS: penalty for too-jagged AND too-smooth. Mean abs 24// second-difference compared to target band. 25// 3. ASPECT_DIVERSITY: stddev of peak-to-peak widths. Catches 26// repetitive tooth patterns. 27// 4. SKY_BALANCE: fraction of view that is sky (above horizon proxy 28// threshold); target 0.3 - 0.7. Catches "all mountains" and "all 29// flatland". 30// 31// EMITS LAYER_VERDICT (kind = NX_LAYER_KIND_READABILITY, 32// REFINE_HINT = REFINE_MORE_FEATURES). 33// 34// genealogy_id: appleton_1996_landscape_aesthetics + 35// horizon_silhouette_canon 36// lineage_id: nx_silhouette_quality_5axis_v1 37 38// nx_safety_envelope: 39// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 40// sil_target: SIL1 41// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 42// verdict: NOT_YET_EVALUATED 43 44import "nx_syscalls.nx" 45import "nx_tier.nx" 46import "nx_camera_q14.nx" 47import "nx_layer_verdict.nx" 48const NX_MAGIC_36000: i64 = 36000 49 50const NX_SQ_Q: nx_int = 16384 51 52// ===== Axis indices in LAYER_VERDICT =============================== 53const NX_SQ_AXIS_RANGE: nx_int = 0 54const NX_SQ_AXIS_PEAK_COUNT: nx_int = 1 55const NX_SQ_AXIS_SMOOTHNESS: nx_int = 2 56const NX_SQ_AXIS_ASPECT_DIV: nx_int = 3 57const NX_SQ_AXIS_SKY_BALANCE: nx_int = 4 58 59const NX_SQ_AXIS_COUNT: nx_int = 5 60 61// ===== Internal: silhouette computation ============================ 62// For each of n_rays azimuths, march along the heightmap up to 63// max_dist (in heightmap cells). Record the max angular elevation 64// proxy = (h - cam_y) * 100 / dist (scaled to fit in Q14 range when 65// dist >> 0). 66// 67// Outputs: silhouette[i] in Q14. Stored in caller-provided buffer. 68// 69// Camera position is (cam_x, cam_y, cam_z) in heightmap cell coords 70// (NOT world Q14). Heightmap is row-major w x h. 71func _sq_build_silhouette( 72 heightmap: *i64, w: nx_int, h: nx_int, 73 cam_x: nx_int, cam_y_q14_m: nx_int, cam_z: nx_int, 74 max_dist: nx_int, 75 silhouette: *i64, n_rays: nx_int 76) { 77 if n_rays <= 0 { return } 78 let q: nx_int = NX_SQ_Q 79 var i: nx_int = 0 80 while i < n_rays { 81 let azimuth_cd: nx_int = (i * NX_MAGIC_36000) / n_rays 82 let cos_a: nx_int = nx_camera_cos_q14_centideg(azimuth_cd) 83 let sin_a: nx_int = nx_camera_sin_q14_centideg(azimuth_cd) 84 var step: nx_int = 1 85 var max_elev: nx_int = 0 - q // very negative initial 86 while step < max_dist { 87 let sx: nx_int = cam_x + (step * cos_a) / q 88 let sz: nx_int = cam_z + (step * sin_a) / q 89 if sx >= 0 { 90 if sx < w { 91 if sz >= 0 { 92 if sz < h { 93 let h_at: nx_int = heightmap[sz * w + sx] 94 // elev_proxy = (h - cam_y) * 100 / dist 95 // h_at already in metres; cam_y_q14_m is Q14 metres. 96 // Convert h_at to Q14 metres (assume input is plain metres): 97 let h_q14_m: nx_int = h_at * q 98 let diff: nx_int = h_q14_m - cam_y_q14_m 99 let elev: nx_int = (diff * 100) / step 100 if elev > max_elev { max_elev = elev } 101 } 102 } 103 } 104 } 105 step = step + 1 106 } 107 silhouette[i] = max_elev 108 i = i + 1 109 } 110} 111 112// ===== Internal: range across silhouette =========================== 113func _sq_range_q14( 114 silhouette: *i64, n_rays: nx_int, max_relief: nx_int 115) -> nx_int { 116 if n_rays <= 0 { return 0 } 117 if max_relief <= 0 { return 0 } 118 var smin: nx_int = silhouette[0] 119 var smax: nx_int = silhouette[0] 120 var i: nx_int = 0 121 while i < n_rays { 122 let v: nx_int = silhouette[i] 123 if v < smin { smin = v } 124 if v > smax { smax = v } 125 i = i + 1 126 } 127 let q: nx_int = NX_SQ_Q 128 let range: nx_int = smax - smin 129 // Target: range >= max_relief * 50 (a 50-m peak at 1-cell distance 130 // gives elev proxy 5000). Normalised against max_relief * 20. 131 var score: nx_int = (range * q) / (max_relief * 20) 132 if score > q { score = q } 133 if score < 0 { score = 0 } 134 return score 135} 136 137// ===== Internal: local-maxima count ================================ 138func _sq_peak_count_q14(silhouette: *i64, n_rays: nx_int) -> nx_int { 139 if n_rays < 3 { return 0 } 140 let q: nx_int = NX_SQ_Q 141 var n_peaks: nx_int = 0 142 var i: nx_int = 1 143 while i < n_rays - 1 { 144 let a: nx_int = silhouette[i - 1] 145 let b: nx_int = silhouette[i] 146 let c: nx_int = silhouette[i + 1] 147 if b > a { if b > c { n_peaks = n_peaks + 1 } } 148 i = i + 1 149 } 150 // Target: 3-8 peaks per half-horizon = 6-16 total. 151 var score: nx_int = 0 152 if n_peaks >= 6 { 153 if n_peaks <= 16 { score = q } 154 if n_peaks > 16 { score = q - (n_peaks - 16) * q / 16 } 155 } 156 if n_peaks < 6 { score = (n_peaks * q) / 6 } 157 if score < 0 { score = 0 } 158 if score > q { score = q } 159 return score 160} 161 162// ===== Internal: smoothness (penalty if jagged or smooth) ========== 163func _sq_smoothness_q14(silhouette: *i64, n_rays: nx_int) -> nx_int { 164 if n_rays < 3 { return 0 } 165 let q: nx_int = NX_SQ_Q 166 var sum_2nd_diff: nx_int = 0 167 var count: nx_int = 0 168 var i: nx_int = 1 169 while i < n_rays - 1 { 170 let d2: nx_int = silhouette[i + 1] - 2 * silhouette[i] + silhouette[i - 1] 171 var ad: nx_int = d2 172 if ad < 0 { ad = 0 - ad } 173 sum_2nd_diff = sum_2nd_diff + ad 174 count = count + 1 175 i = i + 1 176 } 177 if count == 0 { return 0 } 178 let mean_2nd: nx_int = sum_2nd_diff / count 179 // Target: mean_2nd ~= 500 (Q14 elev-proxy units). Higher = jagged; 180 // lower = too smooth. Bell curve around 500. 181 let target: nx_int = 500 182 var dev: nx_int = mean_2nd - target 183 if dev < 0 { dev = 0 - dev } 184 let penalty: nx_int = (dev * q) / 1000 185 var score: nx_int = q - penalty 186 if score < 0 { score = 0 } 187 return score 188} 189 190// ===== Internal: peak-to-peak width diversity ===================== 191func _sq_aspect_div_q14(silhouette: *i64, n_rays: nx_int) -> nx_int { 192 if n_rays < 6 { return 0 } 193 let q: nx_int = NX_SQ_Q 194 // Find peak indices. 195 let peak_idx: *i64 = (sys_mmap(n_rays * NX_SIZEOF_NX_INT)) as *i64 196 var n_peaks: nx_int = 0 197 var i: nx_int = 1 198 while i < n_rays - 1 { 199 let a: nx_int = silhouette[i - 1] 200 let b: nx_int = silhouette[i] 201 let c: nx_int = silhouette[i + 1] 202 if b > a { 203 if b > c { 204 peak_idx[n_peaks] = i 205 n_peaks = n_peaks + 1 206 } 207 } 208 i = i + 1 209 } 210 if n_peaks < 3 { return q / 4 } // too few peaks for variance 211 // Compute peak-to-peak widths. 212 var sum_w: nx_int = 0 213 var widths: *i64 = (sys_mmap(n_peaks * NX_SIZEOF_NX_INT)) as *i64 214 var j: nx_int = 0 215 while j + 1 < n_peaks { 216 let dw: nx_int = peak_idx[j + 1] - peak_idx[j] 217 widths[j] = dw 218 sum_w = sum_w + dw 219 j = j + 1 220 } 221 let n_w: nx_int = n_peaks - 1 222 if n_w <= 0 { return 0 } 223 let mean_w: nx_int = sum_w / n_w 224 if mean_w <= 0 { return 0 } 225 var abs_dev_sum: nx_int = 0 226 var k: nx_int = 0 227 while k < n_w { 228 var d: nx_int = widths[k] - mean_w 229 if d < 0 { d = 0 - d } 230 abs_dev_sum = abs_dev_sum + d 231 k = k + 1 232 } 233 let mean_abs_dev: nx_int = abs_dev_sum / n_w 234 // Target: mean_abs_dev / mean_w >= 0.3. Higher = more diverse. 235 var ratio: nx_int = (mean_abs_dev * q) / mean_w 236 if ratio > q { ratio = q } 237 // Map [0, 0.3Q] -> [0, q], saturate above. 238 var score: nx_int = (ratio * q) / (q * 3 / 10) 239 if score > q { score = q } 240 if score < 0 { score = 0 } 241 return score 242} 243 244// ===== Internal: sky balance ======================================= 245// Sky fraction = count of samples below mean / total. Target 0.3-0.7. 246func _sq_sky_balance_q14(silhouette: *i64, n_rays: nx_int) -> nx_int { 247 if n_rays <= 0 { return 0 } 248 let q: nx_int = NX_SQ_Q 249 var sum: nx_int = 0 250 var i: nx_int = 0 251 while i < n_rays { 252 sum = sum + silhouette[i] 253 i = i + 1 254 } 255 let mean: nx_int = sum / n_rays 256 var n_below: nx_int = 0 257 var j: nx_int = 0 258 while j < n_rays { 259 if silhouette[j] < mean { n_below = n_below + 1 } 260 j = j + 1 261 } 262 let sky_frac_q: nx_int = (n_below * q) / n_rays 263 // Bell-curve target 0.3-0.7. Score = 1 - 2 * |sky_frac - 0.5|. 264 let q_half: nx_int = q / 2 265 var dev: nx_int = sky_frac_q - q_half 266 if dev < 0 { dev = 0 - dev } 267 var score: nx_int = q - 2 * dev 268 if score < 0 { score = 0 } 269 return score 270} 271 272// ===== Public: silhouette grader =================================== 273// Required inputs: heightmap, w, h, max_relief, viewpoint, max_dist. 274// silhouette scratch buffer needs n_rays i64s. 275// out_verdict: 16-i64 LAYER_VERDICT. 276func nx_silhouette_quality_grade( 277 heightmap: *i64, w: nx_int, h: nx_int, max_relief: nx_int, 278 cam_x: nx_int, cam_y_q14_m: nx_int, cam_z: nx_int, 279 max_dist: nx_int, 280 silhouette_scratch: *i64, n_rays: nx_int, 281 out_verdict: *i64 282) { 283 _sq_build_silhouette(heightmap, w, h, cam_x, cam_y_q14_m, cam_z, 284 max_dist, silhouette_scratch, n_rays) 285 286 let range_score: nx_int = _sq_range_q14(silhouette_scratch, n_rays, max_relief) 287 let peak_score: nx_int = _sq_peak_count_q14(silhouette_scratch, n_rays) 288 let smoothness: nx_int = _sq_smoothness_q14(silhouette_scratch, n_rays) 289 let aspect_div: nx_int = _sq_aspect_div_q14(silhouette_scratch, n_rays) 290 let sky_balance: nx_int = _sq_sky_balance_q14(silhouette_scratch, n_rays) 291 292 nx_layer_verdict_init(out_verdict, NX_LAYER_KIND_READABILITY, 293 NX_SQ_AXIS_COUNT, NX_LAYER_REFINE_MORE_FEATURES) 294 out_verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_RANGE] = range_score 295 out_verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_PEAK_COUNT] = peak_score 296 out_verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_SMOOTHNESS] = smoothness 297 out_verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_ASPECT_DIV] = aspect_div 298 out_verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_SKY_BALANCE] = sky_balance 299 nx_layer_verdict_finalize(out_verdict) 300} 301 302// ===== Self-test ==================================================== 303func main() -> i64 { 304 let q: nx_int = NX_SQ_Q 305 let verdict: *i64 = (sys_mmap(NX_LV_STRIDE * NX_SIZEOF_NX_INT)) as *i64 306 307 // Build a 32x32 mountain heightmap with multiple peaks for the 308 // silhouette to see. 309 let w: nx_int = 32 310 let h: nx_int = 32 311 let n: nx_int = w * h 312 let map: *i64 = (sys_mmap(n * NX_SIZEOF_NX_INT)) as *i64 313 var i: nx_int = 0 314 while i < n { 315 let x: nx_int = i % w 316 let y: nx_int = i / w 317 // Three peak centres: (8,16), (16, 24), (24, 8). 318 var v: nx_int = 0 319 let dx1: nx_int = x - 8 320 let dy1: nx_int = y - 16 321 let d1_sq: nx_int = dx1 * dx1 + dy1 * dy1 322 if d1_sq < 36 { v = v + (36 - d1_sq) * 20 } 323 let dx2: nx_int = x - 16 324 let dy2: nx_int = y - 24 325 let d2_sq: nx_int = dx2 * dx2 + dy2 * dy2 326 if d2_sq < 36 { v = v + (36 - d2_sq) * 15 } 327 let dx3: nx_int = x - 24 328 let dy3: nx_int = y - 8 329 let d3_sq: nx_int = dx3 * dx3 + dy3 * dy3 330 if d3_sq < 36 { v = v + (36 - d3_sq) * 25 } 331 map[i] = v 332 i = i + 1 333 } 334 335 let sil: *i64 = (sys_mmap(64 * NX_SIZEOF_NX_INT)) as *i64 336 337 // T1: Camera at centre (16, 0, 16) looking outward. Silhouette 338 // should have meaningful range from the 3 peak centres. 339 nx_silhouette_quality_grade(map, w, h, 1000, 340 16, 0, 16, 20, 341 sil, 32, verdict) 342 if verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_RANGE] <= 0 { 343 return __syscall(93, 1, 0, 0, 0, 0, 0) 344 } 345 // Verdict should have a defined grade. 346 if nx_lv_grade_is_valid(verdict[NX_LV_OFF_GRADE]) != 1 { 347 return __syscall(93, 2, 0, 0, 0, 0, 0) 348 } 349 350 // T2: Flat heightmap = silhouette = zero range. 351 let map_flat: *i64 = (sys_mmap(n * NX_SIZEOF_NX_INT)) as *i64 352 var fi: nx_int = 0 353 while fi < n { map_flat[fi] = 100; fi = fi + 1 } 354 nx_silhouette_quality_grade(map_flat, w, h, 1000, 355 16, 0, 16, 20, 356 sil, 32, verdict) 357 if verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_RANGE] != 0 { 358 return __syscall(93, 10, 0, 0, 0, 0, 0) 359 } 360 // Flat -> peak count = 0 -> score 0. 361 if verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_PEAK_COUNT] != 0 { 362 return __syscall(93, 11, 0, 0, 0, 0, 0) 363 } 364 // Grade = F (all axes LOSS). 365 if verdict[NX_LV_OFF_GRADE] > NX_LV_GRADE_D { 366 return __syscall(93, 12, 0, 0, 0, 0, 0) 367 } 368 369 // T3: Mountain heightmap should produce non-zero peak count and 370 // range; grade at least D. 371 nx_silhouette_quality_grade(map, w, h, 1000, 372 16, 0, 16, 20, 373 sil, 32, verdict) 374 if verdict[NX_LV_OFF_AXIS_0 + NX_SQ_AXIS_RANGE] <= 0 { 375 return __syscall(93, 20, 0, 0, 0, 0, 0) 376 } 377 if verdict[NX_LV_OFF_GRADE] < NX_LV_GRADE_F { 378 return __syscall(93, 21, 0, 0, 0, 0, 0) 379 } 380 381 return 0 382}