nx_wasmfps.nx source
↩ module page · 290 lines · 12386 B
1// nx_wasmfps.nx -- FIRST-PERSON voxel world renderer, base-relative so the SAME code runs native (base = an
2// mmap'd region -> PNG, eyeball-verifiable) and in WASM (base = 0 -> linear memory). Z-buffered, perspective,
3// distance-culled, with trees; realism directional shading. Camera has a world POSITION (you walk/look), not
4// orbit. JS owns trig + camera state and calls render(); wasm owns the world + the hardware-f32 math.
5// World params are constants (fixed world, moving camera). license_tier: ORIGINAL
6import "nx_f32_hw.nx"
7const O_MAGIC_65536: i64 = 65536
8
9const W: i64 = 200
10const H: i64 = 150
11const HW: i64 = 100
12const HH: i64 = 75
13const ZFAR: i64 = 1073741824
14const FOCAL: i64 = 150
15const WN: i64 = 96 // world is WN x WN columns
16const WSEED: i64 = 7
17const WWATER: i64 = 3
18const WMAXH: i64 = 14
19const WLAT: i64 = 6
20const VDIST: i64 = 26 // only draw columns within this Manhattan range of the camera
21
22const O_FB: i64 = 0
23const O_ZB: i64 = 240000 // W*H*8
24const O_R: i64 = 480000
25const O_RY: i64 = 480128
26const O_RX: i64 = 480256
27const O_V: i64 = 480384
28const O_RV: i64 = 480416
29const O_SX: i64 = 480448
30const O_SY: i64 = 480512
31const O_SD: i64 = 480576
32const O_SOK: i64 = 480640
33const O_PJ: i64 = 480704 // project out: x,y,d,ok
34const O_DELTA: i64 = 481024 // per-column height edits, WN*WN i64 (block break/place)
35
36func imin(a: i64, b: i64) -> i64 { if a < b { return a } return b }
37func imax(a: i64, b: i64) -> i64 { if a > b { return a } return b }
38func iabs(a: i64) -> i64 { if a < 0 { return 0 - a } return a }
39func min3(a: i64, b: i64, c: i64) -> i64 { return imin(a, imin(b, c)) }
40func max3(a: i64, b: i64, c: i64) -> i64 { return imax(a, imax(b, c)) }
41func clamp255(v: i64) -> i64 { if v < 0 { return 0 } if v > 255 { return 255 } return v }
42func fratio(num: i64, den: i64) -> i64 { return f32_div(f32_of(num), f32_of(den)) }
43
44func wlat(ix: i64, iz: i64) -> i64 {
45 var n: i64 = (ix * 71 + iz * 191 + ix * iz * 7 + WSEED * 101) % 6
46 if n < 0 { n = n + 6 }
47 return n
48}
49func terrain_h(base: i64, gx: i64, gz: i64) -> i64 {
50 let bx: i64 = gx + 200
51 let bz: i64 = gz + 200
52 let ix: i64 = bx / WLAT
53 let iz: i64 = bz / WLAT
54 let fx: i64 = (bx % WLAT) * 256 / WLAT
55 let fz: i64 = (bz % WLAT) * 256 / WLAT
56 let h00: i64 = wlat(ix, iz)
57 let h10: i64 = wlat(ix + 1, iz)
58 let h01: i64 = wlat(ix, iz + 1)
59 let h11: i64 = wlat(ix + 1, iz + 1)
60 let a: i64 = h00 * 256 + (h10 - h00) * fx
61 let b: i64 = h01 * 256 + (h11 - h01) * fx
62 var h: i64 = (a * 256 + (b - a) * fz) / O_MAGIC_65536 + 1
63 if h < 1 { h = 1 }
64 if h > WMAXH { h = WMAXH }
65 if gx >= 0 { if gx < WN { if gz >= 0 { if gz < WN {
66 let dl: *i64 = (base + O_DELTA) as *i64
67 h = h + dl[gz * WN + gx]
68 } } } }
69 if h < 1 { h = 1 }
70 if h > 30 { h = 30 }
71 return h
72}
73func hN(base: i64, gx: i64, gz: i64) -> i64 { return terrain_h(base, gx, gz) }
74func top_color(h: i64) -> i64 {
75 if h <= WWATER { return 64 + 116 * 256 + 204 * O_MAGIC_65536 }
76 if h >= WMAXH { return 236 + 238 * 256 + 245 * O_MAGIC_65536 }
77 if h == WMAXH - 1 { return 150 + 146 * 256 + 140 * O_MAGIC_65536 }
78 return 96 + 172 * 256 + 76 * O_MAGIC_65536
79}
80func side_color(h: i64) -> i64 {
81 if h <= WWATER { return 58 + 104 * 256 + 188 * O_MAGIC_65536 }
82 if h >= WMAXH - 1 { return 138 + 134 * 256 + 128 * O_MAGIC_65536 }
83 return 134 + 98 * 256 + 60 * O_MAGIC_65536
84}
85func shade(lnx: i64, lny: i64, lnz: i64) -> i64 {
86 var d: i64 = lnx * 2 + lny * 3 + lnz * 1
87 if d < 0 { d = 0 }
88 return clamp255(110 + d * 38)
89}
90func packc(col: i64, bright: i64) -> i64 {
91 let r: i64 = (col & 0xff) * bright / 255
92 let g: i64 = ((col >> 8) & 0xff) * bright / 255
93 let b: i64 = ((col >> 16) & 0xff) * bright / 255
94 return r + g * 256 + b * O_MAGIC_65536
95}
96// z-buffered flat-colour triangle (barycentric, depth interpolated). col is final packed RGB.
97func 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 {
98 let fb: *i64 = (base + O_FB) as *i64
99 let zb: *i64 = (base + O_ZB) as *i64
100 let area: i64 = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
101 if area == 0 { return 0 }
102 let minx: i64 = imax(0, min3(x0, x1, x2))
103 let maxx: i64 = imin(W - 1, max3(x0, x1, x2))
104 let miny: i64 = imax(0, min3(y0, y1, y2))
105 let maxy: i64 = imin(H - 1, max3(y0, y1, y2))
106 var py: i64 = miny
107 while py <= maxy {
108 var px: i64 = minx
109 while px <= maxx {
110 let e0: i64 = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)
111 let e1: i64 = (x0 - x2) * (py - y2) - (y0 - y2) * (px - x2)
112 let e2: i64 = (x1 - x0) * (py - y0) - (y1 - y0) * (px - x0)
113 var inside: i64 = 0
114 if area > 0 { if e0 >= 0 { if e1 >= 0 { if e2 >= 0 { inside = 1 } } } }
115 if area < 0 { if e0 <= 0 { if e1 <= 0 { if e2 <= 0 { inside = 1 } } } }
116 if inside == 1 {
117 let d: i64 = (d0 * e0 + d1 * e1 + d2 * e2) / area
118 let idx: i64 = py * W + px
119 if d < zb[idx] { zb[idx] = d; fb[idx] = col }
120 }
121 px = px + 1
122 }
123 py = py + 1
124 }
125 return 0
126}
127// project world (iwx,iwy,iwz) relative to camera (camx,camy,camz f32) through R -> O_PJ[x,y,d,ok]
128func project(base: i64, camx: i64, camy: i64, camz: i64, iwx: i64, iwy: i64, iwz: i64) -> i64 {
129 let v: *i64 = (base + O_V) as *i64
130 let r: *i64 = (base + O_RV) as *i64
131 let R: *i64 = (base + O_R) as *i64
132 let pj: *i64 = (base + O_PJ) as *i64
133 v[0] = f32_sub(f32_of(iwx), camx)
134 v[1] = f32_sub(f32_of(iwy), camy)
135 v[2] = f32_sub(f32_of(iwz), camz)
136 v[3] = f32_of(0)
137 m4_vec4(R, v, r)
138 let pz: i64 = r[2]
139 if (pz & 0x80000000) != 0 { pj[3] = 0; return 0 }
140 let zi: i64 = f32_int(f32_mul(pz, f32_of(64)))
141 if zi <= 0 { pj[3] = 0; return 0 }
142 pj[0] = HW + f32_int(f32_div(f32_mul(r[0], f32_of(FOCAL)), pz))
143 pj[1] = HH - f32_int(f32_div(f32_mul(r[1], f32_of(FOCAL)), pz))
144 pj[2] = zi
145 pj[3] = 1
146 return 0
147}
148// project the 8 corners of an axis box at (x0,y0,z0) size (sxw,syw,szw) into the SX/SY/SD/SOK scratch arrays
149func box_corners(base: i64, camx: i64, camy: i64, camz: i64, x0: i64, y0: i64, z0: i64, sw: i64, sh: i64, sl: i64) -> i64 {
150 let SX: *i64 = (base + O_SX) as *i64
151 let SY: *i64 = (base + O_SY) as *i64
152 let SD: *i64 = (base + O_SD) as *i64
153 let SOK: *i64 = (base + O_SOK) as *i64
154 let pj: *i64 = (base + O_PJ) as *i64
155 var k: i64 = 0
156 while k < 8 {
157 let dx: i64 = (k & 1) * sw
158 let dy: i64 = ((k >> 1) & 1) * sh
159 let dz: i64 = ((k >> 2) & 1) * sl
160 project(base, camx, camy, camz, x0 + dx, y0 + dy, z0 + dz)
161 SX[k] = pj[0]; SY[k] = pj[1]; SD[k] = pj[2]; SOK[k] = pj[3]
162 k = k + 1
163 }
164 return 0
165}
166func draw_face(base: i64, a: i64, b: i64, c: i64, dd: i64, lnx: i64, lny: i64, lnz: i64, col: i64) -> i64 {
167 let SX: *i64 = (base + O_SX) as *i64
168 let SY: *i64 = (base + O_SY) as *i64
169 let SD: *i64 = (base + O_SD) as *i64
170 let SOK: *i64 = (base + O_SOK) as *i64
171 if SOK[a] == 0 { return 0 }
172 if SOK[b] == 0 { return 0 }
173 if SOK[c] == 0 { return 0 }
174 if SOK[dd] == 0 { return 0 }
175 let fc: i64 = packc(col, shade(lnx, lny, lnz))
176 fill_z(base, SX[a], SY[a], SD[a], SX[b], SY[b], SD[b], SX[c], SY[c], SD[c], fc)
177 fill_z(base, SX[a], SY[a], SD[a], SX[c], SY[c], SD[c], SX[dd], SY[dd], SD[dd], fc)
178 return 0
179}
180func draw_col(base: i64, camx: i64, camy: i64, camz: i64, gx: i64, gz: i64) -> i64 {
181 let h: i64 = terrain_h(base, gx, gz)
182 box_corners(base, camx, camy, camz, gx, 0, gz, 1, h, 1)
183 draw_face(base, 2, 3, 7, 6, 0, 1, 0, top_color(h))
184 if hN(base, gx + 1, gz) < h { draw_face(base, 1, 5, 7, 3, 1, 0, 0, side_color(h)) }
185 if hN(base, gx - 1, gz) < h { draw_face(base, 0, 2, 6, 4, 0 - 1, 0, 0, side_color(h)) }
186 if hN(base, gx, gz + 1) < h { draw_face(base, 4, 5, 7, 6, 0, 0, 1, side_color(h)) }
187 if hN(base, gx, gz - 1) < h { draw_face(base, 0, 1, 3, 2, 0, 0, 0 - 1, side_color(h)) }
188 return 0
189}
190func has_tree(base: i64, gx: i64, gz: i64) -> i64 {
191 let h: i64 = terrain_h(base, gx, gz)
192 if h <= WWATER { return 0 }
193 if h >= WMAXH - 1 { return 0 }
194 var n: i64 = (gx * 131 + gz * 197 + gx * gz * 7 + 13) % 17
195 if n < 0 { n = n + 17 }
196 if n == 0 { return 1 }
197 return 0
198}
199func draw_box(base: i64, camx: i64, camy: i64, camz: i64, x0: i64, y0: i64, z0: i64, sw: i64, sh: i64, sl: i64, col: i64) -> i64 {
200 box_corners(base, camx, camy, camz, x0, y0, z0, sw, sh, sl)
201 draw_face(base, 2, 3, 7, 6, 0, 1, 0, col)
202 draw_face(base, 1, 5, 7, 3, 1, 0, 0, col)
203 draw_face(base, 0, 2, 6, 4, 0 - 1, 0, 0, col)
204 draw_face(base, 4, 5, 7, 6, 0, 0, 1, col)
205 draw_face(base, 0, 1, 3, 2, 0, 0, 0 - 1, col)
206 return 0
207}
208func draw_tree(base: i64, camx: i64, camy: i64, camz: i64, gx: i64, gz: i64) -> i64 {
209 let h: i64 = terrain_h(base, gx, gz)
210 draw_box(base, camx, camy, camz, gx, h, gz, 1, 3, 1, 92 + 64 * 256 + 38 * O_MAGIC_65536) // trunk (brown)
211 draw_box(base, camx, camy, camz, gx - 1, h + 3, gz - 1, 3, 3, 3, 40 + 122 * 256 + 50 * O_MAGIC_65536) // leaves (green)
212 return 0
213}
214func render_at(base: i64, camx: i64, camy: i64, camz: i64, cgx: i64, cgz: i64) -> i64 {
215 let fb: *i64 = (base + O_FB) as *i64
216 let zb: *i64 = (base + O_ZB) as *i64
217 let sky: i64 = 142 + 186 * 256 + 228 * O_MAGIC_65536
218 var i: i64 = 0
219 while i < W * H { fb[i] = sky; zb[i] = ZFAR; i = i + 1 }
220 var gz: i64 = cgz - VDIST
221 while gz <= cgz + VDIST {
222 if gz >= 0 { if gz < WN {
223 var gx: i64 = cgx - VDIST
224 while gx <= cgx + VDIST {
225 if gx >= 0 { if gx < WN {
226 draw_col(base, camx, camy, camz, gx, gz)
227 if has_tree(base, gx, gz) == 1 { draw_tree(base, camx, camy, camz, gx, gz) }
228 } }
229 gx = gx + 1
230 }
231 } }
232 gz = gz + 1
233 }
234 return 0
235}
236func build_R(base: i64, ycos: i64, ysin: i64, pcos: i64, psin: i64) -> i64 {
237 let Ry: *i64 = (base + O_RY) as *i64
238 let Rx: *i64 = (base + O_RX) as *i64
239 let R: *i64 = (base + O_R) as *i64
240 m4_roty(fratio(ycos, 1000), fratio(ysin, 1000), Ry)
241 m4_rotx(fratio(pcos, 1000), f32_neg(fratio(psin, 1000)), Rx)
242 m4_mul(Rx, Ry, R)
243 return 0
244}
245func mem_bytes() -> i64 { return O_DELTA + WN * WN * 8 + 64 }
246// WASM export: camera pos in 1/256 fixed-point ints, yaw/pitch cos/sin in 1/1000 ints (JS owns trig).
247func render(camx256: i64, camy256: i64, camz256: i64, ycos: i64, ysin: i64, pcos: i64, psin: i64) -> i64 {
248 build_R(0, ycos, ysin, pcos, psin)
249 let camx: i64 = f32_div(f32_of(camx256), f32_of(256))
250 let camy: i64 = f32_div(f32_of(camy256), f32_of(256))
251 let camz: i64 = f32_div(f32_of(camz256), f32_of(256))
252 render_at(0, camx, camy, camz, camx256 / 256, camz256 / 256)
253 return 0
254}
255func terrain_at(gx: i64, gz: i64) -> i64 { return terrain_h(0, gx, gz) }
256// block edit: adjust a column's height delta (base-relative so the native gate can verify edits by PNG)
257func edit_delta(base: i64, gx: i64, gz: i64, change: i64) -> i64 {
258 if gx < 0 { return 0 }
259 if gx >= WN { return 0 }
260 if gz < 0 { return 0 }
261 if gz >= WN { return 0 }
262 let dl: *i64 = (base + O_DELTA) as *i64
263 dl[gz * WN + gx] = dl[gz * WN + gx] + change
264 return 0
265}
266func break_block(gx: i64, gz: i64) -> i64 { return edit_delta(0, gx, gz, 0 - 1) }
267func place_block(gx: i64, gz: i64) -> i64 { return edit_delta(0, gx, gz, 1) }
268// raycast from camera along (dir/1000); return the first column hit packed (gx*65536)+gz, or -1
269func pick(camx256: i64, camy256: i64, camz256: i64, dx1000: i64, dy1000: i64, dz1000: i64) -> i64 {
270 var px: i64 = camx256
271 var py: i64 = camy256
272 var pz: i64 = camz256
273 var i: i64 = 0
274 while i < 90 {
275 px = px + dx1000 * 48 / 1000
276 py = py + dy1000 * 48 / 1000
277 pz = pz + dz1000 * 48 / 1000
278 let gx: i64 = px / 256
279 let gz: i64 = pz / 256
280 if gx >= 0 { if gx < WN { if gz >= 0 { if gz < WN {
281 if py / 256 < terrain_h(0, gx, gz) { return (gx * O_MAGIC_65536) + gz }
282 } } } }
283 i = i + 1
284 }
285 return 0 - 1
286}
287func fb_off() -> i64 { return O_FB }
288func ww() -> i64 { return W }
289func hh() -> i64 { return H }
290func main() -> i64 { return 0 }