code wiki / (root) / nx_lighting_quality.nx

nx_lighting_quality.nx source

↩ module page · 223 lines · 9118 B

1// nx_lighting_quality.nx -- lighting composition grader. 2// 3// Per honest audit 2026-05-16: lighting quality was unmeasured. This 4// primitive grades a scene's lighting setup on 5 axes adapted from 5// cinematography conventions (three-point lighting + time-of-day 6// appropriateness). 7// 8// AXES (Q14 [0, Q]; higher = better): 9// 10// 0. KEY_FILL_RATIO: key/fill light ratio. Target [2x, 4x] (the 11// classic "1.5-stop key" cinematography rule); outside drops linearly. 12// 1. RIM_PRESENCE: rim_intensity / key_intensity; target [0.3, 0.8]. 13// Rim light separates subject from background. 14// 2. SHADOW_DENSITY: 1 - (min_visible / max_visible) brightness range. 15// Target [0.5, 0.85] (high contrast but not crushed blacks). 16// 3. TIME_OF_DAY_APPROPRIATE: sun_zenith_q14 within reasonable bounds 17// for the lighting setup. Penalty for "noon sun with deep shadows" 18// contradictions. 19// 4. COLOR_TEMPERATURE: warm/cool key vs fill complement (Q14 20// representation of K_key / K_fill ratio; target [0.9, 1.6] for 21// natural daylight or [0.6, 1.0] for sunset). 22// 23// EMITS LAYER_VERDICT (kind = NX_LAYER_KIND_COLOR -- closest existing 24// kind; lighting/color belong together). 25// 26// genealogy_id: lowell_2000_cinematography + nathan_1999_film_lighting + 27// three_point_lighting_canon 28// lineage_id: nx_lighting_quality_5axis_v1 29 30// nx_safety_envelope: 31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 32// sil_target: SIL1 33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 34// verdict: NOT_YET_EVALUATED 35 36import "nx_syscalls.nx" 37import "nx_tier.nx" 38import "nx_layer_verdict.nx" 39const NX_MAGIC_5500: i64 = 5500 40const NX_MAGIC_5000: i64 = 5000 41 42const NX_LQ_Q: nx_int = 16384 43 44const NX_LQ_AXIS_KEY_FILL: nx_int = 0 45const NX_LQ_AXIS_RIM_PRESENCE: nx_int = 1 46const NX_LQ_AXIS_SHADOW_DENSITY: nx_int = 2 47const NX_LQ_AXIS_TIME_OF_DAY: nx_int = 3 48const NX_LQ_AXIS_COLOR_TEMP: nx_int = 4 49const NX_LQ_AXIS_COUNT: nx_int = 5 50 51func _lq_band_score(val: nx_int, lo_q: nx_int, hi_q: nx_int) -> nx_int { 52 let q: nx_int = NX_LQ_Q 53 if val < 0 { return 0 } 54 if val < lo_q { 55 if lo_q > 0 { return (val * q) / lo_q } 56 return 0 57 } 58 if val <= hi_q { return q } 59 let span: nx_int = hi_q - lo_q 60 if span <= 0 { return 0 } 61 let excess: nx_int = val - hi_q 62 var s: nx_int = q - (excess * q) / span 63 if s < 0 { s = 0 } 64 return s 65} 66 67// ===== Lighting record (8 i64) ===================================== 68// Caller fills with intensities + temperatures from their renderer. 69// All Q14 unless noted. 70// rec[0] = key_intensity 71// rec[1] = fill_intensity 72// rec[2] = rim_intensity 73// rec[3] = ambient_intensity (used in shadow_density) 74// rec[4] = sun_zenith_q14 (0 = directly overhead, Q = horizon) 75// rec[5] = key_color_temp_k Q14 Kelvin / 1000 (e.g. 5500 K -> 5500*Q/1000) 76// rec[6] = fill_color_temp_k 77// rec[7] = reserved 78const NX_LIGHTING_REC_STRIDE: nx_int = 8 79const NX_LIGHTING_OFF_KEY: nx_int = 0 80const NX_LIGHTING_OFF_FILL: nx_int = 1 81const NX_LIGHTING_OFF_RIM: nx_int = 2 82const NX_LIGHTING_OFF_AMBIENT: nx_int = 3 83const NX_LIGHTING_OFF_SUN_ZENITH: nx_int = 4 84const NX_LIGHTING_OFF_KEY_TEMP: nx_int = 5 85const NX_LIGHTING_OFF_FILL_TEMP: nx_int = 6 86 87// ===== Axes ======================================================== 88func _lq_key_fill_q14(rec: *i64) -> nx_int { 89 let q: nx_int = NX_LQ_Q 90 let k: nx_int = rec[NX_LIGHTING_OFF_KEY] 91 let f: nx_int = rec[NX_LIGHTING_OFF_FILL] 92 if f <= 0 { return 0 } 93 let ratio_q: nx_int = (k * q) / f 94 return _lq_band_score(ratio_q, q * 2, q * 4) 95} 96 97func _lq_rim_q14(rec: *i64) -> nx_int { 98 let q: nx_int = NX_LQ_Q 99 let k: nx_int = rec[NX_LIGHTING_OFF_KEY] 100 let r: nx_int = rec[NX_LIGHTING_OFF_RIM] 101 if k <= 0 { return 0 } 102 let ratio_q: nx_int = (r * q) / k 103 return _lq_band_score(ratio_q, q * 3 / 10, q * 8 / 10) 104} 105 106func _lq_shadow_density_q14(rec: *i64) -> nx_int { 107 let q: nx_int = NX_LQ_Q 108 let k: nx_int = rec[NX_LIGHTING_OFF_KEY] 109 let a: nx_int = rec[NX_LIGHTING_OFF_AMBIENT] 110 if k <= 0 { return 0 } 111 // Shadow density = 1 - ambient/key (the fraction of the scene NOT 112 // covered by ambient fill). 113 let ambient_frac: nx_int = (a * q) / k 114 var density: nx_int = q - ambient_frac 115 if density < 0 { density = 0 } 116 if density > q { density = q } 117 return _lq_band_score(density, q / 2, q * 85 / 100) 118} 119 120func _lq_time_of_day_q14(rec: *i64) -> nx_int { 121 let q: nx_int = NX_LQ_Q 122 let sun_z: nx_int = rec[NX_LIGHTING_OFF_SUN_ZENITH] 123 // Best lighting at golden hour (sun_z ~ 0.75Q) and worst at noon 124 // (sun_z = 0) with no shadows. Bell curve peak at 0.75Q with 125 // tolerance band [0.5Q, 0.95Q] for cinematic angles. 126 if sun_z < 0 { return 0 } 127 if sun_z > q { return 0 } 128 let target: nx_int = q * 75 / 100 129 var dev: nx_int = sun_z - target 130 if dev < 0 { dev = 0 - dev } 131 // Score = max(0, 1 - 2 * dev). At dev=0: Q; at dev=Q/2: 0. 132 var s: nx_int = q - dev * 2 133 if s < 0 { s = 0 } 134 return s 135} 136 137func _lq_color_temp_q14(rec: *i64) -> nx_int { 138 let q: nx_int = NX_LQ_Q 139 let k_temp: nx_int = rec[NX_LIGHTING_OFF_KEY_TEMP] 140 let f_temp: nx_int = rec[NX_LIGHTING_OFF_FILL_TEMP] 141 if f_temp <= 0 { return 0 } 142 let ratio_q: nx_int = (k_temp * q) / f_temp 143 // Two acceptable bands: natural daylight key/fill [0.9, 1.6] OR 144 // sunset key warmer than fill [0.6, 1.0]. Score = max(band1, band2). 145 let s1: nx_int = _lq_band_score(ratio_q, q * 9 / 10, q * 16 / 10) 146 let s2: nx_int = _lq_band_score(ratio_q, q * 6 / 10, q) 147 if s1 > s2 { return s1 } 148 return s2 149} 150 151// ===== Public: lighting grader ===================================== 152func nx_lighting_quality_grade(rec: *i64, out_verdict: *i64) { 153 let kf: nx_int = _lq_key_fill_q14(rec) 154 let rp: nx_int = _lq_rim_q14(rec) 155 let sd: nx_int = _lq_shadow_density_q14(rec) 156 let td: nx_int = _lq_time_of_day_q14(rec) 157 let ct: nx_int = _lq_color_temp_q14(rec) 158 nx_layer_verdict_init(out_verdict, NX_LAYER_KIND_COLOR, 159 NX_LQ_AXIS_COUNT, NX_LAYER_REFINE_IMPROVE_READ) 160 out_verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_KEY_FILL] = kf 161 out_verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_RIM_PRESENCE] = rp 162 out_verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_SHADOW_DENSITY] = sd 163 out_verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_TIME_OF_DAY] = td 164 out_verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_COLOR_TEMP] = ct 165 nx_layer_verdict_finalize(out_verdict) 166} 167 168// ===== Self-test ==================================================== 169func main() -> i64 { 170 let q: nx_int = NX_LQ_Q 171 let verdict: *i64 = (sys_mmap(NX_LV_STRIDE * NX_SIZEOF_NX_INT)) as *i64 172 let rec: *i64 = (sys_mmap(NX_LIGHTING_REC_STRIDE * NX_SIZEOF_NX_INT)) as *i64 173 174 // T1: Classic three-point lighting at golden hour, daylight temps. 175 rec[NX_LIGHTING_OFF_KEY] = 3 * q // key 176 rec[NX_LIGHTING_OFF_FILL] = q // fill (3:1 ratio) 177 rec[NX_LIGHTING_OFF_RIM] = q * 5 / 10 // rim 50% of key 178 rec[NX_LIGHTING_OFF_AMBIENT] = q * 3 / 10 // ambient 30% of key 179 rec[NX_LIGHTING_OFF_SUN_ZENITH] = q * 75 / 100 // golden hour 180 rec[NX_LIGHTING_OFF_KEY_TEMP] = q * NX_MAGIC_5500 / 1000 // 5500K daylight 181 rec[NX_LIGHTING_OFF_FILL_TEMP] = q * NX_MAGIC_5000 / 1000 // 5000K (ratio 1.1) 182 nx_lighting_quality_grade(rec, verdict) 183 if verdict[NX_LV_OFF_KIND] != NX_LAYER_KIND_COLOR { 184 return __syscall(93, 1, 0, 0, 0, 0, 0) 185 } 186 if verdict[NX_LV_OFF_GRADE] < NX_LV_GRADE_B { 187 return __syscall(93, 2, 0, 0, 0, 0, 0) 188 } 189 190 // T2: Flat lighting (key = fill = 1) -> key_fill axis = 0. 191 rec[NX_LIGHTING_OFF_KEY] = q 192 rec[NX_LIGHTING_OFF_FILL] = q 193 nx_lighting_quality_grade(rec, verdict) 194 let kf: nx_int = verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_KEY_FILL] 195 // ratio = Q, lo_q = 2Q, ramps linearly so kf = Q/2. 196 if kf * 10 >= q * 6 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 197 198 // T3: No rim light -> rim axis = 0. 199 rec[NX_LIGHTING_OFF_KEY] = 3 * q 200 rec[NX_LIGHTING_OFF_RIM] = 0 201 nx_lighting_quality_grade(rec, verdict) 202 if verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_RIM_PRESENCE] != 0 { 203 return __syscall(93, 20, 0, 0, 0, 0, 0) 204 } 205 206 // T4: Noon lighting (sun_zenith = 0) -> time_of_day axis low 207 // (golden hour target = 0.75Q). 208 rec[NX_LIGHTING_OFF_SUN_ZENITH] = 0 209 nx_lighting_quality_grade(rec, verdict) 210 let td: nx_int = verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_TIME_OF_DAY] 211 // dev from 0.75Q = 0.75Q; score = q - 2*0.75q = -0.5q -> 0. 212 if td != 0 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 213 214 // T5: Color temp matched -> daylight band Q. 215 rec[NX_LIGHTING_OFF_KEY_TEMP] = q * NX_MAGIC_5500 / 1000 216 rec[NX_LIGHTING_OFF_FILL_TEMP] = q * NX_MAGIC_5500 / 1000 // ratio 1.0 217 nx_lighting_quality_grade(rec, verdict) 218 if verdict[NX_LV_OFF_AXIS_0 + NX_LQ_AXIS_COLOR_TEMP] != q { 219 return __syscall(93, 40, 0, 0, 0, 0, 0) 220 } 221 222 return 0 223}