nx_arousal_skin_lib.nx source
↩ module page · 190 lines · 8452 B
1// nx_arousal_skin_lib.nx -- skin OPTICS core (no main). Shared by nx_arousal_skin (CLI) and
2// nx_arousal_render (LOOK path) so the two cannot drift into duplicate twins.
3// Separate from nx_arousal_lib on purpose: that one is PHYSIOLOGY, this one is OPTICS.
4// license_tier: ORIGINAL
5import "nx_arousal_lib.nx"
6
7const SK_NSITES: i64 = 6
8// ext[] layout, passed by the caller so every constant stays conf-bound at the edge:
9// 0 mel_ext_r 1 mel_ext_g 2 mel_ext_b 3 hb_ext_r 4 hb_ext_g 5 hb_ext_b
10// 6 hb_base 7 hb_span 8 flush_span
11const SK_EXTN: i64 = 9
12// out[] layout: 0 local_perf 1 hb 2 r 3 g 4 b 5 flush
13const SK_OUTN: i64 = 6
14
15// flush onset thresholds in perfusion permil, in Masters & Johnson propagation order
16func sk_thresh(site: i64) -> i64 {
17 if site == 0 { return 550 }
18 if site == 1 { return 600 }
19 if site == 2 { return 650 }
20 if site == 3 { return 700 }
21 if site == 4 { return 800 }
22 return 400
23}
24func sk_name(site: i64) -> *u8 {
25 if site == 0 { return "submammary" as *u8 }
26 if site == 1 { return "breast" as *u8 }
27 if site == 2 { return "torso" as *u8 }
28 if site == 3 { return "face" as *u8 }
29 if site == 4 { return "extremities" as *u8 }
30 return "genital" as *u8
31}
32// genital tissue carries far more vasocongestion than surface skin at the same systemic drive
33func sk_site_gain(site: i64) -> i64 {
34 if site == 5 { return 1400 }
35 return 1000
36}
37// CONTINUOUS flush-threshold field over the body, inverse-distance weighted across the SAME site
38// anchors sk_thresh() returns -- so the field and the per-site API cannot drift apart.
39//
40// ★WHY A FIELD AND NOT A LOOKUP: hard-assigning one site per surface point produces STEP EDGES, and
41// on a rendered body that reads as rectangular bands painted on skin rather than skin flushing. Real
42// flush spreads continuously. The propagation ORDER is preserved because the anchors carry it; only
43// the boundaries become smooth.
44// Anchor positions are the rig's own part centres (nx_sdfrender sdf_body), fx1024 units.
45func sk_anchor_x(i: i64) -> i64 {
46 if i == 1 { return 0 }
47 if i == 2 { return 0 }
48 if i == 3 { return 0 }
49 if i == 4 { return 0 - 550 }
50 if i == 5 { return 550 }
51 if i == 6 { return 0 - 195 }
52 if i == 7 { return 195 }
53 return 0
54}
55func sk_anchor_y(i: i64) -> i64 {
56 if i == 1 { return 400 }
57 if i == 2 { return 620 }
58 if i == 3 { return 800 }
59 if i == 4 { return 500 }
60 if i == 5 { return 500 }
61 if i == 6 { return 0 - 700 }
62 if i == 7 { return 0 - 700 }
63 if i == 8 { return 1250 }
64 return 80
65}
66// site index whose threshold each anchor carries
67func sk_anchor_site(i: i64) -> i64 {
68 if i == 1 { return 2 }
69 if i == 2 { return 0 }
70 if i == 3 { return 1 }
71 if i == 4 { return 4 }
72 if i == 5 { return 4 }
73 if i == 6 { return 4 }
74 if i == 7 { return 4 }
75 if i == 8 { return 3 }
76 return 5
77}
78// CONTINUOUS site-gain field. The threshold field alone was not enough: gain multiplies perfusion
79// directly, so a discrete 1.4x genital gain over a RECTANGULAR region left a hard-edged block on the
80// rendered body even after the thresholds were smoothed. Genital tissue really does carry more
81// vasocongestion -- but its boundary is a localised falloff, not a box.
82func sk_gain_field(wx: i64, wy: i64) -> i64 {
83 let dx: i64 = wx / 16
84 let dy: i64 = (wy - 80) / 16
85 let d2: i64 = dx * dx + dy * dy
86 let bump: i64 = 1400 / (d2 / 90 + 1)
87 var g: i64 = 1000 + bump
88 if g > 1400 { g = 1400 }
89 return g
90}
91// sk_thresh_field_rig -- the flush field over an ARBITRARY rig's anchors.
92//
93// WHY THIS EXISTS: sk_anchor_x/y above are HARDCODED LITERALS in fx1024, welded to one body
94// (nx_sdfrender sdf_body). That is body-specific GEOMETRY baked inside an OPTICS library, and it is
95// why this lib cannot be pointed at the NXA rig path (nx_nxa_skin imports a real FBX rig: joints in
96// mm with q12 quats -- a different frame AND different units). Measured: sk_anchor_* has ZERO
97// external callers ecosystem-wide, so no SDF->NXA bridge was ever authored.
98//
99// The fix is not a bridge -- a bridge would enshrine the weld. Geometry belongs with the geometry:
100// the CALLER supplies its own anchors, so the SDF body passes its part centres and an NXA rig passes
101// its joint positions, in whatever units that caller already uses.
102// ax/ay : anchor coordinates, caller's units (must match wx/wy)
103// asite : site index each anchor carries (preserves the Masters & Johnson propagation ORDER)
104// n : anchor count
105// scale : divisor putting caller units on the same footing as the fx1024 tuning (16 for fx1024)
106// Fail-safe: n<=0 returns the same neutral 650 the original returns when the weights vanish.
107// LAW: A LIBRARY THAT HARDCODES ONE SUBJECT'S GEOMETRY CAN ONLY EVER SERVE ONE SUBJECT.
108func sk_thresh_field_rig(wx: i64, wy: i64, ax: *i64, ay: *i64, asite: *i64, n: i64, scale: i64) -> i64 {
109 if n <= 0 { return 650 }
110 let sc: i64 = scale
111 var num: i64 = 0
112 var den: i64 = 0
113 var i: i64 = 0
114 while i < n {
115 let dx: i64 = (wx - ax[i]) / sc
116 let dy: i64 = (wy - ay[i]) / sc
117 let d2: i64 = dx * dx + dy * dy + 4
118 let w: i64 = 4000000 / d2
119 num = num + w * sk_thresh(asite[i])
120 den = den + w
121 i = i + 1
122 }
123 if den <= 0 { return 650 }
124 return num / den
125}
126
127// Original SDF-body entry point, preserved EXACTLY (rule 19) by delegating with this body's own
128// anchors. Every existing caller and gate keeps its behaviour bit-for-bit (gate 29/29 GREEN).
129func sk_thresh_field(wx: i64, wy: i64) -> i64 {
130 let ax: *i64 = sys_mmap(9 * 8) as *i64
131 let ay: *i64 = sys_mmap(9 * 8) as *i64
132 let st: *i64 = sys_mmap(9 * 8) as *i64
133 var i: i64 = 0
134 while i < 9 {
135 ax[i] = sk_anchor_x(i)
136 ay[i] = sk_anchor_y(i)
137 st[i] = sk_anchor_site(i)
138 i = i + 1
139 }
140 return sk_thresh_field_rig(wx, wy, ax, ay, st, 9, 16)
141}
142func sk_clamp(v: i64) -> i64 {
143 if v < 0 { return 0 }
144 if v > 1000 { return 1000 }
145 return v
146}
147// Reflectance in one channel by BEER-LAMBERT: R = exp(-(A_melanin + A_hemoglobin)).
148// Absorbances ADD, transmittances MULTIPLY. Linear subtraction is only the first Taylor term and
149// SATURATES -- it drove blue to a destroyed 0. An exponential never reaches zero.
150func sk_refl(melanin: i64, hb: i64, mel_ext: i64, hb_ext: i64) -> i64 {
151 let a_mel: i64 = (melanin * mel_ext) / 1000
152 let a_hb: i64 = (hb * hb_ext) / 1000
153 return sk_clamp(a_exp_neg(a_mel + a_hb))
154}
155// Full site evaluation. Writes SK_OUTN values into out[].
156//
157// ★THE FLUSH MUST FEED THE COLOUR. The sex flush IS additional cutaneous vasocongestion layered on
158// top of baseline perfusion, so it adds to the dermal hemoglobin term. Computing a flush number and
159// leaving it out of the optics makes the whole Masters & Johnson propagation map VISUALLY INERT --
160// every surface site renders identically because they share the same site gain. That defect is
161// invisible to any byte-level tooth and obvious the moment the field is rendered.
162// Field evaluation at a body position: identical optics to sk_eval, but the flush threshold comes
163// from the CONTINUOUS field rather than a discrete site, so a rendered surface has no step edges.
164// Site gain still applies discretely -- genital tissue really is a different tissue, not a gradient.
165func sk_eval_at(wx: i64, wy: i64, site: i64, perf: i64, mel: i64, ext: *i64, out: *i64) -> i64 {
166 let local: i64 = sk_clamp((perf * sk_gain_field(wx, wy)) / 1000)
167 let flush: i64 = a_flush(local, sk_thresh_field(wx, wy), 1000)
168 let base: i64 = ext[6] + (ext[7] * local) / 1000
169 let hb: i64 = sk_clamp(base + (ext[8] * flush) / 1000)
170 out[0] = local
171 out[1] = hb
172 out[2] = sk_refl(mel, hb, ext[0], ext[3])
173 out[3] = sk_refl(mel, hb, ext[1], ext[4])
174 out[4] = sk_refl(mel, hb, ext[2], ext[5])
175 out[5] = flush
176 return 0
177}
178func sk_eval(site: i64, perf: i64, mel: i64, prop: i64, ext: *i64, out: *i64) -> i64 {
179 let local: i64 = sk_clamp((perf * sk_site_gain(site)) / 1000)
180 let flush: i64 = a_flush(local, sk_thresh(site), prop)
181 let base: i64 = ext[6] + (ext[7] * local) / 1000
182 let hb: i64 = sk_clamp(base + (ext[8] * flush) / 1000)
183 out[0] = local
184 out[1] = hb
185 out[2] = sk_refl(mel, hb, ext[0], ext[3])
186 out[3] = sk_refl(mel, hb, ext[1], ext[4])
187 out[4] = sk_refl(mel, hb, ext[2], ext[5])
188 out[5] = flush
189 return 0
190}