code wiki / (root) / nx_arousal_skin_lib.nx

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}