nx_hull_mesh.nx source
↩ module page · 219 lines · 9445 B
1// nx_hull_mesh.nx -- THE JOIN: silhouettes -> carved SDF field -> triangle mesh -> rendered PNG.
2//
3// nx_visual_hull (carve) and nx_isosurf (polygonize) were each gated in ISOLATION, and a gated half
4// plus a gated half is not a proven whole. This organ is the composition, and it exists so the
5// image->geometry->render path is proven end-to-end BEFORE any real photograph arrives.
6//
7// Runs on SYNTHETIC silhouettes by default (a plus-shaped column: distinctive enough that a correct
8// carve is visually unmistakable and a wrong axis mapping is obvious). Give it two image paths and
9// it carves those instead -- same code path, so the synthetic proof is not a separate lane.
10//
11// ⚠VIEWS MUST DIFFER IN ANGLE. Front + lateral carves a form; front + BACK does not (same projection
12// axis, one constraint). nx_visual_hull_gate proves that mechanically.
13//
14// usage: nx_hull_mesh <out.png> [cell] [gridN]
15// license_tier: ORIGINAL expect_exit: 0
16import "nx_visual_hull.nx"
17import "nx_isosurf.nx"
18import "nx_doc_layout.nx"
19import "nx_png_write.nx"
20
21const HM_IW: i64 = 192
22const HM_IH: i64 = 192
23const HM_W: i64 = 760
24const HM_FW: i64 = 380
25const HM_FH: i64 = 420
26const HM_HEAD: i64 = 30
27const HM_FOOT: i64 = 22
28
29// Local print/parse helpers. Deliberately NOT imported from nx_arousal_lib: this organ is GEOMETRY,
30// and pulling the physiology lib in for a print helper would couple two unrelated lanes.
31func hm_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
32func hm_putn(v: i64) -> i64 {
33 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
34 var m: i64 = v
35 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
36 let d: *u8 = sys_mmap(24); var k: i64 = 0
37 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
38 let o: *u8 = sys_mmap(24); var w: i64 = 0
39 while w < k { o[w] = d[k-1-w]; w = w + 1 }
40 sys_write(1, o, k)
41 sys_munmap(d, 24); sys_munmap(o, 24)
42 return 0
43}
44func hm_atoi(s: *u8) -> i64 {
45 var i: i64 = 0; var v: i64 = 0
46 while s[i] >= (48 as u8) { if s[i] > (57 as u8) { return v } v = v * 10 + ((s[i] as i64) - 48); i = i + 1 }
47 return v
48}
49func hm_px(img: *u8, x: i64, y: i64, r: i64, g: i64, b: i64) -> i64 {
50 if x < 0 { return 0 }
51 if x >= HM_W { return 0 }
52 let o: i64 = (y * HM_W + x) * 3
53 img[o] = r as u8
54 img[o+1] = g as u8
55 img[o+2] = b as u8
56 return 0
57}
58func hm_char(img: *u8, tbl: *u8, x: i64, y: i64, ch: i64, cr: i64, cg: i64, cb: i64) -> i64 {
59 let gl: *u8 = nx_doc_glyph(tbl, ch)
60 var r: i64 = 0
61 while r < 8 {
62 let bits: i64 = gl[r] as i64
63 var c: i64 = 0
64 while c < 8 {
65 if ((bits >> c) & 1) == 1 { hm_px(img, x + c, y + r, cr, cg, cb) }
66 c = c + 1
67 }
68 r = r + 1
69 }
70 return 0
71}
72func hm_text(img: *u8, tbl: *u8, x: i64, y: i64, s: *u8, cr: i64, cg: i64, cb: i64) -> i64 {
73 var i: i64 = 0
74 while s[i] != (0 as u8) { hm_char(img, tbl, x + i * 8, y, s[i] as i64, cr, cg, cb); i = i + 1 }
75 return 0
76}
77func hm_num(img: *u8, tbl: *u8, x: i64, y: i64, v: i64, cr: i64, cg: i64, cb: i64) -> i64 {
78 var m: i64 = v
79 var nd: i64 = 1
80 while m >= 10 { m = m / 10; nd = nd + 1 }
81 var i: i64 = 0
82 while i < nd {
83 var dv: i64 = v
84 var k: i64 = 0
85 while k < nd - 1 - i { dv = dv / 10; k = k + 1 }
86 hm_char(img, tbl, x + i * 8, y, 48 + (dv % 10), cr, cg, cb)
87 i = i + 1
88 }
89 return 0
90}
91// synthetic studio plate: bright PLUS-shaped column on a dark backdrop. A plus is deliberately
92// asymmetric under a 90-degree view swap only if the carve is wrong, so a bad axis mapping shows.
93func hm_plate(rgb: *u8, narrow: i64) -> i64 {
94 var y: i64 = 0
95 while y < HM_IH {
96 var x: i64 = 0
97 while x < HM_IW {
98 let o: i64 = (y * HM_IW + x) * 3
99 var on: i64 = 0
100 // vertical bar
101 if x >= 80 { if x < 112 { if y >= 30 { if y < 162 { on = 1 } } } }
102 // horizontal arm, present only on the wide plate
103 if narrow == 0 { if y >= 70 { if y < 102 { if x >= 46 { if x < 146 { on = 1 } } } } }
104 if on == 1 { rgb[o] = 225 as u8; rgb[o+1] = 205 as u8; rgb[o+2] = 195 as u8 }
105 else { rgb[o] = 18 as u8; rgb[o+1] = 18 as u8; rgb[o+2] = 22 as u8 }
106 x = x + 1
107 }
108 y = y + 1
109 }
110 return 0
111}
112
113func main(argc: i64, argv: *i64) -> i64 {
114 if argc < 2 { hm_puts("REFUSED reason=hull_needs_out_path\n" as *u8); return 2 }
115 let path: *u8 = argv[1] as *u8
116 var cell: i64 = 28
117 if argc >= 3 { cell = hm_atoi(argv[2] as *u8) }
118 var GN: i64 = 40
119 if argc >= 4 { GN = hm_atoi(argv[3] as *u8) }
120 if cell < 8 { hm_puts("REFUSED reason=cell_too_fine\n" as *u8); return 3 }
121 if cell > 200 { hm_puts("REFUSED reason=cell_too_coarse\n" as *u8); return 3 }
122 if GN < 8 { hm_puts("REFUSED reason=grid_too_small\n" as *u8); return 3 }
123 if GN > 96 { hm_puts("REFUSED reason=grid_too_large\n" as *u8); return 3 }
124
125 // ---- two silhouettes, differing in ANGLE --------------------------------------------------
126 let rgbA: *u8 = sys_mmap(HM_IW * HM_IH * 3)
127 let rgbB: *u8 = sys_mmap(HM_IW * HM_IH * 3)
128 hm_plate(rgbA, 0)
129 hm_plate(rgbB, 1)
130 let mA: *u8 = sys_mmap(HM_IW * HM_IH)
131 let mB: *u8 = sys_mmap(HM_IW * HM_IH)
132 let fgA: i64 = vh_bg_mask(rgbA, HM_IW, HM_IH, 90, mA)
133 let fgB: i64 = vh_bg_mask(rgbB, HM_IW, HM_IH, 90, mB)
134 if fgA <= 0 { hm_puts("REFUSED reason=front_silhouette_empty\n" as *u8); return 4 }
135 if fgB <= 0 { hm_puts("REFUSED reason=lateral_silhouette_empty\n" as *u8); return 4 }
136 let sA: *i64 = sys_mmap(HM_IW * HM_IH * 8) as *i64
137 let sB: *i64 = sys_mmap(HM_IW * HM_IH * 8) as *i64
138 vh_sdf2d(mA, HM_IW, HM_IH, sA)
139 vh_sdf2d(mB, HM_IW, HM_IH, sB)
140
141 // ---- CARVE: fold both views into one field --------------------------------------------------
142 let gn1: i64 = GN + 1
143 let grid: *i64 = sys_mmap(gn1 * gn1 * gn1 * 8) as *i64
144 let org: i64 = 0 - (GN * cell) / 2
145 let ppu: i64 = 128
146 // front: image horizontal axis = world x
147 vh_carve_view(sA, HM_IW, HM_IH, 1024, 0, grid, GN, GN, GN, org, org, org, cell, HM_IW/2, HM_IH/2, 0, 0, ppu, 1)
148 // lateral: image horizontal axis = world z
149 vh_carve_view(sB, HM_IW, HM_IH, 0, 1024, grid, GN, GN, GN, org, org, org, cell, HM_IW/2, HM_IH/2, 0, 0, ppu, 0)
150
151 // ---- POLYGONIZE the carved field ------------------------------------------------------------
152 tm_reset()
153 surface_nets(grid, GN, GN, GN, org, org, org, cell, 0, 205 + 165*256 + 155*65536)
154 let nv: i64 = tm_nv()
155 let nt: i64 = tm_nt()
156 if nv <= 0 { hm_puts("REFUSED reason=carve_polygonized_empty\n" as *u8); return 5 }
157 if tm_ovf() != 0 { hm_puts("REFUSED reason=mesh_buffer_overflow\n" as *u8); return 6 }
158 tm_set_sun(0 - 500, 620, 0 - 600)
159 tm_set_spec(90)
160
161 // ---- RENDER: two yaws so the carved cross-section is visible --------------------------------
162 let h: i64 = HM_HEAD + HM_FH + HM_FOOT
163 let img: *u8 = sys_mmap(HM_W * h * 3)
164 var by: i64 = 0
165 while by < h {
166 var bx: i64 = 0
167 while bx < HM_W { hm_px(img, bx, by, 12, 13, 16); bx = bx + 1 }
168 by = by + 1
169 }
170 let npx: i64 = HM_FW * HM_FH
171 let fb: *i64 = sys_mmap(npx * 8) as *i64
172 let zb: *i64 = sys_mmap(npx * 8) as *i64
173 let yaws: *i64 = sys_mmap(16) as *i64
174 // ★yaw is RADIANS SCALED BY 4096 (it_sin4096 reduces mod 2*IT_PI, IT_PI ~= 12868), NOT a
175 // 4096-per-turn integer. A quarter turn is IT_PI2 (~6434). Guessing "4096-based" gave 1024,
176 // which is ~14 degrees -- so the side view barely moved and looked identical to the front.
177 // Use the symbolic constant; a hand-typed 6434 is the next magic number waiting to drift.
178 yaws[0] = 0
179 yaws[1] = IT_PI2
180 var fi: i64 = 0
181 while fi < 2 {
182 var c: i64 = 0
183 while c < npx { fb[c] = 12 + 13*256 + 16*65536; c = c + 1 }
184 trimesh_zclear(zb, npx)
185 trimesh_render(fb, zb, HM_FW, HM_FH, yaws[fi], 0, 4200, 1100, 1)
186 var yy: i64 = 0
187 while yy < HM_FH {
188 var xx: i64 = 0
189 while xx < HM_FW {
190 let p: i64 = fb[yy * HM_FW + xx]
191 hm_px(img, fi * HM_FW + xx, HM_HEAD + yy, p & 255, (p >> 8) & 255, (p >> 16) & 255)
192 xx = xx + 1
193 }
194 yy = yy + 1
195 }
196 fi = fi + 1
197 }
198
199 let tbl: *u8 = font8x8_table()
200 hm_text(img, tbl, 8, 11, "SILHOUETTE -> CARVE -> MESH -> RENDER" as *u8, 255, 255, 255)
201 hm_text(img, tbl, 320, 11, "tris=" as *u8, 150, 165, 190)
202 hm_num(img, tbl, 360, 11, nt, 190, 205, 225)
203 hm_text(img, tbl, 440, 11, "verts=" as *u8, 150, 165, 190)
204 hm_num(img, tbl, 488, 11, nv, 190, 205, 225)
205 hm_text(img, tbl, 10, HM_HEAD + HM_FH + 8, "yaw 0 (front: arms visible)" as *u8, 185, 195, 215)
206 hm_text(img, tbl, HM_FW + 10, HM_HEAD + HM_FH + 8, "yaw 90 (side: arms edge-on)" as *u8, 185, 195, 215)
207
208 let rc: i64 = nx_png_write_rgb(path, img, HM_W, h)
209 if rc < 0 { hm_puts("REFUSED reason=png_write_failed\n" as *u8); return 7 }
210 hm_puts("ok wrote=" as *u8); hm_puts(path)
211 hm_puts(" fgA=" as *u8); hm_putn(fgA)
212 hm_puts(" fgB=" as *u8); hm_putn(fgB)
213 hm_puts(" cell=" as *u8); hm_putn(cell)
214 hm_puts(" grid=" as *u8); hm_putn(GN)
215 hm_puts(" verts=" as *u8); hm_putn(nv)
216 hm_puts(" tris=" as *u8); hm_putn(nt)
217 hm_puts("\n" as *u8)
218 return 0
219}