code wiki / (root) / nx_wasmvox3d.nx

nx_wasmvox3d.nx source

↩ module page · 375 lines · 15497 B

1// nx_wasmvox3d.nx -- TRUE 3D VOXEL world (per-voxel faces, not column boxes) so you can dig holes/tunnels and 2// stack blocks. Base-relative (same code runs native->PNG and wasm->browser). The world = a natural heightmap 3// PLUS a 2-bit-packed 3D edit grid (0=default, 1=placed-solid, 2=dug-air). Render draws only EXPOSED faces of 4// solid voxels (face to an air neighbor) -> tunnels/overhangs/floating blocks all work. Scoped small (48x48x18, 5// 192x144) for software-raster speed. Reuses the proven f32 + raster math. license_tier: ORIGINAL 6import "nx_f32_hw.nx" 7const O_MAGIC_65536: i64 = 65536 8 9const W: i64 = 192 10const H: i64 = 144 11const HW: i64 = 96 12const HH: i64 = 72 13const ZFAR: i64 = 1073741824 14const FOCAL: i64 = 150 15const WN: i64 = 48 16const YMAX: i64 = 18 17const WSEED: i64 = 7 18const WWATER: i64 = 3 19const WMAXH: i64 = 12 20const WLAT: i64 = 6 21const VDIST: i64 = 14 22 23const O_FB: i64 = 0 24const O_ZB: i64 = 221184 // W*H*8 25const O_R: i64 = 442368 26const O_RY: i64 = 442496 27const O_RX: i64 = 442624 28const O_V: i64 = 442752 29const O_RV: i64 = 442784 30const O_SX: i64 = 442816 31const O_SY: i64 = 442880 32const O_SD: i64 = 442944 33const O_SOK: i64 = 443008 34const O_PJ: i64 = 443072 35const O_HC: i64 = 443136 // per-column height cache, WN*WN i64 36const O_GRID: i64 = 461568 // 2-bit 3D edit grid: WN*WN*YMAX cells / 32 per i64 37 38func imin(a: i64, b: i64) -> i64 { if a < b { return a } return b } 39func imax(a: i64, b: i64) -> i64 { if a > b { return a } return b } 40func min3(a: i64, b: i64, c: i64) -> i64 { return imin(a, imin(b, c)) } 41func max3(a: i64, b: i64, c: i64) -> i64 { return imax(a, imax(b, c)) } 42func clamp255(v: i64) -> i64 { if v < 0 { return 0 } if v > 255 { return 255 } return v } 43func fratio(num: i64, den: i64) -> i64 { return f32_div(f32_of(num), f32_of(den)) } 44 45func wlat(ix: i64, iz: i64) -> i64 { 46 var n: i64 = (ix * 71 + iz * 191 + ix * iz * 7 + WSEED * 101) % 6 47 if n < 0 { n = n + 6 } 48 return n 49} 50// natural surface height of a column (no edits) 51func terrain_h0(gx: i64, gz: i64) -> i64 { 52 let bx: i64 = gx + 200 53 let bz: i64 = gz + 200 54 let ix: i64 = bx / WLAT 55 let iz: i64 = bz / WLAT 56 let fx: i64 = (bx % WLAT) * 256 / WLAT 57 let fz: i64 = (bz % WLAT) * 256 / WLAT 58 let h00: i64 = wlat(ix, iz) 59 let h10: i64 = wlat(ix + 1, iz) 60 let h01: i64 = wlat(ix, iz + 1) 61 let h11: i64 = wlat(ix + 1, iz + 1) 62 let a: i64 = h00 * 256 + (h10 - h00) * fx 63 let b: i64 = h01 * 256 + (h11 - h01) * fx 64 var h: i64 = (a * 256 + (b - a) * fz) / O_MAGIC_65536 + 1 65 if h < 1 { h = 1 } 66 if h > WMAXH { h = WMAXH } 67 return h 68} 69// 2-bit edit grid 70func grid_idx(gx: i64, gy: i64, gz: i64) -> i64 { return (gy * WN + gz) * WN + gx } 71func getcell(base: i64, gx: i64, gy: i64, gz: i64) -> i64 { 72 let idx: i64 = grid_idx(gx, gy, gz) 73 let p: *i64 = (base + O_GRID) as *i64 74 return (p[idx / 32] >> ((idx % 32) * 2)) & 3 75} 76func setcell(base: i64, gx: i64, gy: i64, gz: i64, v: i64) -> i64 { 77 let idx: i64 = grid_idx(gx, gy, gz) 78 let p: *i64 = (base + O_GRID) as *i64 79 let sh: i64 = (idx % 32) * 2 80 let cur: i64 = p[idx / 32] 81 p[idx / 32] = (cur - (cur & (3 << sh))) | ((v & 3) << sh) 82 return 0 83} 84// is (gx,gy,gz) solid? hcol = cached natural height of column (gx,gz); pass -1 to force a fresh terrain_h0. 85func solid_h(base: i64, gx: i64, gy: i64, gz: i64, hcol: i64) -> i64 { 86 if gy < 0 { return 1 } 87 if gy >= YMAX { return 0 } 88 if gx < 0 { return 0 } 89 if gx >= WN { return 0 } 90 if gz < 0 { return 0 } 91 if gz >= WN { return 0 } 92 let e: i64 = getcell(base, gx, gy, gz) 93 if e == 1 { return 1 } 94 if e == 2 { return 0 } 95 var h: i64 = hcol 96 if h < 0 { h = terrain_h0(gx, gz) } 97 if gy < h { return 1 } 98 return 0 99} 100func solid(base: i64, gx: i64, gy: i64, gz: i64) -> i64 { 101 // read the cached column height (valid for 0<=gx,gz<WN; else fresh) 102 if gx >= 0 { if gx < WN { if gz >= 0 { if gz < WN { 103 let hc: *i64 = (base + O_HC) as *i64 104 return solid_h(base, gx, gy, gz, hc[gz * WN + gx]) 105 } } } } 106 return solid_h(base, gx, gy, gz, 0 - 1) 107} 108func voxel_color(gy: i64, top: i64) -> i64 { 109 if gy <= WWATER { return 64 + 116 * 256 + 204 * O_MAGIC_65536 } // water blue 110 if top == 1 { 111 if gy >= WMAXH - 1 { return 224 + 226 * 256 + 234 * O_MAGIC_65536 } // snow 112 return 96 + 172 * 256 + 76 * O_MAGIC_65536 // grass 113 } 114 if gy >= WMAXH - 1 { return 150 + 146 * 256 + 140 * O_MAGIC_65536 } // rock 115 if gy <= 2 { return 120 + 116 * 256 + 110 * O_MAGIC_65536 } // stone 116 return 134 + 98 * 256 + 60 * O_MAGIC_65536 // dirt 117} 118func shade(lnx: i64, lny: i64, lnz: i64) -> i64 { 119 var d: i64 = lnx * 2 + lny * 3 + lnz * 1 120 if d < 0 { d = 0 } 121 return clamp255(110 + d * 38) 122} 123func packc(col: i64, bright: i64) -> i64 { 124 let r: i64 = (col & 0xff) * bright / 255 125 let g: i64 = ((col >> 8) & 0xff) * bright / 255 126 let b: i64 = ((col >> 16) & 0xff) * bright / 255 127 return r + g * 256 + b * O_MAGIC_65536 128} 129func fill_z(base: i64, x0: i64, y0: i64, d0: i64, x1: i64, y1: i64, d1: i64, x2: i64, y2: i64, d2: i64, col: i64) -> i64 { 130 let fb: *i64 = (base + O_FB) as *i64 131 let zb: *i64 = (base + O_ZB) as *i64 132 let area: i64 = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0) 133 if area == 0 { return 0 } 134 let minx: i64 = imax(0, min3(x0, x1, x2)) 135 let maxx: i64 = imin(W - 1, max3(x0, x1, x2)) 136 let miny: i64 = imax(0, min3(y0, y1, y2)) 137 let maxy: i64 = imin(H - 1, max3(y0, y1, y2)) 138 var py: i64 = miny 139 while py <= maxy { 140 var px: i64 = minx 141 while px <= maxx { 142 let e0: i64 = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1) 143 let e1: i64 = (x0 - x2) * (py - y2) - (y0 - y2) * (px - x2) 144 let e2: i64 = (x1 - x0) * (py - y0) - (y1 - y0) * (px - x0) 145 var ins: i64 = 0 146 if area > 0 { if e0 >= 0 { if e1 >= 0 { if e2 >= 0 { ins = 1 } } } } 147 if area < 0 { if e0 <= 0 { if e1 <= 0 { if e2 <= 0 { ins = 1 } } } } 148 if ins == 1 { 149 let d: i64 = (d0 * e0 + d1 * e1 + d2 * e2) / area 150 let idx: i64 = py * W + px 151 if d < zb[idx] { zb[idx] = d; fb[idx] = col } 152 } 153 px = px + 1 154 } 155 py = py + 1 156 } 157 return 0 158} 159func project(base: i64, camx: i64, camy: i64, camz: i64, iwx: i64, iwy: i64, iwz: i64) -> i64 { 160 let v: *i64 = (base + O_V) as *i64 161 let r: *i64 = (base + O_RV) as *i64 162 let R: *i64 = (base + O_R) as *i64 163 let pj: *i64 = (base + O_PJ) as *i64 164 v[0] = f32_sub(f32_of(iwx), camx) 165 v[1] = f32_sub(f32_of(iwy), camy) 166 v[2] = f32_sub(f32_of(iwz), camz) 167 v[3] = f32_of(0) 168 m4_vec4(R, v, r) 169 let pz: i64 = r[2] 170 if (pz & 0x80000000) != 0 { pj[3] = 0; return 0 } 171 let zi: i64 = f32_int(f32_mul(pz, f32_of(64))) 172 if zi <= 0 { pj[3] = 0; return 0 } 173 pj[0] = HW + f32_int(f32_div(f32_mul(r[0], f32_of(FOCAL)), pz)) 174 pj[1] = HH - f32_int(f32_div(f32_mul(r[1], f32_of(FOCAL)), pz)) 175 pj[2] = zi 176 pj[3] = 1 177 return 0 178} 179func corners(base: i64, camx: i64, camy: i64, camz: i64, x0: i64, y0: i64, z0: i64) -> i64 { 180 let SX: *i64 = (base + O_SX) as *i64 181 let SY: *i64 = (base + O_SY) as *i64 182 let SD: *i64 = (base + O_SD) as *i64 183 let SOK: *i64 = (base + O_SOK) as *i64 184 let pj: *i64 = (base + O_PJ) as *i64 185 var k: i64 = 0 186 while k < 8 { 187 project(base, camx, camy, camz, x0 + (k & 1), y0 + ((k >> 1) & 1), z0 + ((k >> 2) & 1)) 188 SX[k] = pj[0]; SY[k] = pj[1]; SD[k] = pj[2]; SOK[k] = pj[3] 189 k = k + 1 190 } 191 return 0 192} 193func face(base: i64, a: i64, b: i64, c: i64, dd: i64, lnx: i64, lny: i64, lnz: i64, col: i64) -> i64 { 194 let SX: *i64 = (base + O_SX) as *i64 195 let SY: *i64 = (base + O_SY) as *i64 196 let SD: *i64 = (base + O_SD) as *i64 197 let SOK: *i64 = (base + O_SOK) as *i64 198 if SOK[a] == 0 { return 0 } 199 if SOK[b] == 0 { return 0 } 200 if SOK[c] == 0 { return 0 } 201 if SOK[dd] == 0 { return 0 } 202 let fc: i64 = packc(col, shade(lnx, lny, lnz)) 203 fill_z(base, SX[a], SY[a], SD[a], SX[b], SY[b], SD[b], SX[c], SY[c], SD[c], fc) 204 fill_z(base, SX[a], SY[a], SD[a], SX[c], SY[c], SD[c], SX[dd], SY[dd], SD[dd], fc) 205 return 0 206} 207// draw the exposed faces of solid voxel (gx,gy,gz) 208func draw_voxel(base: i64, camx: i64, camy: i64, camz: i64, gx: i64, gy: i64, gz: i64) -> i64 { 209 let up: i64 = solid(base, gx, gy + 1, gz) 210 let tcol: i64 = voxel_color(gy, 1 - up) // exposed top -> grass/surface tint 211 let scol: i64 = voxel_color(gy, 0) 212 corners(base, camx, camy, camz, gx, gy, gz) 213 if up == 0 { face(base, 2, 3, 7, 6, 0, 1, 0, tcol) } 214 if solid(base, gx + 1, gy, gz) == 0 { face(base, 1, 5, 7, 3, 1, 0, 0, scol) } 215 if solid(base, gx - 1, gy, gz) == 0 { face(base, 0, 2, 6, 4, 0 - 1, 0, 0, scol) } 216 if solid(base, gx, gy, gz + 1) == 0 { face(base, 4, 5, 7, 6, 0, 0, 1, scol) } 217 if solid(base, gx, gy, gz - 1) == 0 { face(base, 0, 1, 3, 2, 0, 0, 0 - 1, scol) } 218 return 0 219} 220func corners_sz(base: i64, camx: i64, camy: i64, camz: i64, x0: i64, y0: i64, z0: i64, sw: i64, sh: i64, sl: i64) -> i64 { 221 let SX: *i64 = (base + O_SX) as *i64 222 let SY: *i64 = (base + O_SY) as *i64 223 let SD: *i64 = (base + O_SD) as *i64 224 let SOK: *i64 = (base + O_SOK) as *i64 225 let pj: *i64 = (base + O_PJ) as *i64 226 var k: i64 = 0 227 while k < 8 { 228 project(base, camx, camy, camz, x0 + (k & 1) * sw, y0 + ((k >> 1) & 1) * sh, z0 + ((k >> 2) & 1) * sl) 229 SX[k] = pj[0]; SY[k] = pj[1]; SD[k] = pj[2]; SOK[k] = pj[3] 230 k = k + 1 231 } 232 return 0 233} 234func draw_solidbox(base: i64, camx: i64, camy: i64, camz: i64, x0: i64, y0: i64, z0: i64, sw: i64, sh: i64, sl: i64, col: i64) -> i64 { 235 corners_sz(base, camx, camy, camz, x0, y0, z0, sw, sh, sl) 236 face(base, 2, 3, 7, 6, 0, 1, 0, col) 237 face(base, 1, 5, 7, 3, 1, 0, 0, col) 238 face(base, 0, 2, 6, 4, 0 - 1, 0, 0, col) 239 face(base, 4, 5, 7, 6, 0, 0, 1, col) 240 face(base, 0, 1, 3, 2, 0, 0, 0 - 1, col) 241 return 0 242} 243func has_tree(gx: i64, gz: i64) -> i64 { 244 let h: i64 = terrain_h0(gx, gz) 245 if h <= WWATER { return 0 } 246 if h >= WMAXH - 1 { return 0 } 247 var n: i64 = (gx * 131 + gz * 197 + gx * gz * 7 + 13) % 17 248 if n < 0 { n = n + 17 } 249 if n == 0 { return 1 } 250 return 0 251} 252func draw_tree(base: i64, camx: i64, camy: i64, camz: i64, gx: i64, gz: i64) -> i64 { 253 let h: i64 = terrain_h0(gx, gz) 254 draw_solidbox(base, camx, camy, camz, gx, h, gz, 1, 3, 1, 92 + 64 * 256 + 38 * O_MAGIC_65536) 255 draw_solidbox(base, camx, camy, camz, gx - 1, h + 3, gz - 1, 3, 3, 3, 40 + 122 * 256 + 50 * O_MAGIC_65536) 256 return 0 257} 258func render_at(base: i64, camx: i64, camy: i64, camz: i64, cgx: i64, cgz: i64) -> i64 { 259 let fb: *i64 = (base + O_FB) as *i64 260 let zb: *i64 = (base + O_ZB) as *i64 261 let sky: i64 = 142 + 186 * 256 + 228 * O_MAGIC_65536 262 var i: i64 = 0 263 while i < W * H { fb[i] = sky; zb[i] = ZFAR; i = i + 1 } 264 // refresh the column-height cache for the world 265 let hc: *i64 = (base + O_HC) as *i64 266 var cz: i64 = 0 267 while cz < WN { var cx: i64 = 0; while cx < WN { hc[cz * WN + cx] = terrain_h0(cx, cz); cx = cx + 1 } cz = cz + 1 } 268 var gz: i64 = cgz - VDIST 269 while gz <= cgz + VDIST { 270 if gz >= 0 { if gz < WN { 271 var gx: i64 = cgx - VDIST 272 while gx <= cgx + VDIST { 273 if gx >= 0 { if gx < WN { 274 var gy: i64 = 0 275 while gy < YMAX { 276 if solid(base, gx, gy, gz) == 1 { draw_voxel(base, camx, camy, camz, gx, gy, gz) } 277 gy = gy + 1 278 } 279 } } 280 gx = gx + 1 281 } 282 } } 283 gz = gz + 1 284 } 285 // trees: decorative boxes after the voxel terrain (z-buffer occludes correctly) 286 var tgz: i64 = cgz - VDIST 287 while tgz <= cgz + VDIST { 288 if tgz >= 0 { if tgz < WN { 289 var tgx: i64 = cgx - VDIST 290 while tgx <= cgx + VDIST { 291 if tgx >= 0 { if tgx < WN { 292 if has_tree(tgx, tgz) == 1 { draw_tree(base, camx, camy, camz, tgx, tgz) } 293 } } 294 tgx = tgx + 1 295 } 296 } } 297 tgz = tgz + 1 298 } 299 return 0 300} 301func build_R(base: i64, ycos: i64, ysin: i64, pcos: i64, psin: i64) -> i64 { 302 let Ry: *i64 = (base + O_RY) as *i64 303 let Rx: *i64 = (base + O_RX) as *i64 304 let R: *i64 = (base + O_R) as *i64 305 m4_roty(fratio(ycos, 1000), fratio(ysin, 1000), Ry) 306 m4_rotx(fratio(pcos, 1000), f32_neg(fratio(psin, 1000)), Rx) 307 m4_mul(Rx, Ry, R) 308 return 0 309} 310func mem_bytes() -> i64 { return O_GRID + (WN * WN * YMAX / 32 + 4) * 8 } 311// surface height (highest solid voxel +1) at a column -- for spawn/collision in the shim 312func surface_y(base: i64, gx: i64, gz: i64) -> i64 { 313 var gy: i64 = YMAX - 1 314 while gy >= 0 { if solid(base, gx, gy, gz) == 1 { return gy + 1 } gy = gy - 1 } 315 return 0 316} 317// 3D-DDA raycast: returns the hit solid cell packed, plus the air cell just before it for placement. 318// out[0]=hit flag, out[1..3]=hit cell, out[4..6]=place cell (last air before hit). 319func raycast(base: i64, camx256: i64, camy256: i64, camz256: i64, dx1000: i64, dy1000: i64, dz1000: i64, out: *i64) -> i64 { 320 var px: i64 = camx256 321 var py: i64 = camy256 322 var pz: i64 = camz256 323 var ax: i64 = px / 256 324 var ay: i64 = py / 256 325 var az: i64 = pz / 256 326 var i: i64 = 0 327 while i < 140 { 328 let gx: i64 = px / 256 329 let gy: i64 = py / 256 330 let gz: i64 = pz / 256 331 if solid(base, gx, gy, gz) == 1 { 332 out[0] = 1; out[1] = gx; out[2] = gy; out[3] = gz; out[4] = ax; out[5] = ay; out[6] = az 333 return 0 334 } 335 ax = gx; ay = gy; az = gz 336 px = px + dx1000 * 38 / 1000 337 py = py + dy1000 * 38 / 1000 338 pz = pz + dz1000 * 38 / 1000 339 i = i + 1 340 } 341 out[0] = 0 342 return 0 343} 344// WASM exports 345func render(camx256: i64, camy256: i64, camz256: i64, ycos: i64, ysin: i64, pcos: i64, psin: i64) -> i64 { 346 build_R(0, ycos, ysin, pcos, psin) 347 let camx: i64 = f32_div(f32_of(camx256), f32_of(256)) 348 let camy: i64 = f32_div(f32_of(camy256), f32_of(256)) 349 let camz: i64 = f32_div(f32_of(camz256), f32_of(256)) 350 render_at(0, camx, camy, camz, camx256 / 256, camz256 / 256) 351 return 0 352} 353func terrain_at(gx: i64, gz: i64) -> i64 { 354 let hc: *i64 = (0 + O_HC) as *i64 355 if gx >= 0 { if gx < WN { if gz >= 0 { if gz < WN { hc[gz * WN + gx] = terrain_h0(gx, gz) } } } } 356 return surface_y(0, gx, gz) 357} 358// break the solid voxel the camera is aiming at (dug = air) 359func break_at(camx256: i64, camy256: i64, camz256: i64, dx1000: i64, dy1000: i64, dz1000: i64) -> i64 { 360 let o: *i64 = (0 + O_PJ) as *i64 361 raycast(0, camx256, camy256, camz256, dx1000, dy1000, dz1000, o) 362 if o[0] == 1 { if o[2] >= 0 { if o[2] < YMAX { setcell(0, o[1], o[2], o[3], 2) } } } 363 return 0 364} 365// place a solid voxel in the air cell just before the hit 366func place_at(camx256: i64, camy256: i64, camz256: i64, dx1000: i64, dy1000: i64, dz1000: i64) -> i64 { 367 let o: *i64 = (0 + O_PJ) as *i64 368 raycast(0, camx256, camy256, camz256, dx1000, dy1000, dz1000, o) 369 if o[0] == 1 { if o[5] >= 0 { if o[5] < YMAX { setcell(0, o[4], o[5], o[6], 1) } } } 370 return 0 371} 372func fb_off() -> i64 { return O_FB } 373func ww() -> i64 { return W } 374func hh() -> i64 { return H } 375func main() -> i64 { return 0 }