code wiki / (root) / nx_arousal_mesh.nx

nx_arousal_mesh.nx source

↩ module page · 287 lines · 12547 B

1// nx_arousal_mesh.nx -- arousal on a real TRIANGLE MESH, not a raymarched implicit surface. 2// 3// ★ADOPTION, NOT INVENTION. The implicit->explicit bridge already existed and this lane was 4// raymarching around it: nx_isosurf's surface_nets polygonizes any SDF into the nx_trimesh buffers, 5// and nx_trimesh is a real rasterizer with sun direction, specular, texture and BAKED SHADOWS -- none 6// of which a hand-rolled sphere-tracer has. nx_isosurf's own gate comment says it exists to 7// "polygonize the holistic BEING's skin SDF -> a real triangle mesh". 8// 9// ★THE PHYSIOLOGY RIDES ON VERTEX COLOUR: after polygonization every vertex is evaluated through 10// sk_eval_at at its own world position, so the flush field is Gouraud-interpolated across real 11// triangles instead of being computed per-screen-pixel. Same optics, better substrate -- and the 12// identical path an IMPORTED scan mesh takes, so nothing here is throwaway. 13// 14// ⚠HONEST LIMIT: surface-nets of the 44-ellipsoid rig yields a triangle mesh OF THAT RIG. This 15// changes the renderer and the shading substrate, NOT the underlying form. The blob silhouette 16// survives polygonization. Only a correct base mesh (nx_obj_import, gated 14/14) removes it. 17// 18// usage: nx_arousal_mesh <out.png> <sex0m1f> <drive_permil> <melanin_permil> [cell_fx] 19// license_tier: ORIGINAL expect_exit: 0 20import "nx_arousal_skin_lib.nx" 21import "nx_body_figure.nx" 22import "nx_isosurf.nx" 23import "nx_doc_layout.nx" 24import "nx_png_write.nx" 25const AM_MAGIC_185000: i64 = 185000 26const AM_MAGIC_210000: i64 = 210000 27const AM_MAGIC_1520: i64 = 1520 28const AM_MAGIC_1760: i64 = 1760 29const AM_MAGIC_3120: i64 = 3120 30const AM_MAGIC_65536: i64 = 65536 31const AM_MAGIC_9000: i64 = 9000 32const AM_MAGIC_1500: i64 = 1500 33 34const AM_FRAMES: i64 = 3 35const AM_FW: i64 = 340 36const AM_FH: i64 = 560 37const AM_HEAD: i64 = 30 38const AM_FOOT: i64 = 24 39const AM_W: i64 = 1020 40const AM_SPAN_MS: i64 = 900000 41 42func am_px(img: *u8, x: i64, y: i64, r: i64, g: i64, b: i64) -> i64 { 43 if x < 0 { return 0 } 44 if x >= AM_W { return 0 } 45 let o: i64 = (y * AM_W + x) * 3 46 img[o] = r as u8 47 img[o+1] = g as u8 48 img[o+2] = b as u8 49 return 0 50} 51func am_char(img: *u8, tbl: *u8, x: i64, y: i64, ch: i64, cr: i64, cg: i64, cb: i64) -> i64 { 52 let gl: *u8 = nx_doc_glyph(tbl, ch) 53 var r: i64 = 0 54 while r < 8 { 55 let bits: i64 = gl[r] as i64 56 var c: i64 = 0 57 while c < 8 { 58 if ((bits >> c) & 1) == 1 { am_px(img, x + c, y + r, cr, cg, cb) } 59 c = c + 1 60 } 61 r = r + 1 62 } 63 return 0 64} 65func am_text(img: *u8, tbl: *u8, x: i64, y: i64, s: *u8, cr: i64, cg: i64, cb: i64) -> i64 { 66 var i: i64 = 0 67 while s[i] != (0 as u8) { am_char(img, tbl, x + i * 8, y, s[i] as i64, cr, cg, cb); i = i + 1 } 68 return 0 69} 70func am_num(img: *u8, tbl: *u8, x: i64, y: i64, v: i64, cr: i64, cg: i64, cb: i64) -> i64 { 71 var m: i64 = v 72 var nd: i64 = 1 73 while m >= 10 { m = m / 10; nd = nd + 1 } 74 var i: i64 = 0 75 while i < nd { 76 var dv: i64 = v 77 var k: i64 = 0 78 while k < nd - 1 - i { dv = dv / 10; k = k + 1 } 79 am_char(img, tbl, x + i * 8, y, 48 + (dv % 10), cr, cg, cb) 80 i = i + 1 81 } 82 return 0 83} 84func am_site(wx: i64, wy: i64) -> i64 { 85 var ax: i64 = wx 86 if ax < 0 { ax = 0 - ax } 87 if wy > 980 { return 3 } 88 if ax > 380 { return 4 } 89 if wy < 0 - 150 { return 4 } 90 if wy > 700 { return 1 } 91 if wy > 560 { return 0 } 92 if wy > 200 { return 2 } 93 return 5 94} 95 96func main(argc: i64, argv: *i64) -> i64 { 97 if argc < 5 { a_puts("REFUSED reason=mesh_needs_4_args\n" as *u8); return 2 } 98 let path: *u8 = argv[1] as *u8 99 let sex: i64 = a_atoi(argv[2] as *u8) 100 let drive: i64 = a_atoi(argv[3] as *u8) 101 let mel: i64 = a_atoi(argv[4] as *u8) 102 var cell: i64 = 26 103 if argc >= 6 { cell = a_atoi(argv[5] as *u8) } 104 if drive <= 0 { a_puts("REFUSED reason=nonpositive_drive\n" as *u8); return 3 } 105 if drive > 1000 { a_puts("REFUSED reason=drive_over_permil\n" as *u8); return 3 } 106 if mel < 0 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 } 107 if mel > 1000 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 } 108 if cell < 12 { a_puts("REFUSED reason=cell_too_fine\n" as *u8); return 3 } 109 if cell > 120 { a_puts("REFUSED reason=cell_too_coarse\n" as *u8); return 3 } 110 111 let cl: *i64 = sys_mmap(16) as *i64 112 cl[0] = 0 113 let cbuf: *u8 = sys_read_file("arousal_params.conf" as *u8, cl) 114 let cn: i64 = cl[0] 115 let ext: *i64 = sys_mmap(SK_EXTN * 8) as *i64 116 ext[0] = a_conf(cbuf, cn, "mel_ext_r" as *u8, 300) 117 ext[1] = a_conf(cbuf, cn, "mel_ext_g" as *u8, 500) 118 ext[2] = a_conf(cbuf, cn, "mel_ext_b" as *u8, 700) 119 ext[3] = a_conf(cbuf, cn, "hb_ext_r" as *u8, 100) 120 ext[4] = a_conf(cbuf, cn, "hb_ext_g" as *u8, 700) 121 ext[5] = a_conf(cbuf, cn, "hb_ext_b" as *u8, 900) 122 ext[6] = a_conf(cbuf, cn, "hb_base" as *u8, 150) 123 ext[7] = a_conf(cbuf, cn, "hb_span" as *u8, 600) 124 ext[8] = a_conf(cbuf, cn, "flush_span" as *u8, 300) 125 var tau: i64 = a_conf(cbuf, cn, "tau_slow_m_ms" as *u8, AM_MAGIC_185000) 126 if sex == 1 { tau = a_conf(cbuf, cn, "tau_slow_f_ms" as *u8, AM_MAGIC_210000) } 127 128 // ---- build the SDF person, then POLYGONIZE it ----------------------------------------------- 129 let base: i64 = sys_mmap(sdf_bytes()) as i64 130 let scratch: i64 = sys_mmap(sdf_bytes()) as i64 131 sdf_person_full(base, scratch, 0) 132 if sex == 1 { figure_apply(base, FIG_FEM) } else { figure_apply(base, FIG_MASC) } 133 134 // sample the field on a grid spanning the body 135 let ox: i64 = 0 - 880 136 let oy: i64 = 0 - AM_MAGIC_1520 137 let oz: i64 = 0 - 460 138 let GX: i64 = AM_MAGIC_1760 / cell 139 let GY: i64 = AM_MAGIC_3120 / cell 140 let GZ: i64 = 920 / cell 141 let grid: *i64 = sys_mmap((GX+1) * (GY+1) * (GZ+1) * 8) as *i64 142 var i: i64 = 0 143 while i <= GX { 144 var j: i64 = 0 145 while j <= GY { 146 var k: i64 = 0 147 while k <= GZ { 148 grid[(i*(GY+1) + j)*(GZ+1) + k] = sdf_eval(base, ox + i*cell, oy + j*cell, oz + k*cell) 149 k = k + 1 150 } 151 j = j + 1 152 } 153 i = i + 1 154 } 155 tm_reset() 156 surface_nets(grid, GX, GY, GZ, ox, oy, oz, cell, 0, 210 + 160*256 + 150*AM_MAGIC_65536) 157 let nv: i64 = tm_nv() 158 let nt: i64 = tm_nt() 159 if nv <= 0 { a_puts("REFUSED reason=polygonize_empty\n" as *u8); return 4 } 160 if tm_ovf() != 0 { a_puts("REFUSED reason=mesh_buffer_overflow\n" as *u8); return 5 } 161 // ⚠DO NOT call tm_compute_normals() here. surface_nets already writes per-vertex normals from 162 // the SDF GRADIENT (smooth and winding-independent); recomputing them by face-averaging over 163 // surface-nets topology clobbers those and produces black faceted artifacts. 164 tm_set_sun(0 - 500, 620, 0 - 600) 165 tm_set_spec(90) 166 167 // ---- render one frame per time, colouring VERTICES by the physiology ----------------------- 168 let h: i64 = AM_HEAD + AM_FH + AM_FOOT 169 let img: *u8 = sys_mmap(AM_W * h * 3) 170 var by: i64 = 0 171 while by < h { 172 var bx: i64 = 0 173 while bx < AM_W { am_px(img, bx, by, 12, 13, 16); bx = bx + 1 } 174 by = by + 1 175 } 176 let npx: i64 = AM_FW * AM_FH 177 let fb: *i64 = sys_mmap(npx * 8) as *i64 178 let zb: *i64 = sys_mmap(npx * 8) as *i64 179 let out: *i64 = sys_mmap(SK_OUTN * 8) as *i64 180 let fp: *i64 = sys_mmap(AM_FRAMES * 8) as *i64 181 let mr: *i64 = sys_mmap(AM_FRAMES * 8) as *i64 182 let mg: *i64 = sys_mmap(AM_FRAMES * 8) as *i64 183 let fr: *i64 = sys_mmap(AM_FRAMES * 8) as *i64 184 let fg2: *i64 = sys_mmap(AM_FRAMES * 8) as *i64 185 186 var fi: i64 = 0 187 while fi < AM_FRAMES { 188 let t: i64 = (fi * AM_SPAN_MS) / (AM_FRAMES - 1) 189 let perf: i64 = a_perf(t, drive, tau) 190 fp[fi] = perf 191 // ★per-VERTEX physiology: same optics as the raymarcher, Gouraud-interpolated across tris 192 var v: i64 = 0 193 var sr: i64 = 0 194 var sg: i64 = 0 195 while v < nv { 196 let vx: i64 = tm_vx(v) 197 let vy: i64 = tm_vy(v) 198 sk_eval_at(vx, vy, am_site(vx, vy), perf, mel, ext, out) 199 let r8: i64 = (out[2] * 255) / 1000 200 let g8: i64 = (out[3] * 255) / 1000 201 let b8: i64 = (out[4] * 255) / 1000 202 tm_vcol(v, r8 + g8 * 256 + b8 * AM_MAGIC_65536) 203 sr = sr + r8 204 sg = sg + g8 205 v = v + 1 206 } 207 // A render organ must emit its physiology as NUMBERS, not only as pixels -- otherwise the 208 // image is unfalsifiable and no gate can tell a working colour path from a discarded one. 209 mr[fi] = sr / nv 210 mg[fi] = sg / nv 211 var c: i64 = 0 212 while c < npx { fb[c] = 12 + 13*256 + 16*AM_MAGIC_65536; c = c + 1 } 213 trimesh_zclear(zb, npx) 214 // camz/focal must frame a ~3000-unit-tall body: too close and it crops to a torso. 215 // ★smooth MUST be 2, not 1. At smooth<2 the rasterizer shades from the per-TRIANGLE colour 216 // (TCOL, the constant handed to surface_nets) and DISCARDS per-vertex colour entirely -- 217 // so all 17k tm_vcol writes were dropped and every frame rendered the same flat tone. 218 // smooth==2 is the multi-material path that Gouraud-interpolates VCOL across the triangle. 219 trimesh_render(fb, zb, AM_FW, AM_FH, 0, 0, AM_MAGIC_9000, AM_MAGIC_1500, 2) 220 // composite the frame into the strip, and MEASURE THE RENDERED PIXELS. 221 // ★The vertex means above prove the physiology was COMPUTED. They would stay identical even 222 // if the rasterizer discarded vertex colour entirely (which is exactly what smooth<2 does). 223 // Only a framebuffer readback proves it was RENDERED -- a marker and the thing it marks are 224 // different assertions. 225 var yy: i64 = 0 226 var pr: i64 = 0 227 var pg: i64 = 0 228 var pn: i64 = 0 229 while yy < AM_FH { 230 var xx: i64 = 0 231 while xx < AM_FW { 232 let p: i64 = fb[yy * AM_FW + xx] 233 let cr8: i64 = p & 255 234 let cg8: i64 = (p >> 8) & 255 235 let cb8: i64 = (p >> 16) & 255 236 am_px(img, fi * AM_FW + xx, AM_HEAD + yy, cr8, cg8, cb8) 237 // skip the backdrop (12,13,16) so the mean reflects the SUBJECT only 238 if cr8 > 40 { pr = pr + cr8; pg = pg + cg8; pn = pn + 1 } 239 xx = xx + 1 240 } 241 yy = yy + 1 242 } 243 if pn > 0 { fr[fi] = pr / pn; fg2[fi] = pg / pn } else { fr[fi] = 0; fg2[fi] = 0 } 244 fi = fi + 1 245 } 246 247 let tbl: *u8 = font8x8_table() 248 am_text(img, tbl, 8, 11, "AROUSAL ON A POLYGON MESH" as *u8, 255, 255, 255) 249 if sex == 0 { am_text(img, tbl, 220, 11, "male" as *u8, 175, 190, 215) } else { am_text(img, tbl, 220, 11, "female" as *u8, 175, 190, 215) } 250 am_text(img, tbl, 300, 11, "surface-nets + Gouraud vertex physiology" as *u8, 150, 165, 190) 251 am_text(img, tbl, 700, 11, "tris=" as *u8, 150, 165, 190) 252 am_num(img, tbl, 740, 11, nt, 190, 205, 225) 253 fi = 0 254 while fi < AM_FRAMES { 255 let tsec: i64 = (fi * AM_SPAN_MS) / (AM_FRAMES - 1) / 1000 256 am_text(img, tbl, fi * AM_FW + 10, AM_HEAD + AM_FH + 8, "t=" as *u8, 185, 195, 215) 257 am_num(img, tbl, fi * AM_FW + 26, AM_HEAD + AM_FH + 8, tsec, 185, 195, 215) 258 am_text(img, tbl, fi * AM_FW + 26 + 32, AM_HEAD + AM_FH + 8, "s P=" as *u8, 150, 165, 190) 259 am_num(img, tbl, fi * AM_FW + 26 + 72, AM_HEAD + AM_FH + 8, fp[fi], 150, 165, 190) 260 fi = fi + 1 261 } 262 263 let rc: i64 = nx_png_write_rgb(path, img, AM_W, h) 264 if rc < 0 { a_puts("REFUSED reason=png_write_failed\n" as *u8); return 6 } 265 a_puts("ok wrote=" as *u8); a_puts(path) 266 a_puts(" w=" as *u8); a_putn(AM_W) 267 a_puts(" h=" as *u8); a_putn(h) 268 a_puts(" cell=" as *u8); a_putn(cell) 269 a_puts(" verts=" as *u8); a_putn(nv) 270 a_puts(" tris=" as *u8); a_putn(nt) 271 fi = 0 272 while fi < AM_FRAMES { a_puts(" p" as *u8); a_putn(fi); a_puts("=" as *u8); a_putn(fp[fi]); fi = fi + 1 } 273 fi = 0 274 while fi < AM_FRAMES { 275 a_puts(" vr" as *u8); a_putn(fi); a_puts("=" as *u8); a_putn(mr[fi]) 276 a_puts(" vg" as *u8); a_putn(fi); a_puts("=" as *u8); a_putn(mg[fi]) 277 fi = fi + 1 278 } 279 fi = 0 280 while fi < AM_FRAMES { 281 a_puts(" fr" as *u8); a_putn(fi); a_puts("=" as *u8); a_putn(fr[fi]) 282 a_puts(" fg" as *u8); a_putn(fi); a_puts("=" as *u8); a_putn(fg2[fi]) 283 fi = fi + 1 284 } 285 a_puts("\n" as *u8) 286 return 0 287}