nx_meshrender.nx source
↩ module page · 187 lines · 10171 B
1// nx_meshrender.nx -- LIB: SOVEREIGN GENERAL TRIANGLE-MESH RENDERER (the keystone rung above nx_render_core).
2// Transforms + projects + z-buffer-rasterizes an ARBITRARY loaded triangle mesh (CAD STL / game asset) through
3// OUR OWN pipeline -- no OpenGL, no WebGL, no 3rd-party graphics. render_core gives the z-buffered triangle
4// primitive + Q14 perspective/mat4/sovereign-trig; this composes them over a vertex+index buffer so any mesh
5// renders, z-correct, in software (rule 15/22: compose, re-implement nothing). HAL-FREE (no syscalls, all
6// buffers caller-supplied) so the IDENTICAL code compiles to --target wat/wasm -> the mesh renders in ANY
7// browser via OUR sovereign WASM (the "emit, don't use 3rd party" path that nx_wasmcube/nx_pets3d_wasm proved).
8// license_tier: ORIGINAL
9//
10// Vertex layout (stride 4 i64): [x_q14, y_q14, z_q14, packed_rgba]. Index layout (stride 3 i64): [i0,i1,i2].
11// Caller supplies scratch: proj/roty/mv/trans/mvp (16 i64 each), vbuf/clipbuf (4 i64), scr (nverts*4 i64:
12// [screen_x, screen_y, depth_q14, visible]), tribuf (12 i64). fb/zb = w*h i64. Returns triangles drawn.
13import "nx_render_core.nx"
14import "nx_vecmath.nx"
15const MR_MAGIC_65536: i64 = 65536
16const MR_MAGIC_16777216: i64 = 16777216
17
18const MR_NEAR: i64 = 16384 // 1.0 in Q14 (near plane)
19const MR_FAR: i64 = 16384000 // 1000.0 in Q14 (far plane)
20
21// Y-rotation matrix (Q14, row-major) into out[16]. Uses render_core's sovereign fixed-point trig (no JS/table).
22func mr_roty(deg: i64, out: *i64) -> i64 {
23 let c: i64 = rc_cos_q14(deg)
24 let s: i64 = rc_sin_q14(deg)
25 var i: i64 = 0
26 while i < 16 { out[i] = 0; i = i + 1 }
27 out[0] = c
28 out[2] = s
29 out[5] = RC_Q
30 out[8] = 0 - s
31 out[10] = c
32 out[15] = RC_Q
33 return 0
34}
35
36// Project one model vertex through mvp -> screen (col,row), depth (Q14, smaller=closer), visibility.
37// Writes scr[base+0..3] = [screen_x, screen_y, depth, visible(1/0)]. vbuf/clipbuf are 4-i64 scratch.
38func mr_project(mvp: *i64, vx: i64, vy: i64, vz: i64, w: i64, h: i64, vbuf: *i64, clipbuf: *i64, scr: *i64, base: i64) -> i64 {
39 vbuf[0] = vx
40 vbuf[1] = vy
41 vbuf[2] = vz
42 vbuf[3] = RC_Q
43 rc_mat4_vec4(mvp, vbuf, clipbuf)
44 let cw: i64 = clipbuf[3]
45 if cw <= 0 { scr[base + 3] = 0; return 0 } // behind camera / at-or-past the near plane -> not visible
46 let ndcx: i64 = clipbuf[0] * RC_Q / cw
47 let ndcy: i64 = clipbuf[1] * RC_Q / cw
48 let ndcz: i64 = clipbuf[2] * RC_Q / cw
49 scr[base + 0] = (ndcx + RC_Q) * w / (2 * RC_Q) // NDC [-1,1] -> [0,w]
50 scr[base + 1] = (RC_Q - ndcy) * h / (2 * RC_Q) // NDC y up -> screen row down
51 scr[base + 2] = ndcz + RC_Q // [-1,1] -> [0,2Q]; smaller=closer; zclear far >> this
52 scr[base + 3] = 1
53 return 0
54}
55
56// ===== CAP-PBR rung-1: Lambert diffuse directional shading (per-vertex UNIT normals, Q14) =====
57// dot of two Q14 vectors / RC_Q -> for UNIT vectors this equals cos(angle) in Q14.
58func mr_dot3(ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64) -> i64 { return (ax * bx + ay * by + az * bz) / RC_Q }
59// Lambert term: max(0, dot(N,L)) for unit N,L -> [0,RC_Q]. Back-faces (dot<0) go dark = no negative light.
60func mr_lambert(nx: i64, ny: i64, nz: i64, lx: i64, ly: i64, lz: i64) -> i64 { let d: i64 = mr_dot3(nx, ny, nz, lx, ly, lz); if d < 0 { return 0 } return d }
61// rotate a normal by a Y-rotation (cos c, sin s in Q14) into out[0..2] -> the WORLD normal as the object orbits.
62func mr_rotn(c: i64, s: i64, nx: i64, ny: i64, nz: i64, out: *i64) -> i64 { out[0] = (c * nx + s * nz) / RC_Q; out[1] = ny; out[2] = ((0 - s) * nx + c * nz) / RC_Q; return 0 }
63// shade a packed rgba by brightness b in [0,RC_Q] with an ambient floor amb in [0,RC_Q]; alpha kept 255.
64func mr_shade(rgba: i64, b: i64, amb: i64) -> i64 {
65 var bb: i64 = b
66 if bb < amb { bb = amb }
67 let r: i64 = (rgba % 256) * bb / RC_Q
68 let g: i64 = ((rgba / 256) % 256) * bb / RC_Q
69 let bl: i64 = ((rgba / MR_MAGIC_65536) % 256) * bb / RC_Q
70 return r + g * 256 + bl * MR_MAGIC_65536 + 255 * MR_MAGIC_16777216
71}
72
73// ===== CAP-SKELETAL-ANIM rung-1: linear blend skinning (2-bone core) =====
74// skinned = w0*(M0*rest) + w1*(M1*rest), all Q14 (weights sum to RC_Q). rest=(x,y,z,RC_Q) homogeneous; M0/M1 = Q14
75// mat4 bone transforms (render_core builders: rc_identity/rc_translation/mr_roty); tmp[4] scratch; out[4] = the
76// skinned homogeneous position. Reuses rc_mat4_vec4 (rule 15). N-bone = the same accumulate over more (M_i,w_i)
77// pairs -- 2 influences is the rung-1 core (most vertices bind to <=4 bones).
78func mr_skin2(rest: *i64, m0: *i64, w0: i64, m1: *i64, w1: i64, tmp: *i64, out: *i64) -> i64 {
79 rc_mat4_vec4(m0, rest, tmp)
80 out[0] = w0 * tmp[0] / RC_Q
81 out[1] = w0 * tmp[1] / RC_Q
82 out[2] = w0 * tmp[2] / RC_Q
83 out[3] = w0 * tmp[3] / RC_Q
84 rc_mat4_vec4(m1, rest, tmp)
85 out[0] = out[0] + w1 * tmp[0] / RC_Q
86 out[1] = out[1] + w1 * tmp[1] / RC_Q
87 out[2] = out[2] + w1 * tmp[2] / RC_Q
88 out[3] = out[3] + w1 * tmp[3] / RC_Q
89 return 0
90}
91
92// ===== skinned normals: lighting from the ACTUAL (deformed) geometry =====
93// sovereign integer sqrt (Newton). returns floor(sqrt(n)) for n>=0.
94func mr_isqrt(n: i64) -> i64 { return vm_isqrt(n) }
95// face normal from 3 (possibly SKINNED/deformed) positions -> Lambert brightness [0,RC_Q] with UNIT light L (Q14).
96// N = (p1-p0) x (p2-p0); brightness = max(0, dot(N,L)) / |N|. Lighting derives from the real geometry, so it is
97// CORRECT under skinning deformation (no pre-baked normal). Q14 in -> Q14 brightness out; mr_isqrt for |N|.
98func mr_face_lambert(p0x: i64, p0y: i64, p0z: i64, p1x: i64, p1y: i64, p1z: i64, p2x: i64, p2y: i64, p2z: i64, lx: i64, ly: i64, lz: i64) -> i64 {
99 let e1x: i64=p1x-p0x; let e1y: i64=p1y-p0y; let e1z: i64=p1z-p0z
100 let e2x: i64=p2x-p0x; let e2y: i64=p2y-p0y; let e2z: i64=p2z-p0z
101 let nx: i64=e1y*e2z - e1z*e2y
102 let ny: i64=e1z*e2x - e1x*e2z
103 let nz: i64=e1x*e2y - e1y*e2x
104 let dotnl: i64=nx*lx + ny*ly + nz*lz
105 if dotnl <= 0 { return 0 }
106 let mag2: i64=nx*nx + ny*ny + nz*nz
107 if mag2 <= 0 { return 0 }
108 let m: i64=mr_isqrt(mag2)
109 if m <= 0 { return 0 }
110 return dotnl / m
111}
112
113// ===== CAP-PBR rung-2: Blinn-Phong specular (the glossy highlight from the half-vector) =====
114// integer power in Q14: base,result in [0,RC_Q]; pow(b,0)=RC_Q, pow(b,n)=b^n. Higher n = sharper/tighter highlight.
115func mr_pow_q14(base: i64, n: i64) -> i64 {
116 var r: i64 = RC_Q
117 var i: i64 = 0
118 while i < n {
119 r = r * base / RC_Q
120 i = i + 1
121 }
122 return r
123}
124// Blinn-Phong specular term [0,RC_Q] from the (skinned/deformed) face geometry: N=(p1-p0)x(p2-p0),
125// H=unit(L+V) (L=light dir, V=view dir, both unit Q14), spec=max(0,dot(unitN,H))^shininess. Normalises N and H to
126// unit Q14 FIRST (bounds the integer range), then the power sharpens the highlight. Like mr_face_lambert it derives
127// from the real geometry, so the highlight is CORRECT under deformation. Small (unit-ish) Q14 coords (no overflow).
128func mr_face_specular(p0x: i64, p0y: i64, p0z: i64, p1x: i64, p1y: i64, p1z: i64, p2x: i64, p2y: i64, p2z: i64, lx: i64, ly: i64, lz: i64, vx: i64, vy: i64, vz: i64, shin: i64) -> i64 {
129 let e1x: i64=p1x-p0x; let e1y: i64=p1y-p0y; let e1z: i64=p1z-p0z
130 let e2x: i64=p2x-p0x; let e2y: i64=p2y-p0y; let e2z: i64=p2z-p0z
131 let nx: i64=e1y*e2z - e1z*e2y
132 let ny: i64=e1z*e2x - e1x*e2z
133 let nz: i64=e1x*e2y - e1y*e2x
134 let nmag2: i64=nx*nx + ny*ny + nz*nz
135 if nmag2 <= 0 { return 0 }
136 let nmag: i64=mr_isqrt(nmag2)
137 if nmag <= 0 { return 0 }
138 let hx: i64=lx+vx; let hy: i64=ly+vy; let hz: i64=lz+vz
139 let hmag2: i64=hx*hx + hy*hy + hz*hz
140 if hmag2 <= 0 { return 0 }
141 let hmag: i64=mr_isqrt(hmag2)
142 if hmag <= 0 { return 0 }
143 let unx: i64=nx*RC_Q/nmag; let uny: i64=ny*RC_Q/nmag; let unz: i64=nz*RC_Q/nmag
144 let uhx: i64=hx*RC_Q/hmag; let uhy: i64=hy*RC_Q/hmag; let uhz: i64=hz*RC_Q/hmag
145 let ndoth: i64=(unx*uhx + uny*uhy + unz*uhz) / RC_Q
146 if ndoth <= 0 { return 0 }
147 var nd: i64=ndoth
148 if nd > RC_Q { nd = RC_Q }
149 return mr_pow_q14(nd, shin)
150}
151
152// Render the whole mesh. angle_deg orbits the model about Y; dist pushes it down -Z (Q14); fov_half_deg = half
153// the vertical field of view. Clears fb to bg + zb to far, builds MVP = Proj * (Translate * RotY), projects every
154// vertex, then z-buffer-rasterizes every triangle whose 3 vertices are visible. Returns triangles drawn.
155func mr_render(verts: *i64, nverts: i64, idx: *i64, ntris: i64, proj: *i64, roty: *i64, mv: *i64, trans: *i64, mvp: *i64, vbuf: *i64, clipbuf: *i64, scr: *i64, tribuf: *i64, fb: *i64, zb: *i64, w: i64, h: i64, angle_deg: i64, dist: i64, fov_half_deg: i64, bg: i64) -> i64 {
156 rc_clear(fb, w, h, bg)
157 rc_zclear(zb, w, h)
158 let cosh: i64 = rc_cos_q14(fov_half_deg)
159 let sinh: i64 = rc_sin_q14(fov_half_deg)
160 let aspect: i64 = w * RC_Q / h
161 rc_perspective_cs(cosh, sinh, aspect, MR_NEAR, MR_FAR, proj)
162 mr_roty(angle_deg, roty)
163 rc_translation_4x4(0, 0, 0 - dist, trans)
164 rc_mat4_mul(trans, roty, mv) // mv = Translate * RotY (model -> view)
165 rc_mat4_mul(proj, mv, mvp) // mvp = Proj * mv (model -> clip)
166 var vi: i64 = 0
167 while vi < nverts {
168 mr_project(mvp, verts[vi * 4 + 0], verts[vi * 4 + 1], verts[vi * 4 + 2], w, h, vbuf, clipbuf, scr, vi * 4)
169 vi = vi + 1
170 }
171 var ti: i64 = 0
172 var drawn: i64 = 0
173 while ti < ntris {
174 let a: i64 = idx[ti * 3 + 0]
175 let b: i64 = idx[ti * 3 + 1]
176 let c: i64 = idx[ti * 3 + 2]
177 if scr[a * 4 + 3] == 1 { if scr[b * 4 + 3] == 1 { if scr[c * 4 + 3] == 1 {
178 tribuf[0] = scr[a * 4 + 0]; tribuf[1] = scr[a * 4 + 1]; tribuf[2] = scr[a * 4 + 2]; tribuf[3] = verts[a * 4 + 3]
179 tribuf[4] = scr[b * 4 + 0]; tribuf[5] = scr[b * 4 + 1]; tribuf[6] = scr[b * 4 + 2]; tribuf[7] = verts[b * 4 + 3]
180 tribuf[8] = scr[c * 4 + 0]; tribuf[9] = scr[c * 4 + 1]; tribuf[10] = scr[c * 4 + 2]; tribuf[11] = verts[c * 4 + 3]
181 rc_triangle(fb, zb, w, h, tribuf)
182 drawn = drawn + 1
183 } } }
184 ti = ti + 1
185 }
186 return drawn
187}