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}