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}