code wiki / (root) / nx_meshrender.nx

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}