code wiki / (root) / nx_skin_ita.nx

nx_skin_ita.nx source

↩ module page · 249 lines · 11339 B

1// nx_skin_ita.nx -- SKIN ALBEDO DERIVED FROM THE GENOME, VIA A PUBLISHED STANDARD (charsim R0, 2// debt 1786549493 residue a). LIB, no main (ecosystem convention). 3// 4// WHAT IT REPLACES AND WHY. nx_nxa_texbake_lib baked skin from TWELVE free sRGB constants 5// (NTB_FACE_R=228, NTB_TORS_G=184, ...). Nothing derived them, nothing could falsify them, and every 6// bred creature got the identical skin -- a fixed ramp wearing the shape of a material model. Rule 25 7// says build intelligence, never fewer options; rule 11 says a threshold nobody can point to a source 8// for is a defect. This organ removes all twelve. 9// 10// THE STANDARD. Dermatology classifies constitutive skin colour by the INDIVIDUAL TYPOLOGY ANGLE 11// (Chardon et al. 1991), defined in CIE L*a*b* as ITA = atan((L* - 50) / b*) in degrees, with six 12// published categories separated at 55, 41, 28, 10 and -30 degrees. Those five boundaries are the ONLY 13// classification numbers in this file, and each is stored as its TANGENT so the whole organ runs in 14// integers with no trigonometry at all: comparing ITA against an angle is exactly comparing 15// (L* - 50) / b* against that angle's tangent. 16// 17// WHAT IS DERIVED vs WHAT IS CHOSEN -- stated plainly, because a reader who cannot tell will trust the 18// chosen ones as measured (the fabricated-constant class): 19// DERIVED from the standard: the five class boundaries; the lightness endpoints of the locus, which 20// fall out of the boundary tangents rather than being picked (see si_lstar_of_gene). 21// CHOSEN, and this is a MODEL not a measurement: that skin lies on a straight locus of fixed a* and 22// b* (literature-typical redness and yellowness), and the four per-region lightness offsets. They 23// are expressed as offsets in L*, a perceptually uniform axis, so "slightly lighter" means the same 24// thing everywhere -- which is precisely what twelve free sRGB numbers could not express. 25// The IMPRECISION accepted: fixed-point, with the fifth root taken at 1e3 so one output step is under 26// half an 8-bit code. Colour is exact to the byte after the final search. 27// 28// NO SECOND TRANSFER CURVE. si_srgb_of_lab finds the sRGB byte by searching the FORWARD IEC 61966-2-1 29// transfer, so this organ carries the generating function, not a rival copy of nx_eink_tone's baked 30// table. The gate proves they agree on that organ's own banked anchor (sRGB 128 -> linear 21586 -> 31// L* 53.59); consolidating both onto one shared lib is filed, not done here. 32// license_tier: ORIGINAL No hw writes (Rule 26). 33import "nx_syscalls.nx" 34 35// ---- fixed-point scales ---- 36const SI_E3: i64 = 1000 // milli-units: L*, a*, b* 37const SI_E4: i64 = 10000 // the f() domain in the L*a*b* transform 38const SI_E5: i64 = 100000 // linear light and XYZ, matching nx_eink_tone's 1e5 convention 39const SI_E7: i64 = 10000000 40const SI_E12: i64 = 1000000000000 41 42// ---- ITA class boundaries (Chardon 1991), stored as tan(angle) x 1000 ---- 43// tan(55)=1.42815 tan(41)=0.86929 tan(28)=0.53171 tan(10)=0.17633 tan(-30)=-0.57735 44const SI_TAN_55: i64 = 1428 45const SI_TAN_41: i64 = 869 46const SI_TAN_28: i64 = 532 47const SI_TAN_10: i64 = 176 48const SI_TAN_N30: i64 = 0 - 577 49// THE MODELLED SPAN. Both endpoints must lie OUTSIDE the extreme boundaries, or the outermost class 50// is unreachable and the locus silently spans five classes while looking correct -- the gate caught 51// exactly that: an endpoint sitting ON tan(55) never satisfies "> tan(55)", so VERY LIGHT never 52// occurred. tan(-55)=-1.42815 tan(62)=1.88073 53const SI_TAN_N55: i64 = 0 - 1428 54const SI_TAN_62: i64 = 1881 55// class ids, light to dark 56const SI_C_VERYLIGHT: i64 = 0 57const SI_C_LIGHT: i64 = 1 58const SI_C_INTERMEDIATE: i64 = 2 59const SI_C_TAN: i64 = 3 60const SI_C_BROWN: i64 = 4 61const SI_C_DARK: i64 = 5 62const SI_NCLASS: i64 = 6 63 64// ---- the locus: CHOSEN model parameters (literature-typical skin chromaticity) ---- 65const SI_A_STAR: i64 = 12000 // a* = 12.0, skin redness 66const SI_B_STAR: i64 = 13000 // b* = 13.0, skin yellowness (literature-typical band is ~10-22; 67 // this value is what puts the DERIVED L* endpoints, below, on the 68 // real human range 31..75 rather than running off into porcelain) 69const SI_L_PIVOT: i64 = 50000 // L* = 50, the pivot the ITA definition is written about 70 71// ---- per-region lightness offsets, in MILLI-L* (chosen model, perceptual axis) ---- 72const SI_DL_FACE: i64 = 1500 // face reads slightly lighter (perfusion, less exposure keratin) 73const SI_DL_TORSO: i64 = 0 // the reference region 74const SI_DL_LIMBS: i64 = 0 - 2000 // limbs read slightly darker 75const SI_DL_GENS: i64 = 0 - 1000 76 77// ---- genome trait span ---- 78const SI_GENE_MAX: i64 = 1000 // nx_breeding GN_T_HUE range is 0..1000 79 80// ---- model identity ---- 81// A baked skin is only interpretable if you know WHICH locus produced it. Anything that emits skin 82// stamps this, so a future reader can tell a v1 atlas from a later locus instead of guessing. 83// Bump on ANY change to the locus parameters or the class boundaries. 84const SI_MODEL_V: i64 = 1 85 86// ---- CIE L*a*b* transform constants (CIE 15) ---- 87const SI_F_DELTA: i64 = 2069 // 6/29 x 1e4 88const SI_F_OFF: i64 = 1379 // 4/29 x 1e4 89const SI_F_LINK: i64 = 1284 // 3*(6/29)^2 x 1e4 90const SI_L_ADD: i64 = 16000 // the +16 of L* = 116 f(Y) - 16, x 1e3 91const SI_L_DIV: i64 = 116 92const SI_A_DIV: i64 = 50 // a*/500 expressed in the 1e4 f-domain 93const SI_B_DIV: i64 = 20 // b*/200 expressed in the 1e4 f-domain 94// D65 white point x 1e5 95const SI_XN: i64 = 95047 96const SI_ZN: i64 = 108883 97// XYZ -> linear sRGB matrix (IEC 61966-2-1, D65) x 1e4 98const SI_M_RX: i64 = 32406 99const SI_M_RY: i64 = 0 - 15372 100const SI_M_RZ: i64 = 0 - 4986 101const SI_M_GX: i64 = 0 - 9689 102const SI_M_GY: i64 = 18758 103const SI_M_GZ: i64 = 415 104const SI_M_BX: i64 = 557 105const SI_M_BY: i64 = 0 - 2040 106const SI_M_BZ: i64 = 10570 107// sRGB transfer (IEC 61966-2-1) x 1e5 108const SI_T_KNEE: i64 = 4045 // 0.04045 109const SI_T_SLOPE: i64 = 1292 // 12.92 x 100 110const SI_T_A: i64 = 5500 // 0.055 x 1e5 111const SI_T_S: i64 = 105500 // 1.055 x 1e5 112const SI_BYTE_MAX: i64 = 255 113 114// ---- integer roots (bisection: cannot diverge, and the bound is the answer's own range) ---- 115// fifth root of n, both sides at 1e3: returns y with y^5 closest to n from below 116func si_root5_e3(x_e3: i64) -> i64 { 117 if x_e3 <= 0 { return 0 } 118 let target: i64 = x_e3 * SI_E12 119 var lo: i64 = 0 120 var hi: i64 = SI_E3 121 while lo < hi { 122 let mid: i64 = (lo + hi + 1) / 2 123 let p2: i64 = mid * mid 124 let p4: i64 = p2 * p2 125 var over: i64 = 0 126 if p4 > target / mid { over = 1 } 127 if over == 1 { hi = mid - 1 } else { lo = mid } 128 } 129 return lo 130} 131 132// cube root of t (1e5 in, 1e5 out) -- the f() of the forward L* transform 133func si_cbrt_e5(x_e5: i64) -> i64 { 134 if x_e5 <= 0 { return 0 } 135 var lo: i64 = 0 136 var hi: i64 = SI_E5 137 while lo < hi { 138 let mid: i64 = (lo + hi + 1) / 2 139 var over: i64 = 0 140 let m2: i64 = mid * mid / SI_E5 141 if m2 * mid / SI_E5 > x_e5 { over = 1 } 142 if over == 1 { hi = mid - 1 } else { lo = mid } 143 } 144 return lo 145} 146 147// ---- the sRGB transfer, FORWARD (the standard's own direction) ---- 148// sRGB byte -> linear light x 1e5 149func si_linear_of_byte(s: i64) -> i64 { 150 var u: i64 = s * SI_E5 / SI_BYTE_MAX 151 if u < 0 { u = 0 } 152 if u <= SI_T_KNEE { return u * 100 / SI_T_SLOPE } 153 let v: i64 = (u + SI_T_A) * SI_E5 / SI_T_S 154 let v2: i64 = v * v / SI_E5 155 let r5: i64 = si_root5_e3(v2 / 100) 156 return v2 * r5 / SI_E3 157} 158 159// linear light x 1e5 -> nearest sRGB byte, by searching the forward transfer (no inverse curve) 160func si_byte_of_linear(c_e5: i64) -> i64 { 161 var c: i64 = c_e5 162 if c < 0 { c = 0 } 163 if c > SI_E5 { c = SI_E5 } 164 var lo: i64 = 0 165 var hi: i64 = SI_BYTE_MAX 166 while lo < hi { 167 let mid: i64 = (lo + hi) / 2 168 if si_linear_of_byte(mid) < c { lo = mid + 1 } else { hi = mid } 169 } 170 if lo > 0 { 171 let dhi: i64 = si_linear_of_byte(lo) - c 172 let dlo: i64 = c - si_linear_of_byte(lo - 1) 173 if dlo < dhi { return lo - 1 } 174 } 175 return lo 176} 177 178// ---- L*a*b* -> sRGB ---- 179func si_finv_e5(t_e4: i64) -> i64 { 180 if t_e4 > SI_F_DELTA { return t_e4 * t_e4 / SI_E4 * t_e4 / SI_E3 } 181 return (t_e4 - SI_F_OFF) * SI_F_LINK / SI_E3 182} 183 184// writes r,g,b (0..255) into out3. Returns 0. 185func si_srgb_of_lab(l_e3: i64, a_e3: i64, b_e3: i64, out3: *i64) -> i64 { 186 let fy: i64 = (l_e3 + SI_L_ADD) * 10 / SI_L_DIV 187 let fx: i64 = fy + a_e3 / SI_A_DIV 188 let fz: i64 = fy - b_e3 / SI_B_DIV 189 let yy: i64 = si_finv_e5(fy) 190 let xx: i64 = SI_XN * si_finv_e5(fx) / SI_E5 191 let zz: i64 = SI_ZN * si_finv_e5(fz) / SI_E5 192 let lr: i64 = (SI_M_RX * xx + SI_M_RY * yy + SI_M_RZ * zz) / SI_E4 193 let lg: i64 = (SI_M_GX * xx + SI_M_GY * yy + SI_M_GZ * zz) / SI_E4 194 let lb: i64 = (SI_M_BX * xx + SI_M_BY * yy + SI_M_BZ * zz) / SI_E4 195 out3[0] = si_byte_of_linear(lr) 196 out3[1] = si_byte_of_linear(lg) 197 out3[2] = si_byte_of_linear(lb) 198 return 0 199} 200 201// ---- sRGB -> L* (x 1e3), for the known-answer cross-check against nx_eink_tone ---- 202func si_lstar_of_byte(r: i64, g: i64, b: i64) -> i64 { 203 let lr: i64 = si_linear_of_byte(r) 204 let lg: i64 = si_linear_of_byte(g) 205 let lb: i64 = si_linear_of_byte(b) 206 let y: i64 = (2126 * lr + 7152 * lg + 722 * lb) / SI_E4 207 let fy: i64 = si_cbrt_e5(y) 208 return SI_L_DIV * fy / 100 - SI_L_ADD 209} 210 211// ---- the genome axis ---- 212// The lightness endpoints are DERIVED, not chosen: L* = 50 + b* tan(ITA), evaluated just OUTSIDE the 213// outermost published boundaries (62 into VERY LIGHT, -55 into DARK) so that every one of the six 214// classes is actually reachable. Widening or renumbering the class set moves these endpoints with it. 215func si_lstar_of_gene(hue: i64, region_dl: i64) -> i64 { 216 var h: i64 = hue 217 if h < 0 { h = 0 } 218 if h > SI_GENE_MAX { h = SI_GENE_MAX } 219 let tanv: i64 = SI_TAN_N55 + (SI_TAN_62 - SI_TAN_N55) * h / SI_GENE_MAX 220 return SI_L_PIVOT + SI_B_STAR * tanv / SI_E3 + region_dl 221} 222 223// ITA class of a lightness, by TANGENT comparison -- the definition, with no trigonometry 224func si_class_of_lstar(l_e3: i64) -> i64 { 225 let t: i64 = (l_e3 - SI_L_PIVOT) * SI_E3 / SI_B_STAR 226 if t > SI_TAN_55 { return SI_C_VERYLIGHT } 227 if t > SI_TAN_41 { return SI_C_LIGHT } 228 if t > SI_TAN_28 { return SI_C_INTERMEDIATE } 229 if t > SI_TAN_10 { return SI_C_TAN } 230 if t > SI_TAN_N30 { return SI_C_BROWN } 231 return SI_C_DARK 232} 233 234// THE ENTRY POINT the baker calls: genome hue + a region lightness offset -> sRGB albedo. 235func si_albedo_of_gene(hue: i64, region_dl: i64, out3: *i64) -> i64 { 236 let l: i64 = si_lstar_of_gene(hue, region_dl) 237 return si_srgb_of_lab(l, SI_A_STAR, SI_B_STAR, out3) 238} 239 240// the locus generation that produced a given skin -- stamped by every emitter 241func si_model_version() -> i64 { return SI_MODEL_V } 242 243// region id -> its lightness offset (the baker's region ids are its own; this maps by value) 244func si_dl_of_region(reg: i64) -> i64 { 245 if reg == 0 { return SI_DL_FACE } 246 if reg == 1 { return SI_DL_TORSO } 247 if reg == 2 { return SI_DL_LIMBS } 248 return SI_DL_GENS 249}