code wiki / (root) / nx_pets_voxel_view.nx

nx_pets_voxel_view.nx source

↩ module page · 297 lines · 12718 B

1// nx_pets_voxel_view.nx -- RUNG 1 of the Pixelmon-exceed ladder: render the PETS world in 2// first-person 3D by COMPOSING the team's CERTIFIED voxel renderer, not by re-authoring one. 3// 4// Composes (parts-composition doctrine, game_parts.tsv `render-input`): 5// nx_raycast_voxel -- the certified DDA voxel ray-walk (Q14 fixed-point, no FPU; the 6// "minecraft on lowest hardware" core). We do NOT reimplement DDA. 7// nx_tissue -- the certified palette-packed 3D voxel store (the world container). 8// 9// What THIS organ authors (the composition glue = tutor tooling, per game-spec authorship law): 10// - lift the pets 16x12 map into a tissue (walls/trees = solid voxels), 11// - a camera-plane ray sweep (one ray per screen column) over the 4-direction facing the 12// pets game already uses (free-look/trig = a LATER rung; this rung stays atomic), 13// - perspective projection of wall slices. KEY: with a cardinal-facing camera the 14// perpendicular distance to a hit is exactly (hit_axis - cam_axis) in voxels -> zero 15// fisheye, zero fixed-point t-scaling ambiguity, 16// - face + distance-fog shading, floor/sky split, 17// - a viewable 24-bit BMP (last-mile image, same standing as the canvas framebuffer), 18// - a BAKED pos/neg gate: HIT coords + perspective-monotonic + occlusion + face-shading, 19// so the render capability self-proves (no false green). 20// NO browser yet: in-browser 3D delivery is the NAMED next rung (wasm/stream surface = the 21// MINECRAFTCLONE bits-up gap). Native-3D-now, browser-3D-next, no floating capability. 22// license_tier: ORIGINAL 23import "nx_raycast_voxel.nx" 24import "nx_tissue.nx" 25import "nx_syscalls.nx" 26const NX_MAGIC_10000: i64 = 10000 27const NX_MAGIC_8192: i64 = 8192 28const NX_MAGIC_2835: i64 = 2835 29 30const NX_W: nx_int = 320 31const NX_H: nx_int = 200 32const NX_Q14: nx_int = 16384 33const NX_HALFFOV: nx_int = 10637 // tan(33deg)*Q14 ~ 0.649*16384 (FOV ~66deg) 34const NX_MAXSTEP: nx_int = 64 35 36// ---- little-endian header writers ------------------------------------------------- 37func vv_w16(buf: *u8, off: nx_int, v: nx_int) -> nx_int { 38 buf[off] = (v & 255) as u8 39 buf[off + 1] = ((v >> 8) & 255) as u8 40 return off + 2 41} 42func vv_w32(buf: *u8, off: nx_int, v: nx_int) -> nx_int { 43 buf[off] = (v & 255) as u8 44 buf[off + 1] = ((v >> 8) & 255) as u8 45 buf[off + 2] = ((v >> 16) & 255) as u8 46 buf[off + 3] = ((v >> 24) & 255) as u8 47 return off + 4 48} 49 50// ---- pets map -> tissue (16 x 1 x 12; walls/trees full-height columns) ------------- 51// idx: 1 stone(border/pillar) 2 tree 4 placed-block. A KNOWN deterministic scene so 52// the gate can assert exact hit coords. 53func vv_build_world() -> *NxTissue { 54 let t: *NxTissue = nx_tissue_new(16 as nx_size, 1 as nx_size, 12 as nx_size, NX_BPP_4) 55 var z: nx_int = 0 56 while z < 12 { 57 var x: nx_int = 0 58 while x < 16 { 59 var s: nx_int = 0 60 if x == 0 { s = 1 } 61 if x == 15 { s = 1 } 62 if z == 0 { s = 1 } 63 if z == 11 { s = 1 } 64 if s > 0 { nx_tissue_set(t, x as nx_size, 0 as nx_size, z as nx_size, s) } 65 x = x + 1 66 } 67 z = z + 1 68 } 69 // interior scene: a tree pillar ahead, a stone pillar, a placed block 70 nx_tissue_set(t, 8 as nx_size, 0 as nx_size, 3 as nx_size, 2) // tree dead ahead of cam(3,3)/E 71 nx_tissue_set(t, 11 as nx_size, 0 as nx_size, 3 as nx_size, 1) // stone further along same row 72 nx_tissue_set(t, 6 as nx_size, 0 as nx_size, 6 as nx_size, 4) // placed block off to the side 73 nx_tissue_set(t, 6 as nx_size, 0 as nx_size, 7 as nx_size, 4) 74 return t 75} 76 77// ---- exact perpendicular distance (voxels) for a cardinal-facing camera ------------ 78// facing: 0 N(-Z) 1 S(+Z) 2 W(-X) 3 E(+X) (matches pets gs[23]) 79func vv_perp(facing: nx_int, cam_x: nx_int, cam_z: nx_int, hx: nx_int, hz: nx_int) -> nx_int { 80 var d: nx_int = 0 81 if facing == 0 { d = cam_z - hz } 82 if facing == 1 { d = hz - cam_z } 83 if facing == 2 { d = cam_x - hx } 84 if facing == 3 { d = hx - cam_x } 85 if d < 1 { d = 1 } 86 return d 87} 88// projected wall-slice height; monotonically shrinks with distance (the gate checks this) 89func vv_line_h(perp: nx_int) -> nx_int { 90 var h: nx_int = (NX_H * 3) / perp 91 if h > NX_H { h = NX_H } 92 return h 93} 94 95// ---- per-facing camera direction + plane (Q14), no trig -------------------------- 96func vv_dir_x(facing: nx_int) -> nx_int { 97 if facing == 2 { return 0 - NX_Q14 } 98 if facing == 3 { return NX_Q14 } 99 return 0 100} 101func vv_dir_z(facing: nx_int) -> nx_int { 102 if facing == 0 { return 0 - NX_Q14 } 103 if facing == 1 { return NX_Q14 } 104 return 0 105} 106func vv_plane_x(facing: nx_int) -> nx_int { 107 if facing == 0 { return NX_HALFFOV } 108 if facing == 1 { return 0 - NX_HALFFOV } 109 return 0 110} 111func vv_plane_z(facing: nx_int) -> nx_int { 112 if facing == 3 { return NX_HALFFOV } 113 if facing == 2 { return 0 - NX_HALFFOV } 114 return 0 115} 116 117// shade channel = base * face_factor * fog_factor / 10000 118func vv_shade(base: nx_int, face: nx_int, perp: nx_int) -> nx_int { 119 var ff: nx_int = 100 120 if face == NX_RF_X_POS { ff = 72 } 121 if face == NX_RF_X_NEG { ff = 72 } 122 var fog: nx_int = 100 - perp * 6 123 if fog < 38 { fog = 38 } 124 var v: nx_int = base * ff * fog / NX_MAGIC_10000 125 if v > 255 { v = 255 } 126 if v < 0 { v = 0 } 127 return v 128} 129 130// ---- render one frame into an RGB (top-down) framebuffer --------------------------- 131func vv_render(fb: *u8, world: *NxTissue, cam_x: nx_int, cam_z: nx_int, facing: nx_int, band: nx_int) -> nx_int { 132 let ray: *NxRay = (sys_mmap(56)) as *NxRay 133 let hit: *NxRayHit = (sys_mmap(56)) as *NxRayHit 134 let dx: nx_int = vv_dir_x(facing) 135 let dz: nx_int = vv_dir_z(facing) 136 let pxp: nx_int = vv_plane_x(facing) 137 let pzp: nx_int = vv_plane_z(facing) 138 var c: nx_int = 0 139 while c < NX_W { 140 let camx_q14: nx_int = ((2 * c - NX_W) * NX_Q14) / NX_W 141 ray.origin_x_q14 = cam_x * NX_Q14 + NX_MAGIC_8192 142 ray.origin_y_q14 = NX_MAGIC_8192 143 ray.origin_z_q14 = cam_z * NX_Q14 + NX_MAGIC_8192 144 ray.dir_x_q14 = dx + (pxp * camx_q14) / NX_Q14 145 ray.dir_y_q14 = 0 146 ray.dir_z_q14 = dz + (pzp * camx_q14) / NX_Q14 147 ray.max_steps = NX_MAXSTEP 148 let verdict: nx_int = nx_raycast_voxel(ray, world, hit) 149 // defaults: sky top half, floor bottom half 150 var top: nx_int = NX_H / 2 151 var bot: nx_int = NX_H / 2 152 var r: nx_int = 40 153 var g: nx_int = 60 154 var b: nx_int = 90 155 if verdict == NX_RV_HIT { 156 let perp: nx_int = vv_perp(facing, cam_x, cam_z, hit.hit_x, hit.hit_z) 157 let lh: nx_int = vv_line_h(perp) 158 top = (NX_H - lh) / 2 159 bot = (NX_H + lh) / 2 160 var br: nx_int = 150 161 var bg: nx_int = 150 162 var bb: nx_int = 160 163 if hit.palette_idx == 2 { br = 70; bg = 165; bb = 80 } // tree 164 if hit.palette_idx == 4 { br = 160; bg = 110; bb = 70 } // placed 165 r = vv_shade(br, hit.face, perp) 166 g = vv_shade(bg, hit.face, perp) 167 b = vv_shade(bb, hit.face, perp) 168 } 169 // paint the column: sky / wall / floor 170 var y: nx_int = 0 171 while y < NX_H { 172 var pr: nx_int = 28 173 var pg: nx_int = 38 174 var pb: nx_int = 70 // sky 175 if y >= bot { pr = 58; pg = 48; pb = 38 } // floor 176 if y >= top { if y < bot { pr = r; pg = g; pb = b } } // wall slice 177 let o: nx_int = ((band + y) * NX_W + c) * 3 178 fb[o] = pr as u8 179 fb[o + 1] = pg as u8 180 fb[o + 2] = pb as u8 181 y = y + 1 182 } 183 c = c + 1 184 } 185 return 0 186} 187 188// ---- write the framebuffer as a 24-bit BMP (bottom-up, BGR) ------------------------ 189func vv_write_bmp(fb: *u8, path: *u8, h: nx_int) -> nx_int { 190 let npix: nx_int = NX_W * h * 3 191 let total: nx_int = 54 + npix 192 let out: *u8 = sys_mmap(total) 193 var o: nx_int = 0 194 out[0] = 66 as u8; out[1] = 77 as u8 // 'B''M' 195 o = vv_w32(out, 2, total) 196 o = vv_w32(out, 6, 0) 197 o = vv_w32(out, 10, 54) 198 o = vv_w32(out, 14, 40) 199 o = vv_w32(out, 18, NX_W) 200 o = vv_w32(out, 22, h) // positive -> bottom-up 201 o = vv_w16(out, 26, 1) 202 o = vv_w16(out, 28, 24) 203 o = vv_w32(out, 30, 0) 204 o = vv_w32(out, 34, npix) 205 o = vv_w32(out, 38, NX_MAGIC_2835) 206 o = vv_w32(out, 42, NX_MAGIC_2835) 207 o = vv_w32(out, 46, 0) 208 o = vv_w32(out, 50, 0) 209 var y: nx_int = 0 210 while y < h { 211 let srcy: nx_int = h - 1 - y // flip vertically for bottom-up BMP 212 var x: nx_int = 0 213 while x < NX_W { 214 let si: nx_int = (srcy * NX_W + x) * 3 215 let di: nx_int = 54 + (y * NX_W + x) * 3 216 out[di] = fb[si + 2] // B 217 out[di + 1] = fb[si + 1] // G 218 out[di + 2] = fb[si] // R 219 x = x + 1 220 } 221 y = y + 1 222 } 223 let fd: nx_int = sys_openat_wr(path, 420) 224 if fd < 0 { return 0 - 1 } 225 sys_write(fd, out, total) 226 sys_close(fd) 227 return 0 228} 229 230// ---- single-ray probe helper for the gate ----------------------------------------- 231func vv_cast1(world: *NxTissue, cam_x: nx_int, cam_z: nx_int, dx: nx_int, dz: nx_int, 232 hit: *NxRayHit) -> nx_int { 233 let ray: *NxRay = (sys_mmap(56)) as *NxRay 234 ray.origin_x_q14 = cam_x * NX_Q14 + NX_MAGIC_8192 235 ray.origin_y_q14 = NX_MAGIC_8192 236 ray.origin_z_q14 = cam_z * NX_Q14 + NX_MAGIC_8192 237 ray.dir_x_q14 = dx 238 ray.dir_y_q14 = 0 239 ray.dir_z_q14 = dz 240 ray.max_steps = NX_MAXSTEP 241 return nx_raycast_voxel(ray, world, hit) 242} 243 244func vv_print(s: *u8, n: nx_int) -> nx_int { sys_write(1, s, n); return 0 } 245 246func main() -> nx_int { 247 let world: *NxTissue = vv_build_world() 248 let hit: *NxRayHit = (sys_mmap(56)) as *NxRayHit 249 var pass: nx_int = 1 250 251 // CHECK A: camera (3,3) facing +X hits the tree at x=8 (occlusion: tree before the x=11 stone) 252 let vA: nx_int = vv_cast1(world, 3, 3, NX_Q14, 0, hit) 253 if vA != NX_RV_HIT { pass = 0; vv_print("A FAIL: no hit\n" as *u8, 15) } 254 if vA == NX_RV_HIT { 255 if hit.hit_x == 8 { vv_print("A OK: hit tree x=8 (near pillar occludes far)\n" as *u8, 46) } 256 else { pass = 0; vv_print("A FAIL: wrong hit_x\n" as *u8, 20) } 257 if hit.palette_idx != 2 { pass = 0; vv_print("A FAIL: wrong block\n" as *u8, 20) } 258 } 259 let perpA: nx_int = vv_perp(3, 3, 3, hit.hit_x, hit.hit_z) // = 8-3 = 5 260 261 // CHECK B: perspective monotonic -- a nearer wall yields a TALLER slice than a far one 262 let near_h: nx_int = vv_line_h(2) 263 let far_h: nx_int = vv_line_h(8) 264 if near_h > far_h { vv_print("B OK: perspective (near taller than far)\n" as *u8, 41) } 265 else { pass = 0; vv_print("B FAIL: perspective not monotonic\n" as *u8, 35) } 266 267 // CHECK C: occlusion -- remove the near tree, the SAME ray now reaches the far x=11 stone 268 nx_tissue_set(world, 8 as nx_size, 0 as nx_size, 3 as nx_size, 0) 269 let vC: nx_int = vv_cast1(world, 3, 3, NX_Q14, 0, hit) 270 if vC == NX_RV_HIT { if hit.hit_x == 11 { 271 vv_print("C OK: occlusion (far stone x=11 revealed once near cleared)\n" as *u8, 59) 272 } else { pass = 0; vv_print("C FAIL: not the far block\n" as *u8, 26) } } 273 else { pass = 0; vv_print("C FAIL: no far hit\n" as *u8, 19) } 274 nx_tissue_set(world, 8 as nx_size, 0 as nx_size, 3 as nx_size, 2) // restore scene 275 276 // CHECK D: face shading distinguishes a front X-face from a side Z-face 277 let frontShade: nx_int = vv_shade(150, NX_RF_X_NEG, perpA) 278 let sideShade: nx_int = vv_shade(150, NX_RF_Z_NEG, perpA) 279 if sideShade > frontShade { vv_print("D OK: face shading (side brighter than front)\n" as *u8, 47) } 280 else { pass = 0; vv_print("D FAIL: face shading flat\n" as *u8, 26) } 281 282 // render a full turn-around (all 4 cardinal facings from the player's spot) stacked 283 // into one 320x800 frame -- proves the world renders coherently from every angle 284 let fb: *u8 = sys_mmap(NX_W * 800 * 3) 285 var fc: nx_int = 0 286 while fc < 4 { 287 vv_render(fb, world, 3, 3, fc, fc * NX_H) 288 fc = fc + 1 289 } 290 let w: nx_int = vv_write_bmp(fb, "web_assets/_game_build/pets3d.bmp" as *u8, 800) 291 if w == 0 { vv_print("BMP web_assets/_game_build/pets3d.bmp 320x800 NSWE turn-around\n" as *u8, 63) } 292 else { pass = 0; vv_print("BMP FAIL: write\n" as *u8, 16) } 293 294 if pass == 1 { vv_print("PETS3D-OK: first-person voxel render composes the certified raycaster\n" as *u8, 69); return 0 } 295 vv_print("PETS3D-INCOMPLETE\n" as *u8, 18) 296 return 1 297}