nx_trimesh.nx source
↩ module page · 774 lines · 45206 B
1// nx_trimesh.nx -- ★SOVEREIGN TRIANGLE-MESH RASTERIZER (operator 2026-07-10, frank feedback: "we just have circles,
2// no triangle or other capabilities for shapes to make more and more real things"). Correct: everything so far is
3// axis-aligned ELLIPSOIDS (smooth blobs) -- no flat faces, sharp edges, or arbitrary form. This adds the UNIVERSAL
4// primitive: the TRIANGLE. A real integer rasterizer -- transform -> perspective project -> barycentric fill with a
5// Z-BUFFER + flat per-face shading -> arbitrary polygon meshes with FLAT FACES and SHARP EDGES. Plus a primitive
6// library (cube/tetra/octa/pyramid/prism/icosahedron/uv-sphere) and composition (build real things from many meshes).
7// 100% integer, sovereign (own trig, own raster). license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_itrig.nx"
10import "nx_vecmath.nx"
11import "nx_relief_lib.nx"
12const TM_MAGIC_1024: i64 = 1024
13const TM_MAGIC_1000000000: i64 = 1000000000
14const TM_MAGIC_374761393: i64 = 374761393
15const TM_MAGIC_668265263: i64 = 668265263
16const TM_MAGIC_1610612741: i64 = 1610612741
17const TM_MAGIC_1274126177: i64 = 1274126177
18const TM_MAGIC_1048576: i64 = 1048576
19const TM_MAGIC_4096: i64 = 4096
20const TM_MAGIC_1618: i64 = 1618
21const TM_MAGIC_65536: i64 = 65536
22const TM_MAGIC_1300: i64 = 1300
23const TM_MAGIC_2400: i64 = 2400
24const TM_MAGIC_3000: i64 = 3000
25const TM_MAGIC_100000: i64 = 100000
26const TM_MAGIC_90000: i64 = 90000
27const TM_MAGIC_65535: i64 = 65535
28const TM_MAGIC_37777: i64 = 37777
29const TM_MAGIC_91131: i64 = 91131
30const TM_MAGIC_6144: i64 = 6144
31const TM_MAGIC_81111: i64 = 81111
32const TM_MAGIC_2000000000: i64 = 2000000000
33
34// ★CAPACITY IS DERIVED, NOT TYPED. Surface nets emits at most one vertex per grid cell, so a mesh from
35// an N^3 grid cannot exceed N^3 vertices -- but the surface is 2-DIMENSIONAL, so the real bound is the
36// number of cells the surface passes through, which scales as N^2. TM_GRID is the largest grid any
37// caller polygonizes; everything else follows from it, with a safety factor for a folded surface that
38// crosses a cell column more than once. Change the grid and the caps move with it.
39// (The previous 48k was typed, and silently truncated a skull at 2mm cells -- the organ then reported a
40// bounding box measured from an INCOMPLETE mesh, a wrong number that looks like a right one.)
41const TM_GRID: i64 = 256 // max polygonizer grid resolution supported
42const TM_FOLD: i64 = 16 // surface-crossings per cell column, worst case. MEASURED
43 // 2026-08-10: 6 truncated a real skull voxelization at 1mm -- a
44 // two-sided thin shell with sinuses, orbits and a tooth row crosses
45 // a column far more than 6 times (786,432-tri cap hit, half the
46 // skull dropped, overflow=1 the only witness). Same failure the
47 // comment above already records for the typed 48k.
48const TM_VCAP: i64 = TM_GRID*TM_GRID*TM_FOLD
49const TM_TCAP: i64 = TM_VCAP*2 // surface nets emits ~2 triangles per vertex
50// ★ACTIVE capacity, chosen by the CALLER. TM_VCAP/TM_TCAP above are the POLYGONIZER's worst case, and
51// a 256^3 surface-nets run genuinely needs them -- but because capacity was a COMPILE-TIME const,
52// every other consumer was forced to pay that floor: 13 arrays = 136 MiB. A skinned character is
53// 14,164 verts / 28,092 tris = 1.83 MiB, so the fixed cap overcharged it 74x, and that overcharge put
54// the estate's ONLY 3D rasterizer out of reach of every memory-bounded target. The wasm world page is
55// the sharp case: it cannot call sys_mmap AT ALL (sys_mmap is __syscall(SYS_MMAP,...) and wasm has no
56// syscalls), which is exactly why the nx_wasm_* modules address everything as base+offset.
57// These two hold whatever the ACTIVE allocation is; 0 means "not yet allocated", so the accessors
58// fall back to the const and every existing caller is unaffected.
59static TM_VCAPN: i64
60static TM_TCAPN: i64
61func tm_vcap() -> i64 { if TM_VCAPN == 0 { return TM_VCAP } return TM_VCAPN }
62func tm_tcap() -> i64 { if TM_TCAPN == 0 { return TM_TCAP } return TM_TCAPN }
63static TM_VX: i64 // model-space vert x,y,z (3 arrays interleaved via ptrs)
64static TM_VY: i64
65static TM_VZ: i64
66static TM_TA: i64 // tri vert index a,b,c + colour (4 arrays)
67static TM_TB: i64
68static TM_TC: i64
69static TM_TCOL: i64
70static TM_VNX: i64 // ★F3: per-vertex model-space normal (for Gouraud smooth shading)
71static TM_VNY: i64
72static TM_VNZ: i64
73static TM_VCOL: i64 // per-vertex colour (for multi-material meshes, e.g. the meshed anatomy)
74static TM_NV: i64
75static TM_NT: i64
76static TM_OVF: i64 // set to 1 if a vertex/triangle add hit the capacity (so callers detect truncation)
77func tm_ovf() -> i64 { return TM_OVF }
78static TM_SPEC: i64 // ★F3 Blinn-Phong specular strength (0 = off/matte; ~110 = shiny). opt-in, zero-cost when 0.
79func tm_set_spec(v: i64) -> i64 { TM_SPEC = v; return 0 }
80static TM_TEX: i64 // ★F3 procedural 3D solid-noise texture: lattice scale in units (0 = off). opt-in.
81func tm_set_tex(scale: i64) -> i64 { TM_TEX = scale; return 0 }
82// ★Q2 SUN SHADOWS (2026-07-12, the quality ladder): a world-space orthographic SHADOW MAP. Bake once per scene
83// (model space -- camera-independent, so one bake serves every view), then the pixel path darkens fragments whose
84// sun-depth is behind the nearest blocker. Opt-in: tm_set_sun + tm_shadow_bake + tm_set_shadow(1); zero-cost off.
85const TM_SHRES: i64 = 576
86static TM_SUNX: i64 // direction TOWARD the sun, normalized *1024
87static TM_SUNY: i64
88static TM_SUNZ: i64
89static TM_SHON: i64 // 1 = apply the baked map in the pixel path
90static TM_SH: i64 // the map: TM_SHRES^2 i64 sun-depths (max = nearest blocker)
91static TM_SRX: i64 // ortho basis R (*1024)
92static TM_SRY: i64
93static TM_SRZ: i64
94static TM_SUX: i64 // ortho basis U (*1024)
95static TM_SUY: i64
96static TM_SUZ: i64
97static TM_SHA0: i64 // a-min / b-min / spans (world units) -> texel mapping
98static TM_SHB0: i64
99static TM_SHSA: i64
100static TM_SHSB: i64
101func tm_set_sun(x: i64, y: i64, z: i64) -> i64 {
102 let l: i64 = tm_isqrt(x*x + y*y + z*z) + 1
103 TM_SUNX = x*TM_MAGIC_1024/l
104 TM_SUNY = y*TM_MAGIC_1024/l
105 TM_SUNZ = z*TM_MAGIC_1024/l
106 return 0
107}
108func tm_set_shadow(v: i64) -> i64 { TM_SHON = v; return 0 }
109func tm_shadow_bake() -> i64 {
110 if TM_SH == 0 { TM_SH = sys_mmap(TM_SHRES*TM_SHRES*8) as i64 }
111 let sh: *i64 = TM_SH as *i64
112 var i: i64 = 0
113 while i < TM_SHRES*TM_SHRES { sh[i] = 0 - TM_MAGIC_1000000000; i = i + 1 }
114 // ortho basis from the sun dir: R = S x up (fallback x-axis), U = S x R
115 var rx: i64 = TM_SUNZ
116 var ry: i64 = 0
117 var rz: i64 = 0 - TM_SUNX
118 var rl: i64 = tm_isqrt(rx*rx + rz*rz)
119 if rl < 40 { rx = TM_MAGIC_1024; ry = 0; rz = 0; rl = TM_MAGIC_1024 }
120 rx = rx*TM_MAGIC_1024/rl; rz = rz*TM_MAGIC_1024/rl
121 let ux: i64 = (TM_SUNY*rz - TM_SUNZ*ry)/TM_MAGIC_1024
122 let uy: i64 = (TM_SUNZ*rx - TM_SUNX*rz)/TM_MAGIC_1024
123 let uz: i64 = (TM_SUNX*ry - TM_SUNY*rx)/TM_MAGIC_1024
124 TM_SRX = rx; TM_SRY = ry; TM_SRZ = rz
125 TM_SUX = ux; TM_SUY = uy; TM_SUZ = uz
126 // project all verts to (a,b,d) sun space
127 let px: *i64 = TM_VX as *i64
128 let py: *i64 = TM_VY as *i64
129 let pz: *i64 = TM_VZ as *i64
130 let sa: *i64 = sys_mmap(TM_NV*8) as *i64
131 let sb: *i64 = sys_mmap(TM_NV*8) as *i64
132 let sd: *i64 = sys_mmap(TM_NV*8) as *i64
133 var amin: i64 = TM_MAGIC_1000000000
134 var amax: i64 = 0-TM_MAGIC_1000000000
135 var bmin: i64 = TM_MAGIC_1000000000
136 var bmax: i64 = 0-TM_MAGIC_1000000000
137 i = 0
138 while i < TM_NV {
139 let a: i64 = (px[i]*rx + py[i]*ry + pz[i]*rz)/TM_MAGIC_1024
140 let b: i64 = (px[i]*ux + py[i]*uy + pz[i]*uz)/TM_MAGIC_1024
141 let d: i64 = (px[i]*TM_SUNX + py[i]*TM_SUNY + pz[i]*TM_SUNZ)/TM_MAGIC_1024
142 sa[i] = a; sb[i] = b; sd[i] = d
143 if a < amin { amin = a } if a > amax { amax = a }
144 if b < bmin { bmin = b } if b > bmax { bmax = b }
145 i = i + 1
146 }
147 TM_SHA0 = amin
148 TM_SHB0 = bmin
149 TM_SHSA = amax - amin + 1
150 TM_SHSB = bmax - bmin + 1
151 // rasterize every triangle into the map, keeping MAX sun-depth (nearest blocker)
152 let TA: *i64 = TM_TA as *i64
153 let TB: *i64 = TM_TB as *i64
154 let TC: *i64 = TM_TC as *i64
155 var t: i64 = 0
156 while t < TM_NT {
157 let i0: i64 = TA[t]; let i1: i64 = TB[t]; let i2: i64 = TC[t]
158 let ax: i64 = (sa[i0]-amin)*TM_SHRES/TM_SHSA; let ay: i64 = (sb[i0]-bmin)*TM_SHRES/TM_SHSB
159 let bx: i64 = (sa[i1]-amin)*TM_SHRES/TM_SHSA; let by: i64 = (sb[i1]-bmin)*TM_SHRES/TM_SHSB
160 let cx: i64 = (sa[i2]-amin)*TM_SHRES/TM_SHSA; let cy: i64 = (sb[i2]-bmin)*TM_SHRES/TM_SHSB
161 let area: i64 = (bx-ax)*(cy-ay) - (by-ay)*(cx-ax)
162 if area != 0 {
163 var minx: i64 = tm_min3(ax,bx,cx); var maxx: i64 = tm_max3(ax,bx,cx)
164 var miny: i64 = tm_min3(ay,by,cy); var maxy: i64 = tm_max3(ay,by,cy)
165 if minx < 0 { minx = 0 } if miny < 0 { miny = 0 }
166 if maxx > TM_SHRES-1 { maxx = TM_SHRES-1 } if maxy > TM_SHRES-1 { maxy = TM_SHRES-1 }
167 var qy: i64 = miny
168 while qy <= maxy {
169 var qx: i64 = minx
170 while qx <= maxx {
171 let e0: i64 = (cx-bx)*(qy-by) - (cy-by)*(qx-bx)
172 let e1: i64 = (ax-cx)*(qy-cy) - (ay-cy)*(qx-cx)
173 let e2: i64 = (bx-ax)*(qy-ay) - (by-ay)*(qx-ax)
174 var inside: i64 = 0
175 if area > 0 { if e0>=0 { if e1>=0 { if e2>=0 { inside=1 } } } }
176 else { if e0<=0 { if e1<=0 { if e2<=0 { inside=1 } } } }
177 if inside == 1 {
178 let dd: i64 = (e0*sd[i0] + e1*sd[i1] + e2*sd[i2]) / area
179 let mi: i64 = qy*TM_SHRES + qx
180 if dd > sh[mi] { sh[mi] = dd }
181 }
182 qx = qx + 1
183 }
184 qy = qy + 1
185 }
186 }
187 t = t + 1
188 }
189 return 0
190}
191// shadow factor at a model-space point: 1024 = lit, else attenuated. Bias scales with texel size (acne guard).
192func tm_shadow_at(wx: i64, wy: i64, wz: i64) -> i64 {
193 let a: i64 = (wx*TM_SRX + wy*TM_SRY + wz*TM_SRZ)/TM_MAGIC_1024
194 let b: i64 = (wx*TM_SUX + wy*TM_SUY + wz*TM_SUZ)/TM_MAGIC_1024
195 let tx: i64 = (a - TM_SHA0)*TM_SHRES/TM_SHSA
196 let ty: i64 = (b - TM_SHB0)*TM_SHRES/TM_SHSB
197 if tx < 0 { return TM_MAGIC_1024 }
198 if ty < 0 { return TM_MAGIC_1024 }
199 if tx > TM_SHRES-1 { return TM_MAGIC_1024 }
200 if ty > TM_SHRES-1 { return TM_MAGIC_1024 }
201 let d: i64 = (wx*TM_SUNX + wy*TM_SUNY + wz*TM_SUNZ)/TM_MAGIC_1024
202 let sh: *i64 = TM_SH as *i64
203 var bias: i64 = TM_SHSA/TM_SHRES*3
204 if bias < 22 { bias = 22 }
205 if sh[ty*TM_SHRES + tx] > d + bias { return 340 }
206 return TM_MAGIC_1024
207}
208static TM_IMG: i64 // ★F3 IMAGE texture (packed w*h*3 RGB) sampled via per-vertex UV. 0 = off.
209static TM_IMGW: i64
210static TM_IMGH: i64
211func tm_set_image(rgb: *u8, w: i64, h: i64) -> i64 { TM_IMG = rgb as i64; TM_IMGW = w; TM_IMGH = h; return 0 }
212static TM_NRM: i64 // ★R4 NORMAL map (packed w*h*3, tangent-space, 128=zero). 0 = off.
213static TM_NRMW: i64
214static TM_NRMH: i64
215const TM_NMID: i64 = 128
216const TM_NSCL: i64 = 127
217func tm_set_normalmap(rgb: *u8, w: i64, h: i64) -> i64 { TM_NRM = rgb as i64; TM_NRMW = w; TM_NRMH = h; return 0 }
218// Build a deterministic orthonormal basis from a unit normal and rotate a tangent-space normal into it.
219// Method: Duff et al. 2017, "Building an Orthonormal Basis, Revisited" -- branchless, and well conditioned
220// EVERYWHERE because the sign trick keeps the denominator in [1,2] instead of collapsing near the pole.
221// ★WHY A UV-FREE BASIS IS CORRECT HERE, and where it would not be: our relief is ISOTROPIC band-limited
222// noise, and an isotropic detail normal has no preferred tangent direction, so any consistent basis gives
223// a statistically identical result. An ANISOTROPIC or authored map (real skin follows Langer's lines)
224// WOULD need UV-aligned tangents from mesh derivatives. That is a named rung, not an oversight.
225// All vectors are fixed-point with TM_MAGIC_4096 representing 1.0.
226func tm_perturb_normal(nx: i64, ny: i64, nz: i64, tx: i64, ty: i64, tz: i64, out3: *i64) -> i64 {
227 let S: i64 = TM_MAGIC_4096
228 var nl: i64 = tm_isqrt(nx*nx + ny*ny + nz*nz)
229 if nl < 1 { nl = 1 }
230 let ux: i64 = nx*S/nl
231 let uy: i64 = ny*S/nl
232 let uz: i64 = nz*S/nl
233 var sg: i64 = 1
234 if uz < 0 { sg = 0 - 1 }
235 let den: i64 = sg*S + uz
236 if den == 0 { out3[0] = ux; out3[1] = uy; out3[2] = uz; return 0 }
237 let aa: i64 = 0 - (S*S)/den
238 let bb: i64 = ux*uy/S*aa/S
239 let t0: i64 = S + sg*ux/S*ux*aa/S
240 let t1: i64 = sg*bb
241 let t2: i64 = 0 - sg*ux
242 let b0: i64 = bb
243 let b1: i64 = sg*S + uy*uy/S*aa/S
244 let b2: i64 = 0 - uy
245 let px: i64 = (tx*t0 + ty*b0 + tz*ux)/S
246 let py: i64 = (tx*t1 + ty*b1 + tz*uy)/S
247 let pz: i64 = (tx*t2 + ty*b2 + tz*uz)/S
248 var pl: i64 = tm_isqrt(px*px + py*py + pz*pz)
249 if pl < 1 { pl = 1 }
250 out3[0] = px*S/pl
251 out3[1] = py*S/pl
252 out3[2] = pz*S/pl
253 return 0
254}
255static TM_VU: i64 // per-vertex UV, Q16 (65536 = 1.0)
256static TM_VV: i64
257func tm_vuv(idx: i64, u: i64, v: i64) -> i64 { let pu: *i64 = TM_VU as *i64; let pv: *i64 = TM_VV as *i64; pu[idx] = u; pv[idx] = v; return 0 }
258// ★DELEGATES to nx_relief_lib, the ONE OWNER of this field since 2026-08-25. The body moved VERBATIM,
259// so every caller -- the texture baker, nx_worldpipe, the engine gates -- is behaviourally identical BY
260// CONSTRUCTION, not by a second copy that happens to agree today.
261func tm_hash3(a: i64, b: i64, c: i64) -> i64 { return rlf_hash3(a, b, c) }
262// trilinear value noise at 3D position, lattice scale S. Returns 0..1023 (smooth). Offset keeps floor-division positive.
263// ★DELEGATES to nx_relief_lib (ONE OWNER, 2026-08-25). Body moved VERBATIM.
264func tm_noise3(x: i64, y: i64, z: i64, S: i64) -> i64 { return rlf_noise3(x, y, z, S) }
265
266func tm_isqrt(v: i64) -> i64 { return vm_isqrt(v) }
267func tm_min(a: i64, b: i64) -> i64 { if a<b {return a} return b }
268func tm_max(a: i64, b: i64) -> i64 { if a>b {return a} return b }
269func tm_min3(a: i64, b: i64, c: i64) -> i64 { return tm_min(tm_min(a,b),c) }
270func tm_max3(a: i64, b: i64, c: i64) -> i64 { return tm_max(tm_max(a,b),c) }
271
272func tm_reset() -> i64 {
273 if TM_VX == 0 {
274 TM_VX = sys_mmap(TM_VCAP*8) as i64; TM_VY = sys_mmap(TM_VCAP*8) as i64; TM_VZ = sys_mmap(TM_VCAP*8) as i64
275 TM_TA = sys_mmap(TM_TCAP*8) as i64; TM_TB = sys_mmap(TM_TCAP*8) as i64; TM_TC = sys_mmap(TM_TCAP*8) as i64; TM_TCOL = sys_mmap(TM_TCAP*8) as i64
276 TM_VNX = sys_mmap(TM_VCAP*8) as i64; TM_VNY = sys_mmap(TM_VCAP*8) as i64; TM_VNZ = sys_mmap(TM_VCAP*8) as i64
277 TM_VCOL = sys_mmap(TM_VCAP*8) as i64
278 TM_VU = sys_mmap(TM_VCAP*8) as i64; TM_VV = sys_mmap(TM_VCAP*8) as i64
279 }
280 TM_VCAPN = TM_VCAP; TM_TCAPN = TM_TCAP
281 TM_NV = 0; TM_NT = 0; TM_OVF = 0
282 return 0
283}
284// ★ARENA BINDING -- THE SAME RASTERIZER, NO SYSCALLS. Points the 13 arrays at consecutive slices of
285// memory the CALLER already owns, so a wasm module (base+offset, no mmap) or any memory-bounded
286// target can drive trimesh_render UNCHANGED. That function already takes fb and zbuf as PARAMETERS,
287// so vertex STORAGE was the only thing tying this organ to Linux -- binding it is all that was
288// missing. ONE rasterizer with two allocation strategies, never a second copy to drift.
289// Layout: 9 vertex arrays then 4 triangle arrays, 8-byte words, in exactly the order below.
290// Size the arena with tm_bind_bytes(nv, nt) -- do not hand-compute it at the call site.
291func tm_bind_bytes(nv: i64, nt: i64) -> i64 { return nv*8*9 + nt*8*4 }
292func tm_bind(mem: i64, nv: i64, nt: i64) -> i64 {
293 if mem == 0 { return 3 }
294 if nv <= 0 { return 3 }
295 if nt <= 0 { return 3 }
296 var o: i64 = mem
297 TM_VX = o; o = o + nv*8
298 TM_VY = o; o = o + nv*8
299 TM_VZ = o; o = o + nv*8
300 TM_VNX = o; o = o + nv*8
301 TM_VNY = o; o = o + nv*8
302 TM_VNZ = o; o = o + nv*8
303 TM_VCOL = o; o = o + nv*8
304 TM_VU = o; o = o + nv*8
305 TM_VV = o; o = o + nv*8
306 TM_TA = o; o = o + nt*8
307 TM_TB = o; o = o + nt*8
308 TM_TC = o; o = o + nt*8
309 TM_TCOL = o; o = o + nt*8
310 TM_VCAPN = nv
311 TM_TCAPN = nt
312 TM_NV = 0; TM_NT = 0; TM_OVF = 0
313 return 0
314}
315func tm_vcol(idx: i64, col: i64) -> i64 { let p: *i64 = TM_VCOL as *i64; p[idx] = col; return 0 }
316// NOTE: declare the pointer up front -- inline-cast-index READ `(TM_VX as *i64)[i]` miscompiles to the base pointer.
317func tm_vpos(idx: i64, out: *i64) -> i64 { let px: *i64=TM_VX as *i64; let py: *i64=TM_VY as *i64; let pz: *i64=TM_VZ as *i64; out[0]=px[idx]; out[1]=py[idx]; out[2]=pz[idx]; return 0 }
318func tm_vnorm(idx: i64, out: *i64) -> i64 { let px: *i64=TM_VNX as *i64; let py: *i64=TM_VNY as *i64; let pz: *i64=TM_VNZ as *i64; out[0]=px[idx]; out[1]=py[idx]; out[2]=pz[idx]; return 0 }
319func tm_getvcol(idx: i64) -> i64 { let p: *i64=TM_VCOL as *i64; return p[idx] }
320// ★read-only accessors so a caller can EXPORT what surface_nets built (e.g. write NXMSH2 and then look
321// at it). Without these the polygonizer could fill these buffers and nothing could get the geometry
322// back out, which is how meshes got produced that nobody could view.
323func tm_vx(i: i64) -> i64 { let p: *i64=TM_VX as *i64; return p[i] }
324func tm_vy(i: i64) -> i64 { let p: *i64=TM_VY as *i64; return p[i] }
325func tm_vz(i: i64) -> i64 { let p: *i64=TM_VZ as *i64; return p[i] }
326func tm_vnx(i: i64) -> i64 { let p: *i64=TM_VNX as *i64; return p[i] }
327func tm_vny(i: i64) -> i64 { let p: *i64=TM_VNY as *i64; return p[i] }
328func tm_vnz(i: i64) -> i64 { let p: *i64=TM_VNZ as *i64; return p[i] }
329func tm_ta(i: i64) -> i64 { let p: *i64=TM_TA as *i64; return p[i] }
330func tm_tb(i: i64) -> i64 { let p: *i64=TM_TB as *i64; return p[i] }
331func tm_tc(i: i64) -> i64 { let p: *i64=TM_TC as *i64; return p[i] }
332func tm_tget(t: i64, out: *i64) -> i64 { let pa: *i64=TM_TA as *i64; let pb: *i64=TM_TB as *i64; let pc: *i64=TM_TC as *i64; out[0]=pa[t]; out[1]=pb[t]; out[2]=pc[t]; return 0 }
333// translate (and optionally uniform-scale, mil=1000 -> 1.0) vertices [from,to) -- scene placement of a loaded asset
334func tm_place(from: i64, to: i64, dx: i64, dy: i64, dz: i64, mil: i64) -> i64 {
335 let px: *i64 = TM_VX as *i64; let py: *i64 = TM_VY as *i64; let pz: *i64 = TM_VZ as *i64
336 var i: i64 = from
337 while i < to {
338 px[i] = px[i]*mil/1000 + dx
339 py[i] = py[i]*mil/1000 + dy
340 pz[i] = pz[i]*mil/1000 + dz
341 i = i + 1
342 }
343 return 0
344}
345// ★F3: compute per-vertex normals = normalized sum of adjacent FACE normals (model space). Enables Gouraud smooth shading.
346func tm_compute_normals() -> i64 {
347 let nx: *i64 = TM_VNX as *i64; let ny: *i64 = TM_VNY as *i64; let nz: *i64 = TM_VNZ as *i64
348 let vx: *i64 = TM_VX as *i64; let vy: *i64 = TM_VY as *i64; let vz: *i64 = TM_VZ as *i64
349 var i: i64 = 0
350 while i < TM_NV { nx[i]=0; ny[i]=0; nz[i]=0; i=i+1 }
351 let TA: *i64 = TM_TA as *i64; let TB: *i64 = TM_TB as *i64; let TC: *i64 = TM_TC as *i64
352 var t: i64 = 0
353 while t < TM_NT {
354 let a: i64 = TA[t]; let b: i64 = TB[t]; let c: i64 = TC[t]
355 let ux: i64 = vx[b]-vx[a]; let uy: i64 = vy[b]-vy[a]; let uz: i64 = vz[b]-vz[a]
356 let wx: i64 = vx[c]-vx[a]; let wy: i64 = vy[c]-vy[a]; let wz: i64 = vz[c]-vz[a]
357 var fnx: i64 = uy*wz-uz*wy; var fny: i64 = uz*wx-ux*wz; var fnz: i64 = ux*wy-uy*wx
358 let fl: i64 = tm_isqrt(fnx*fnx+fny*fny+fnz*fnz) + 1 // unit-ish (256) so big tris don't dominate the average
359 fnx = fnx*256/fl; fny = fny*256/fl; fnz = fnz*256/fl
360 nx[a]=nx[a]+fnx; ny[a]=ny[a]+fny; nz[a]=nz[a]+fnz
361 nx[b]=nx[b]+fnx; ny[b]=ny[b]+fny; nz[b]=nz[b]+fnz
362 nx[c]=nx[c]+fnx; ny[c]=ny[c]+fny; nz[c]=nz[c]+fnz
363 t = t + 1
364 }
365 i = 0
366 while i < TM_NV {
367 let l: i64 = tm_isqrt(nx[i]*nx[i]+ny[i]*ny[i]+nz[i]*nz[i]) + 1
368 nx[i]=nx[i]*TM_MAGIC_4096/l; ny[i]=ny[i]*TM_MAGIC_4096/l; nz[i]=nz[i]*TM_MAGIC_4096/l // unit normal fx4096
369 i = i + 1
370 }
371 return 0
372}
373func tm_vert(x: i64, y: i64, z: i64) -> i64 {
374 if TM_NV >= tm_vcap() { TM_OVF = 1; return tm_vcap()-1 } // capacity guard: never write OOB
375 let i: i64 = TM_NV
376 let px: *i64 = TM_VX as *i64; let py: *i64 = TM_VY as *i64; let pz: *i64 = TM_VZ as *i64
377 px[i] = x; py[i] = y; pz[i] = z
378 TM_NV = TM_NV + 1
379 return i
380}
381// add a vertex WITH an explicit per-vertex normal (for isosurface: SDF-gradient normals, winding-independent smooth shading)
382func tm_vert_n(x: i64, y: i64, z: i64, nx: i64, ny: i64, nz: i64) -> i64 {
383 if TM_NV >= tm_vcap() { TM_OVF = 1; return tm_vcap()-1 } // capacity guard
384 let i: i64 = TM_NV
385 let px: *i64 = TM_VX as *i64; let py: *i64 = TM_VY as *i64; let pz: *i64 = TM_VZ as *i64
386 let qx: *i64 = TM_VNX as *i64; let qy: *i64 = TM_VNY as *i64; let qz: *i64 = TM_VNZ as *i64
387 px[i]=x; py[i]=y; pz[i]=z; qx[i]=nx; qy[i]=ny; qz[i]=nz
388 TM_NV = TM_NV + 1
389 return i
390}
391func tm_tri(a: i64, b: i64, c: i64, col: i64) -> i64 {
392 if TM_NT >= tm_tcap() { TM_OVF = 1; return tm_tcap()-1 } // capacity guard
393 let i: i64 = TM_NT
394 let pa: *i64 = TM_TA as *i64; let pb: *i64 = TM_TB as *i64; let pc: *i64 = TM_TC as *i64; let pcol: *i64 = TM_TCOL as *i64
395 pa[i] = a; pb[i] = b; pc[i] = c; pcol[i] = col
396 TM_NT = TM_NT + 1
397 return i
398}
399func tm_quad(a: i64, b: i64, c: i64, d: i64, col: i64) -> i64 { tm_tri(a,b,c,col); tm_tri(a,c,d,col); return 0 }
400func tm_nt() -> i64 { return TM_NT }
401func tm_nv() -> i64 { return TM_NV }
402
403// ---- primitive mesh builders (append at center cx,cy,cz, half-size s, colour col) ----
404func tm_cube(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 {
405 let b: i64 = TM_NV
406 tm_vert(cx-s,cy-s,cz-s); tm_vert(cx+s,cy-s,cz-s); tm_vert(cx+s,cy+s,cz-s); tm_vert(cx-s,cy+s,cz-s) // back z-
407 tm_vert(cx-s,cy-s,cz+s); tm_vert(cx+s,cy-s,cz+s); tm_vert(cx+s,cy+s,cz+s); tm_vert(cx-s,cy+s,cz+s) // front z+
408 tm_quad(b+4,b+5,b+6,b+7,col) // front
409 tm_quad(b+1,b+0,b+3,b+2,col) // back
410 tm_quad(b+0,b+4,b+7,b+3,col) // left
411 tm_quad(b+5,b+1,b+2,b+6,col) // right
412 tm_quad(b+3,b+7,b+6,b+2,col) // top
413 tm_quad(b+0,b+1,b+5,b+4,col) // bottom
414 return 0
415}
416func tm_tetra(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 {
417 let b: i64 = TM_NV
418 tm_vert(cx+s,cy+s,cz+s); tm_vert(cx+s,cy-s,cz-s); tm_vert(cx-s,cy+s,cz-s); tm_vert(cx-s,cy-s,cz+s) // alternating cube corners = regular tetra
419 tm_tri(b+0,b+1,b+2,col); tm_tri(b+0,b+3,b+1,col); tm_tri(b+0,b+2,b+3,col); tm_tri(b+1,b+3,b+2,col)
420 return 0
421}
422func tm_octa(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 {
423 let b: i64 = TM_NV
424 tm_vert(cx+s,cy,cz); tm_vert(cx-s,cy,cz); tm_vert(cx,cy+s,cz); tm_vert(cx,cy-s,cz); tm_vert(cx,cy,cz+s); tm_vert(cx,cy,cz-s) // +x -x +y -y +z -z
425 tm_tri(b+2,b+0,b+4,col); tm_tri(b+2,b+4,b+1,col); tm_tri(b+2,b+1,b+5,col); tm_tri(b+2,b+5,b+0,col) // top 4
426 tm_tri(b+3,b+4,b+0,col); tm_tri(b+3,b+1,b+4,col); tm_tri(b+3,b+5,b+1,col); tm_tri(b+3,b+0,b+5,col) // bottom 4
427 return 0
428}
429func tm_pyramid(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 {
430 let b: i64 = TM_NV
431 tm_vert(cx-s,cy-s,cz-s); tm_vert(cx+s,cy-s,cz-s); tm_vert(cx+s,cy-s,cz+s); tm_vert(cx-s,cy-s,cz+s) // square base
432 tm_vert(cx,cy+s,cz) // apex
433 tm_tri(b+0,b+1,b+4,col); tm_tri(b+1,b+2,b+4,col); tm_tri(b+2,b+3,b+4,col); tm_tri(b+3,b+0,b+4,col) // 4 sides
434 tm_quad(b+0,b+3,b+2,b+1,col) // base
435 return 0
436}
437func tm_prism(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 { // triangular prism (extruded triangle along z)
438 let b: i64 = TM_NV
439 tm_vert(cx-s,cy-s,cz-s); tm_vert(cx+s,cy-s,cz-s); tm_vert(cx,cy+s,cz-s) // back triangle
440 tm_vert(cx-s,cy-s,cz+s); tm_vert(cx+s,cy-s,cz+s); tm_vert(cx,cy+s,cz+s) // front triangle
441 tm_tri(b+3,b+4,b+5,col); tm_tri(b+1,b+0,b+2,col) // caps
442 tm_quad(b+0,b+1,b+4,b+3,col); tm_quad(b+1,b+2,b+5,b+4,col); tm_quad(b+2,b+0,b+3,b+5,col) // 3 rects
443 return 0
444}
445// icosahedron (12 verts via golden ratio phi~1.618, integer *1000). 20 triangular faces -- the iconic polyhedron.
446func tm_icosa(cx: i64, cy: i64, cz: i64, s: i64, col: i64) -> i64 {
447 let b: i64 = TM_NV
448 let p: i64 = s*TM_MAGIC_1618/1000
449 tm_vert(cx,cy+s,cz+p); tm_vert(cx,cy-s,cz+p); tm_vert(cx,cy+s,cz-p); tm_vert(cx,cy-s,cz-p) // 0..3 (0,±1,±phi)
450 tm_vert(cx+s,cy+p,cz); tm_vert(cx-s,cy+p,cz); tm_vert(cx+s,cy-p,cz); tm_vert(cx-s,cy-p,cz) // 4..7 (±1,±phi,0)
451 tm_vert(cx+p,cy,cz+s); tm_vert(cx-p,cy,cz+s); tm_vert(cx+p,cy,cz-s); tm_vert(cx-p,cy,cz-s) // 8..11 (±phi,0,±1)
452 tm_tri(b+0,b+1,b+8,col); tm_tri(b+0,b+8,b+4,col); tm_tri(b+0,b+4,b+5,col); tm_tri(b+0,b+5,b+9,col); tm_tri(b+0,b+9,b+1,col)
453 tm_tri(b+1,b+6,b+8,col); tm_tri(b+8,b+6,b+10,col); tm_tri(b+8,b+10,b+4,col); tm_tri(b+4,b+10,b+2,col); tm_tri(b+4,b+2,b+5,col)
454 tm_tri(b+5,b+2,b+11,col); tm_tri(b+5,b+11,b+9,col); tm_tri(b+9,b+11,b+7,col); tm_tri(b+9,b+7,b+1,col); tm_tri(b+1,b+7,b+6,col)
455 tm_tri(b+3,b+6,b+7,col); tm_tri(b+3,b+7,b+11,col); tm_tri(b+3,b+11,b+2,col); tm_tri(b+3,b+2,b+10,col); tm_tri(b+3,b+10,b+6,col)
456 return 0
457}
458// triangulated UV-sphere (rings x segs) -- shows even a ROUND shape is now real triangles, not an SDF blob.
459func tm_uvsphere(cx: i64, cy: i64, cz: i64, s: i64, rings: i64, segs: i64, col: i64) -> i64 {
460 let b: i64 = TM_NV
461 var r: i64 = 0
462 while r <= rings {
463 let theta: i64 = IT_PI * r / rings // 0..PI (lat)
464 let st: i64 = it_sin4096(theta); let ct: i64 = it_cos4096(theta)
465 var sg: i64 = 0
466 while sg <= segs {
467 let phi: i64 = 2*IT_PI * sg / segs // 0..2PI (lon)
468 let sp: i64 = it_sin4096(phi); let cp: i64 = it_cos4096(phi)
469 let x: i64 = cx + s*st/TM_MAGIC_4096*cp/TM_MAGIC_4096
470 let y: i64 = cy + s*ct/TM_MAGIC_4096
471 let z: i64 = cz + s*st/TM_MAGIC_4096*sp/TM_MAGIC_4096
472 tm_vert(x,y,z)
473 sg = sg + 1
474 }
475 r = r + 1
476 }
477 let w: i64 = segs+1
478 r = 0
479 while r < rings {
480 var sg: i64 = 0
481 while sg < segs {
482 let v0: i64 = b + r*w + sg
483 let v1: i64 = b + r*w + sg+1
484 let v2: i64 = b + (r+1)*w + sg
485 let v3: i64 = b + (r+1)*w + sg+1
486 tm_quad(v0,v1,v3,v2,col)
487 sg = sg + 1
488 }
489 r = r + 1
490 }
491 return 0
492}
493// ★compose a RECOGNIZABLE thing from primitives: a low-poly humanoid (boxes) -- the shape kernel applied to a figure,
494// the angular counterpart to the ellipsoid being. Proves we can build real things from triangles.
495func tm_figure(cx: i64, cy: i64, cz: i64, col: i64) -> i64 {
496 let skin: i64 = 214 + 170*256 + 150*TM_MAGIC_65536
497 tm_cube(cx, cy+300, cz, 78, skin) // head
498 tm_cube(cx, cy+120, cz, 96, col) // torso (taller via two stacked -> keep one for low-poly)
499 tm_cube(cx, cy-10, cz, 88, col) // lower torso
500 tm_cube(cx-150, cy+150, cz, 42, skin) // L upper arm
501 tm_cube(cx+150, cy+150, cz, 42, skin) // R upper arm
502 tm_cube(cx-150, cy-30, cz, 40, skin) // L forearm
503 tm_cube(cx+150, cy-30, cz, 40, skin) // R forearm
504 tm_cube(cx-56, cy-230, cz, 52, col) // L thigh
505 tm_cube(cx+56, cy-230, cz, 52, col) // R thigh
506 tm_cube(cx-56, cy-410, cz, 48, col) // L shin
507 tm_cube(cx+56, cy-410, cz, 48, col) // R shin
508 return 0
509}
510
511// ---- the rasterizer: transform -> project -> barycentric fill + Z-BUFFER + flat per-face shading ----
512// fb, zbuf sized W*H. yaw/pitch it4096. camz shifts model into +Z. Background left as-is by caller.
513func trimesh_render(fb: *i64, zbuf: *i64, W: i64, H: i64, yaw: i64, pitch: i64, camz: i64, focal: i64, smooth: i64) -> i64 {
514 let cyw: i64 = it_cos4096(yaw); let syw: i64 = it_sin4096(yaw)
515 let cpt: i64 = it_cos4096(pitch); let spt: i64 = it_sin4096(pitch)
516 // light dir (view space), upper-left-front
517 let lx: i64 = 0-TM_MAGIC_1300; let ly: i64 = TM_MAGIC_2400; let lz: i64 = 0-TM_MAGIC_3000
518 let llen: i64 = tm_isqrt(lx*lx+ly*ly+lz*lz) + 1
519 // per-vertex projected screen + view-space
520 let SX: *i64 = sys_mmap(TM_NV*8) as *i64
521 let SY: *i64 = sys_mmap(TM_NV*8) as *i64
522 let VX: *i64 = sys_mmap(TM_NV*8) as *i64
523 let VY: *i64 = sys_mmap(TM_NV*8) as *i64
524 let VZ: *i64 = sys_mmap(TM_NV*8) as *i64
525 let mvx: *i64 = TM_VX as *i64; let mvy: *i64 = TM_VY as *i64; let mvz: *i64 = TM_VZ as *i64
526 var i: i64 = 0
527 while i < TM_NV {
528 let x: i64 = mvx[i]; let y: i64 = mvy[i]; let z: i64 = mvz[i]
529 let x1: i64 = (x*cyw + z*syw)/TM_MAGIC_4096
530 let z1: i64 = (0-x*syw + z*cyw)/TM_MAGIC_4096
531 let y2: i64 = (y*cpt - z1*spt)/TM_MAGIC_4096
532 let z2: i64 = (y*spt + z1*cpt)/TM_MAGIC_4096
533 let vz: i64 = z2 + camz
534 VX[i] = x1; VY[i] = y2; VZ[i] = vz
535 if vz > 20 { SX[i] = W/2 + focal*x1/vz; SY[i] = H/2 - focal*y2/vz }
536 else { SX[i] = 0-TM_MAGIC_100000; SY[i] = 0-TM_MAGIC_100000 }
537 i = i + 1
538 }
539 // ★F3 GOURAUD: per-vertex Lambert (rotate model normal into view space, dot with light). fx1024, 0..1024.
540 // ★F3 BLINN-PHONG: per-vertex specular = (N.H)^16 * TM_SPEC, H = halfway(light, view); view dir toward camera = -z.
541 let LAMV: *i64 = sys_mmap(TM_NV*8) as *i64
542 let SPECV: *i64 = sys_mmap(TM_NV*8) as *i64
543 // view-space per-vertex normals, kept so the fragment stage can interpolate a REAL normal for
544 // normal mapping. Allocated once per render, never inside the pixel loop.
545 let NVXV: *i64 = sys_mmap(TM_NV*8) as *i64
546 let NVYV: *i64 = sys_mmap(TM_NV*8) as *i64
547 let NVZV: *i64 = sys_mmap(TM_NV*8) as *i64
548 let PN3: *i64 = sys_mmap(3*8) as *i64
549 let hx: i64 = lx*TM_MAGIC_1024/llen
550 let hy: i64 = ly*TM_MAGIC_1024/llen
551 let hz: i64 = lz*TM_MAGIC_1024/llen - TM_MAGIC_1024 // + Vhat=(0,0,-1)
552 let hlen: i64 = tm_isqrt(hx*hx + hy*hy + hz*hz) + 1
553 if smooth >= 1 { // needed for BOTH smooth==1 (Gouraud) and smooth==2 (per-vertex colour)
554 let mnx: *i64 = TM_VNX as *i64; let mny: *i64 = TM_VNY as *i64; let mnz: *i64 = TM_VNZ as *i64
555 var q: i64 = 0
556 while q < TM_NV {
557 let mx: i64 = mnx[q]; let my: i64 = mny[q]; let mz: i64 = mnz[q]
558 let nx1: i64 = (mx*cyw + mz*syw)/TM_MAGIC_4096
559 let nz1: i64 = (0-mx*syw + mz*cyw)/TM_MAGIC_4096
560 let nvy: i64 = (my*cpt - nz1*spt)/TM_MAGIC_4096
561 let nvz: i64 = (my*spt + nz1*cpt)/TM_MAGIC_4096
562 let nvx: i64 = nx1
563 var lam: i64 = (nvx*lx + nvy*ly + nvz*lz) / (4*llen) // |n|=TM_MAGIC_4096 -> /(TM_MAGIC_4096*llen/TM_MAGIC_1024)=/(4*llen)
564 if lam < 0 { lam = 0 }
565 if lam > TM_MAGIC_1024 { lam = TM_MAGIC_1024 }
566 NVXV[q] = nvx
567 NVYV[q] = nvy
568 NVZV[q] = nvz
569 LAMV[q] = lam
570 var sv: i64 = 0
571 if TM_SPEC > 0 {
572 var sc: i64 = (nvx*hx + nvy*hy + nvz*hz) / (4*hlen) // N.H, fx1024 (|N|=TM_MAGIC_4096)
573 if sc < 0 { sc = 0 }
574 if sc > TM_MAGIC_1024 { sc = TM_MAGIC_1024 }
575 let c2: i64 = sc*sc/TM_MAGIC_1024; let c4: i64 = c2*c2/TM_MAGIC_1024; let c8: i64 = c4*c4/TM_MAGIC_1024; let c16: i64 = c8*c8/TM_MAGIC_1024
576 sv = TM_SPEC*c16/TM_MAGIC_1024
577 }
578 SPECV[q] = sv
579 q = q + 1
580 }
581 }
582 let TA: *i64 = TM_TA as *i64; let TB: *i64 = TM_TB as *i64; let TC: *i64 = TM_TC as *i64; let TCOL: *i64 = TM_TCOL as *i64
583 let VCOL: *i64 = TM_VCOL as *i64
584 var t: i64 = 0
585 while t < TM_NT {
586 let i0: i64 = TA[t]; let i1: i64 = TB[t]; let i2: i64 = TC[t]
587 let ax: i64 = SX[i0]; let ay: i64 = SY[i0]
588 let bx: i64 = SX[i1]; let by: i64 = SY[i1]
589 let cx: i64 = SX[i2]; let cy: i64 = SY[i2]
590 var skip: i64 = 0
591 if ax < 0-TM_MAGIC_90000 { skip = 1 } if bx < 0-TM_MAGIC_90000 { skip = 1 } if cx < 0-TM_MAGIC_90000 { skip = 1 }
592 let area: i64 = (bx-ax)*(cy-ay) - (by-ay)*(cx-ax)
593 if area == 0 { skip = 1 }
594 if skip == 0 {
595 // face normal (view space), oriented toward the camera (-Z)
596 let ux: i64 = VX[i1]-VX[i0]; let uy: i64 = VY[i1]-VY[i0]; let uz: i64 = VZ[i1]-VZ[i0]
597 let wx: i64 = VX[i2]-VX[i0]; let wy: i64 = VY[i2]-VY[i0]; let wz: i64 = VZ[i2]-VZ[i0]
598 var nx: i64 = uy*wz - uz*wy; var ny: i64 = uz*wx - ux*wz; var nz: i64 = ux*wy - uy*wx
599 if nz > 0 { nx = 0-nx; ny = 0-ny; nz = 0-nz }
600 let nl: i64 = tm_isqrt(nx*nx+ny*ny+nz*nz) + 1
601 var lam: i64 = (nx*lx + ny*ly + nz*lz) / (nl*llen/TM_MAGIC_1024)
602 if lam < 0 { lam = 0 }
603 let col: i64 = TCOL[t]
604 let br: i64 = col&255; let bg: i64 = (col>>8)&255; let bb: i64 = (col>>16)&255
605 let sh: i64 = 42 + 86*lam/TM_MAGIC_1024
606 var cr: i64 = br*sh/128; var cg: i64 = bg*sh/128; var cb: i64 = bb*sh/128
607 if cr>255 {cr=255} if cg>255 {cg=255} if cb>255 {cb=255}
608 let fcol: i64 = cr + cg*256 + cb*TM_MAGIC_65536
609 // per-vertex colours (smooth==2 multi-material meshes)
610 let k0: i64 = VCOL[i0]; let k1: i64 = VCOL[i1]; let k2: i64 = VCOL[i2]
611 let k0r: i64 = k0&255; let k0g: i64 = (k0>>8)&255; let k0b: i64 = (k0>>16)&255
612 let k1r: i64 = k1&255; let k1g: i64 = (k1>>8)&255; let k1b: i64 = (k1>>16)&255
613 let k2r: i64 = k2&255; let k2g: i64 = (k2>>8)&255; let k2b: i64 = (k2>>16)&255
614 // per-vertex UVs (Q16) for image-texture sampling
615 let TVU: *i64 = TM_VU as *i64
616 let TVV: *i64 = TM_VV as *i64
617 let u0: i64 = TVU[i0]; let u1: i64 = TVU[i1]; let u2: i64 = TVU[i2]
618 let w0: i64 = TVV[i0]; let w1: i64 = TVV[i1]; let w2: i64 = TVV[i2]
619 // bbox (clamped)
620 var minx: i64 = tm_min3(ax,bx,cx); var maxx: i64 = tm_max3(ax,bx,cx)
621 var miny: i64 = tm_min3(ay,by,cy); var maxy: i64 = tm_max3(ay,by,cy)
622 if minx < 0 { minx = 0 } if miny < 0 { miny = 0 }
623 if maxx > W-1 { maxx = W-1 } if maxy > H-1 { maxy = H-1 }
624 var py: i64 = miny
625 while py <= maxy {
626 var px: i64 = minx
627 while px <= maxx {
628 let e0: i64 = (cx-bx)*(py-by) - (cy-by)*(px-bx) // weight of vertex i0
629 let e1: i64 = (ax-cx)*(py-cy) - (ay-cy)*(px-cx) // weight of i1
630 let e2: i64 = (bx-ax)*(py-ay) - (by-ay)*(px-ax) // weight of i2
631 var inside: i64 = 0
632 if area > 0 { if e0>=0 { if e1>=0 { if e2>=0 { inside=1 } } } }
633 else { if e0<=0 { if e1<=0 { if e2<=0 { inside=1 } } } }
634 if inside == 1 {
635 let zz: i64 = (e0*VZ[i0] + e1*VZ[i1] + e2*VZ[i2]) / area // interpolated view depth
636 let idx: i64 = py*W + px
637 if zz < zbuf[idx] {
638 zbuf[idx] = zz
639 if smooth == 1 {
640 var l2: i64 = (e0*LAMV[i0] + e1*LAMV[i1] + e2*LAMV[i2]) / area // interpolated (Gouraud)
641 if l2 < 0 { l2 = 0 }
642 var sp2: i64 = (e0*SPECV[i0] + e1*SPECV[i1] + e2*SPECV[i2]) / area // specular highlight
643 if sp2 < 0 { sp2 = 0 }
644 if TM_SHON == 1 { // ★Q2 sun shadow
645 let wx2: i64 = (e0*mvx[i0] + e1*mvx[i1] + e2*mvx[i2]) / area
646 let wy2: i64 = (e0*mvy[i0] + e1*mvy[i1] + e2*mvy[i2]) / area
647 let wz2: i64 = (e0*mvz[i0] + e1*mvz[i1] + e2*mvz[i2]) / area
648 let sf2: i64 = tm_shadow_at(wx2, wy2, wz2)
649 l2 = l2*sf2/TM_MAGIC_1024
650 sp2 = sp2*sf2/TM_MAGIC_1024
651 }
652 let s2: i64 = 42 + 86*l2/TM_MAGIC_1024
653 var r2: i64 = br*s2/128 + sp2; var g2: i64 = bg*s2/128 + sp2; var b2: i64 = bb*s2/128 + sp2
654 if r2>255 {r2=255} if g2>255 {g2=255} if b2>255 {b2=255}
655 fb[idx] = r2 + g2*256 + b2*TM_MAGIC_65536
656 } else { if smooth == 2 {
657 var l3: i64 = (e0*LAMV[i0] + e1*LAMV[i1] + e2*LAMV[i2]) / area // Gouraud shade
658 if l3 < 0 { l3 = 0 }
659 var sp3: i64 = (e0*SPECV[i0] + e1*SPECV[i1] + e2*SPECV[i2]) / area
660 if sp3 < 0 { sp3 = 0 }
661 var mr: i64 = (e0*k0r + e1*k1r + e2*k2r) / area // interpolated per-vertex colour
662 var mg: i64 = (e0*k0g + e1*k1g + e2*k2g) / area
663 var mb: i64 = (e0*k0b + e1*k1b + e2*k2b) / area
664 if TM_IMG != 0 { // ★IMAGE texture via interpolated UV (REPEAT wrap)
665 var iu: i64 = ((e0*u0 + e1*u1 + e2*u2) / area) & TM_MAGIC_65535
666 var iv: i64 = ((e0*w0 + e1*w1 + e2*w2) / area) & TM_MAGIC_65535
667 let tx: i64 = iu*TM_IMGW/TM_MAGIC_65536
668 let ty: i64 = iv*TM_IMGH/TM_MAGIC_65536
669 let timg: *u8 = TM_IMG as *u8
670 let toff: i64 = (ty*TM_IMGW + tx)*3
671 mr = timg[toff] as i64
672 mg = timg[toff+1] as i64
673 mb = timg[toff+2] as i64
674 }
675 if TM_NRM != 0 { // ★R4 NORMAL MAP: real per-fragment normal
676 let ju: i64 = ((e0*u0 + e1*u1 + e2*u2) / area) & TM_MAGIC_65535
677 let jv: i64 = ((e0*w0 + e1*w1 + e2*w2) / area) & TM_MAGIC_65535
678 let nsx: i64 = ju*TM_NRMW/TM_MAGIC_65536
679 let nsy: i64 = jv*TM_NRMH/TM_MAGIC_65536
680 let nimg: *u8 = TM_NRM as *u8
681 let noff: i64 = (nsy*TM_NRMW + nsx)*3
682 let etx: i64 = ((nimg[noff] as i64) - TM_NMID)*TM_MAGIC_4096/TM_NSCL
683 let ety: i64 = ((nimg[noff+1] as i64) - TM_NMID)*TM_MAGIC_4096/TM_NSCL
684 let etz: i64 = ((nimg[noff+2] as i64) - TM_NMID)*TM_MAGIC_4096/TM_NSCL
685 let fnx: i64 = (e0*NVXV[i0] + e1*NVXV[i1] + e2*NVXV[i2]) / area
686 let fny: i64 = (e0*NVYV[i0] + e1*NVYV[i1] + e2*NVYV[i2]) / area
687 let fnz: i64 = (e0*NVZV[i0] + e1*NVZV[i1] + e2*NVZV[i2]) / area
688 tm_perturb_normal(fnx, fny, fnz, etx, ety, etz, PN3)
689 var lm: i64 = (PN3[0]*lx + PN3[1]*ly + PN3[2]*lz) / (4*llen)
690 if lm < 0 { lm = 0 }
691 if lm > TM_MAGIC_1024 { lm = TM_MAGIC_1024 }
692 l3 = lm
693 }
694 var needw: i64 = 0
695 if TM_TEX > 0 { needw = 1 }
696 if TM_SHON == 1 { needw = 1 }
697 if needw == 1 {
698 let wx: i64 = (e0*mvx[i0] + e1*mvx[i1] + e2*mvx[i2]) / area
699 let wy: i64 = (e0*mvy[i0] + e1*mvy[i1] + e2*mvy[i2]) / area
700 let wz: i64 = (e0*mvz[i0] + e1*mvz[i1] + e2*mvz[i2]) / area
701 if TM_TEX > 0 {
702 // ★Q1 MULTI-OCTAVE ALBEDO (not one grey mottle): 3 octaves of brightness
703 // + a decorrelated HUE octave (warm/cool tonal variation) -- the natural
704 // self-similar detail the photoreal judge (ns_assess MSCN/scale) measures.
705 let n1: i64 = tm_noise3(wx, wy, wz, TM_TEX as i64)
706 let n2: i64 = tm_noise3(wx, wy+TM_MAGIC_37777, wz, TM_TEX/3+1)
707 let n3: i64 = tm_noise3(wx+TM_MAGIC_91131, wy, wz, TM_TEX/9+1)
708 let tf: i64 = 620 + (n1*3 + n2*2 + n3)*470/TM_MAGIC_6144 // 0.60..1.06
709 let hue: i64 = tm_noise3(wx, wy+TM_MAGIC_81111, wz, TM_TEX*2) - 512 // warm/cool +-
710 mr = mr*tf/TM_MAGIC_1024 + hue*22/512
711 mg = mg*tf/TM_MAGIC_1024 + hue*8/512
712 mb = mb*tf/TM_MAGIC_1024 - hue*20/512
713 if mr < 0 { mr = 0 }
714 if mg < 0 { mg = 0 }
715 if mb < 0 { mb = 0 }
716 }
717 if TM_SHON == 1 { // ★Q2 sun shadow
718 let sf: i64 = tm_shadow_at(wx, wy, wz)
719 l3 = l3*sf/TM_MAGIC_1024
720 sp3 = sp3*sf/TM_MAGIC_1024
721 }
722 }
723 let s3: i64 = 42 + 86*l3/TM_MAGIC_1024
724 var r3: i64 = mr*s3/128 + sp3; var g3: i64 = mg*s3/128 + sp3; var b3: i64 = mb*s3/128 + sp3
725 if r3>255 {r3=255} if g3>255 {g3=255} if b3>255 {b3=255}
726 fb[idx] = r3 + g3*256 + b3*TM_MAGIC_65536
727 } else { if smooth == 3 {
728 // UNLIT per-vertex colour (radiosity display: the vertex colour IS the lighting)
729 var pr: i64 = (e0*k0r + e1*k1r + e2*k2r) / area
730 var pg: i64 = (e0*k0g + e1*k1g + e2*k2g) / area
731 var pb: i64 = (e0*k0b + e1*k1b + e2*k2b) / area
732 if pr<0 {pr=0} if pg<0 {pg=0} if pb<0 {pb=0}
733 if pr>255 {pr=255} if pg>255 {pg=255} if pb>255 {pb=255}
734 fb[idx] = pr + pg*256 + pb*TM_MAGIC_65536
735 } else { fb[idx] = fcol } } }
736 }
737 }
738 px = px + 1
739 }
740 py = py + 1
741 }
742 }
743 t = t + 1
744 }
745 return 0
746}
747// clear a z-buffer to +far
748func trimesh_zclear(zbuf: *i64, n: i64) -> i64 { var i: i64=0; while i<n { zbuf[i] = TM_MAGIC_2000000000; i=i+1 } return 0 }
749// ★F3 ANTI-ALIASING (SSAA): render at 2x resolution, box-downsample into fb -> smooth edges. fb must be pre-cleared to bg.
750func trimesh_render_aa(fb: *i64, W: i64, H: i64, yaw: i64, pitch: i64, camz: i64, focal: i64, smooth: i64) -> i64 {
751 let W2: i64 = W*2; let H2: i64 = H*2
752 let big: *i64 = sys_mmap(W2*H2*8) as *i64
753 let bz: *i64 = sys_mmap(W2*H2*8) as *i64
754 let bg: i64 = fb[0]
755 var i: i64 = 0
756 while i < W2*H2 { big[i] = bg; i = i+1 }
757 trimesh_zclear(bz, W2*H2)
758 trimesh_render(big, bz, W2, H2, yaw, pitch, camz, focal*2, smooth) // focal*2 keeps the 2x framing identical
759 var y: i64 = 0
760 while y < H {
761 var x: i64 = 0
762 while x < W {
763 let p0: i64 = big[(2*y)*W2 + 2*x]; let p1: i64 = big[(2*y)*W2 + 2*x+1]
764 let p2: i64 = big[(2*y+1)*W2 + 2*x]; let p3: i64 = big[(2*y+1)*W2 + 2*x+1]
765 let r: i64 = ((p0&255)+(p1&255)+(p2&255)+(p3&255))/4
766 let g: i64 = (((p0>>8)&255)+((p1>>8)&255)+((p2>>8)&255)+((p3>>8)&255))/4
767 let b: i64 = (((p0>>16)&255)+((p1>>16)&255)+((p2>>16)&255)+((p3>>16)&255))/4
768 fb[y*W + x] = r + g*256 + b*TM_MAGIC_65536
769 x = x + 1
770 }
771 y = y + 1
772 }
773 return 0
774}