code wiki / _hdl_build / nx_meshview.nx
nx_meshview.nx source
↩ module page · 283 lines · 14924 B
1// nx_meshview.nx -- look at an .nxmesh. The program emits NXMSH2 meshes and had NO WAY TO VIEW ONE,
2// which is exactly how a skull that renders as an egg passed a 6/6 gate: every tooth measured counts
3// and determinism, and nothing ever drew the thing.
4//
5// Orthographic z-buffered triangle fill, flat-shaded off the stored per-triangle normal, three views
6// side by side (front / side / three-quarter) so a shape can be judged rather than described.
7//
8// nx_meshview <in.nxmesh> <out.png> [layer] layer: -1 all (default), 0 skin, 1 muscle, 2 bone
9// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26).
10import "nx_png.nx"
11const MV_F32_FRACTION_BITS: i64 = 23 // IEEE 754 binary32 stored fraction width.
12
13const MV_W: i64 = 1200
14const MV_H: i64 = 460
15const MV_VW: i64 = 400 // per-view width
16const MV_TS: i64 = 84 // triangle stride in the file
17// ⚠THE HEADER SIZE IS NOT A CONSTANT. It is 16 + nlayer*24, and nlayer varies by producer: nx_body_gen
18// writes 3 layers (skin/muscle/bone) so 88 bytes, while the BodyParts3D oracle has ONE layer and a
19// 40-byte header. Hardcoding 88 read the oracle 48 bytes off and produced a perfectly plausible-looking
20// 2x2x2 bounding box of garbage, which rendered as an empty frame.
21const MV_FAR: i64 = 2000000000
22const MV_BG: i64 = 0x141c28 // the dark slate the other gates use, so renders are comparable
23const MV_MARGIN: i64 = 24
24
25func mv_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
26func mv_pn(v: i64) -> i64 {
27 let b: *u8 = sys_mmap(32); var x: i64=v; var ng: i64=0
28 if x<0 { ng=1; x=0-x }
29 var i: i64=31
30 if x==0 { b[i]=48 as u8; i=i-1 }
31 while x>0 { b[i]=(48+x%10) as u8; x=x/10; i=i-1 }
32 if ng==1 { b[i]=45 as u8; i=i-1 }
33 sys_write(1,(b as i64 + i + 1) as *u8, 31-i); return 0
34}
35func mv_atoi(s: *u8) -> i64 {
36 var i: i64=0; var n: i64=0; var sg: i64=1
37 if s[0]==(45 as u8) { sg=0-1; i=1 }
38 while s[i]!=(0 as u8) { let c: i64=s[i] as i64; if c>=48 { if c<=57 { n=n*10+(c-48) } } i=i+1 }
39 return n*sg
40}
41func mv_abs(v: i64) -> i64 { if v<0 { return 0-v } return v }
42func mv_rd32(b: *u8, o: i64) -> i64 {
43 return (b[o] as i64) | ((b[o+1] as i64)<<8) | ((b[o+2] as i64)<<16) | ((b[o+3] as i64)<<24)
44}
45// ★IEEE-754 single -> integer scaled by `scale`. The mesh stores floats; this organ is integer-only, so
46// the bits are decoded by hand rather than trusting a cast. Denormals and zero collapse to 0, which is
47// correct for geometry and avoids a special case that would only ever fire on garbage input.
48func mv_f32(bits: i64, scale: i64) -> i64 {
49 let s: i64 = (bits >> 31) & 1
50 let e: i64 = (bits >> 23) & 255
51 let m: i64 = bits & ((1 << MV_F32_FRACTION_BITS) - 1)
52 if e == 0 { return 0 }
53 if e == 255 { return 0 }
54 let mant: i64 = (1 << MV_F32_FRACTION_BITS) | m
55 let sh: i64 = e - 127 - 23
56 var v: i64 = 0
57 if sh >= 0 {
58 if sh > 30 { return 0 }
59 v = mant * scale
60 v = v << sh
61 } else {
62 let rs: i64 = 0 - sh
63 if rs > 62 { return 0 }
64 v = mant * scale
65 v = v >> rs
66 }
67 if s == 1 { return 0 - v }
68 return v
69}
70
71// project a model point to a view. view 0 = front (x,y), 1 = side (z,y), 2 = three-quarter (blend).
72func mv_px(vx: i64, vy: i64, vz: i64, view: i64) -> i64 {
73 if view == 0 { return vx }
74 if view == 1 { return vz }
75 return (vx*7 - vz*7)/10 // yaw 45: screen x = (x - z)/sqrt2
76}
77// ⚠DEPTH SIGN. The rasterizer keeps the SMALLER value, so "depth" must INCREASE with distance from the
78// camera. The first version returned +z for the front view, which keeps the FARTHEST surface -- the
79// front panel was rendering the back of the skull, and it looked plausible enough to nearly pass.
80func mv_pz(vx: i64, vy: i64, vz: i64, view: i64) -> i64 {
81 if view == 0 { return 0 - vz } // camera on +z: nearer = larger z
82 if view == 1 { return 0 - vx } // camera on +x
83 return (0 - vx*7 - vz*7)/10 // camera on (+x,+z)
84}
85
86func main(argc: i64, argv: *i64) -> i64 {
87 if argc < 3 { mv_puts("usage: nx_meshview <in.nxmesh> <out.png> [layer -1|0|1|2]\n" as *u8); return 2 }
88 var want: i64 = 0-1
89 if argc > 3 { want = mv_atoi(argv[3] as *u8) }
90 let ln: *i64 = sys_mmap(16) as *i64
91 let buf: *u8 = sys_read_file(argv[1] as *u8, ln)
92 if (buf as i64) == 0 { mv_puts("{\x22error\x22:\x22cannot read mesh\x22}\n" as *u8); return 3 }
93 if buf[0] != (78 as u8) { mv_puts("{\x22error\x22:\x22not NXMSH2\x22}\n" as *u8); return 4 }
94 let nlayer: i64 = mv_rd32(buf, 8)
95 let nt: i64 = mv_rd32(buf, 12)
96 if nt <= 0 { mv_puts("{\x22error\x22:\x22no triangles\x22}\n" as *u8); return 5 }
97 let MV_HDR: i64 = 16 + nlayer*24
98 let lay: i64 = MV_HDR + nt*MV_TS
99
100 // ---- pass 1: bounding box over the SELECTED layer only, so a single layer fills the frame
101 var lox: i64 = MV_FAR; var hix: i64 = 0-MV_FAR
102 var loy: i64 = MV_FAR; var hiy: i64 = 0-MV_FAR
103 var loz: i64 = MV_FAR; var hiz: i64 = 0-MV_FAR
104 var kept: i64 = 0
105 var t: i64 = 0
106 while t < nt {
107 var take: i64 = 1
108 if want >= 0 { if mv_rd32(buf, lay + t*4) != want { take = 0 } }
109 if take == 1 {
110 kept = kept + 1
111 var j: i64 = 0
112 while j < 3 {
113 let o: i64 = MV_HDR + t*MV_TS + j*12
114 // ⚠read at MICROMETRE precision, not integer units. The BodyParts3D oracle is stored in
115 // METRES, so reading at scale 1 truncated the whole 171k-triangle skull to a 2x2x2 box.
116 // The view auto-fits from the bounding box, so the absolute unit is irrelevant -- only
117 // the precision matters, and this makes mm-scale and metre-scale meshes both work.
118 let x: i64 = mv_f32(mv_rd32(buf,o), 1000)
119 let y: i64 = mv_f32(mv_rd32(buf,o+4), 1000)
120 let z: i64 = mv_f32(mv_rd32(buf,o+8), 1000)
121 if x < lox { lox = x }
122 if x > hix { hix = x }
123 if y < loy { loy = y }
124 if y > hiy { hiy = y }
125 if z < loz { loz = z }
126 if z > hiz { hiz = z }
127 j = j + 1
128 }
129 }
130 t = t + 1
131 }
132 if kept == 0 { mv_puts("{\x22error\x22:\x22layer empty\x22}\n" as *u8); return 6 }
133 var spanx: i64 = hix - lox
134 var spany: i64 = hiy - loy
135 var spanz: i64 = hiz - loz
136 if spanx < 1 { spanx = 1 }
137 if spany < 1 { spany = 1 }
138 if spanz < 1 { spanz = 1 }
139 // one scale for EVERY view, so the three panels are directly comparable rather than each auto-fitted
140 var span: i64 = spanx
141 if spany > span { span = spany }
142 if spanz > span { span = spanz }
143 let usable: i64 = MV_H - MV_MARGIN*2
144 // Form the projection ratio after multiplication; a precomputed ratio collapses large meshes.
145 let cx: i64 = (lox+hix)/2
146 let cy: i64 = (loy+hiy)/2
147 let cz: i64 = (loz+hiz)/2
148
149 let fb: *i64 = sys_mmap(MV_W*MV_H*8) as *i64
150 let zb: *i64 = sys_mmap(MV_W*MV_H*8) as *i64
151 var p: i64 = 0
152 while p < MV_W*MV_H { fb[p] = MV_BG; zb[p] = MV_FAR; p = p + 1 }
153
154 var view: i64 = 0
155 while view < 3 {
156 let ox: i64 = view*MV_VW + MV_VW/2
157 let oy: i64 = MV_H/2
158 t = 0
159 while t < nt {
160 var take: i64 = 1
161 if want >= 0 { if mv_rd32(buf, lay + t*4) != want { take = 0 } }
162 if take == 1 {
163 let base: i64 = MV_HDR + t*MV_TS
164 // three projected vertices
165 let sx: *i64 = sys_mmap(64) as *i64
166 let sy: *i64 = sys_mmap(64) as *i64
167 let sd: *i64 = sys_mmap(64) as *i64
168 var j: i64 = 0
169 while j < 3 {
170 let o: i64 = base + j*12
171 let X: i64 = mv_f32(mv_rd32(buf,o), 1000) - cx
172 let Y: i64 = mv_f32(mv_rd32(buf,o+4), 1000) - cy
173 let Z: i64 = mv_f32(mv_rd32(buf,o+8), 1000) - cz
174 sx[j] = ox + mv_px(X,Y,Z,view)*usable/span
175 sy[j] = oy - Y*usable/span
176 sd[j] = mv_pz(X,Y,Z,view)
177 j = j + 1
178 }
179 // flat shade from the stored normal: lambert against a fixed key, plus ambient
180 // ★AVERAGE ALL THREE VERTEX NORMALS. Flat-shading from vertex A alone means adjacent
181 // triangles are lit by whichever normal happens to sit at their first vertex; with
182 // gradient normals from a polygonizer those differ sharply between neighbours, and the
183 // surface renders as speckled mottling. An STL-sourced mesh hides this because it
184 // carries one consistent facet normal -- which is why the reference looked clean and
185 // ours did not, through the very same renderer.
186 let nx: i64 = (mv_f32(mv_rd32(buf, base+36), 1000) + mv_f32(mv_rd32(buf, base+48), 1000) + mv_f32(mv_rd32(buf, base+60), 1000))/3
187 let ny: i64 = (mv_f32(mv_rd32(buf, base+40), 1000) + mv_f32(mv_rd32(buf, base+52), 1000) + mv_f32(mv_rd32(buf, base+64), 1000))/3
188 let nz: i64 = (mv_f32(mv_rd32(buf, base+44), 1000) + mv_f32(mv_rd32(buf, base+56), 1000) + mv_f32(mv_rd32(buf, base+68), 1000))/3
189 var lam: i64 = (nx*3 + ny*5 + nz*8)/10
190 if lam < 0 { lam = 0 - lam }
191 var sh: i64 = 300 + lam*700/1000
192 if sh > 1000 { sh = 1000 }
193 // ⚠the mesh stores colour as PER-MILLE (written as bg_f32(c,1000)), not 0..255. Reading it
194 // as 8-bit blew every surface to pure white and the first render showed silhouette only,
195 // with no form at all -- the shading was there and invisible.
196 var cr: i64 = mv_f32(mv_rd32(buf, base+72), 1000)*255/1000
197 var cg: i64 = mv_f32(mv_rd32(buf, base+76), 1000)*255/1000
198 var cb: i64 = mv_f32(mv_rd32(buf, base+80), 1000)*255/1000
199 // ⚠a mesh may carry NO colour (the BodyParts3D oracle stores zeros). Multiplying shade by
200 // zero renders the whole model black on a dark background -- an empty image that looks
201 // exactly like a failed load. Fall back to bone so geometry is always visible.
202 if cr + cg + cb < 12 { cr = 216; cg = 210; cb = 198 }
203 var rr: i64 = cr*sh/1000; var gg: i64 = cg*sh/1000; var bb: i64 = cb*sh/1000
204 if rr > 255 { rr = 255 }
205 if gg > 255 { gg = 255 }
206 if bb > 255 { bb = 255 }
207 if rr < 0 { rr = 0 }
208 if gg < 0 { gg = 0 }
209 if bb < 0 { bb = 0 }
210 let col: i64 = (rr<<16) | (gg<<8) | bb
211 // bounding-box scan with edge functions (barycentric inside test)
212 var minx: i64 = sx[0]; var maxx: i64 = sx[0]
213 var miny: i64 = sy[0]; var maxy: i64 = sy[0]
214 var k: i64 = 1
215 while k < 3 {
216 if sx[k] < minx { minx = sx[k] }
217 if sx[k] > maxx { maxx = sx[k] }
218 if sy[k] < miny { miny = sy[k] }
219 if sy[k] > maxy { maxy = sy[k] }
220 k = k + 1
221 }
222 if minx < view*MV_VW { minx = view*MV_VW }
223 if maxx >= (view+1)*MV_VW { maxx = (view+1)*MV_VW - 1 }
224 if miny < 0 { miny = 0 }
225 if maxy >= MV_H { maxy = MV_H - 1 }
226 let area: i64 = (sx[1]-sx[0])*(sy[2]-sy[0]) - (sy[1]-sy[0])*(sx[2]-sx[0])
227 var hit: i64 = 0
228 if area != 0 {
229 var py: i64 = miny
230 while py <= maxy {
231 var px: i64 = minx
232 while px <= maxx {
233 let w0: i64 = (sx[1]-sx[0])*(py-sy[0]) - (sy[1]-sy[0])*(px-sx[0])
234 let w1: i64 = (sx[2]-sx[1])*(py-sy[1]) - (sy[2]-sy[1])*(px-sx[1])
235 let w2: i64 = (sx[0]-sx[2])*(py-sy[2]) - (sy[0]-sy[2])*(px-sx[2])
236 var inside: i64 = 0
237 if w0 >= 0 { if w1 >= 0 { if w2 >= 0 { inside = 1 } } }
238 if w0 <= 0 { if w1 <= 0 { if w2 <= 0 { inside = 1 } } }
239 if inside == 1 {
240 hit = 1
241 // ★INTERPOLATE DEPTH PER PIXEL. Using the triangle's centroid depth for
242 // every pixel it covers makes overlapping triangles win and lose the z
243 // test inconsistently, and a dense mesh renders as speckle. The edge
244 // functions ARE the barycentric weights, so this costs one divide.
245 var wsum: i64 = w0 + w1 + w2
246 var d: i64 = (sd[0]+sd[1]+sd[2])/3
247 if wsum != 0 { d = (w1*sd[0] + w2*sd[1] + w0*sd[2]) / wsum }
248 let idx: i64 = py*MV_W + px
249 if d < zb[idx] { zb[idx] = d; fb[idx] = col }
250 }
251 px = px + 1
252 }
253 py = py + 1
254 }
255 }
256 // ★SUB-PIXEL FALLBACK. A dense mesh puts many triangles inside a single pixel, and an
257 // edge test evaluated at the pixel CENTRE rejects every one that does not happen to
258 // cover it -- the surface renders as speckle full of holes. If a triangle covered no
259 // pixel, plot its centroid, so coverage never depends on a triangle being large enough.
260 if hit == 0 {
261 let mx: i64 = (sx[0]+sx[1]+sx[2])/3
262 let my2: i64 = (sy[0]+sy[1]+sy[2])/3
263 if mx >= view*MV_VW { if mx < (view+1)*MV_VW { if my2 >= 0 { if my2 < MV_H {
264 let d2: i64 = (sd[0]+sd[1]+sd[2])/3
265 let idx2: i64 = my2*MV_W + mx
266 if d2 < zb[idx2] { zb[idx2] = d2; fb[idx2] = col }
267 } } } }
268 }
269 }
270 t = t + 1
271 }
272 view = view + 1
273 }
274 write_png(fb, MV_W, MV_H, argv[2] as *u8)
275 mv_puts("{\x22organ\x22:\x22nx_meshview\x22,\x22tris_total\x22:" as *u8); mv_pn(nt)
276 mv_puts(",\x22tris_drawn\x22:" as *u8); mv_pn(kept)
277 mv_puts(",\x22layer\x22:" as *u8); mv_pn(want)
278 mv_puts(",\x22bbox_x\x22:" as *u8); mv_pn(spanx)
279 mv_puts(",\x22bbox_y\x22:" as *u8); mv_pn(spany)
280 mv_puts(",\x22bbox_z\x22:" as *u8); mv_pn(spanz)
281 mv_puts(",\x22views\x22:\x22front|side|three-quarter, ONE shared scale so the panels compare\x22}\n" as *u8)
282 return 0
283}