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}