nx_gfx_iso.nx source
↩ module page · 313 lines · 15179 B
1// nx_gfx_iso.nx -- Renders isometric diamond tiles with shaded gradients and dynamic lighting on a provided framebuffer.
2const K_MAGIC_374761393: i64 = 374761393
3const K_MAGIC_668265263: i64 = 668265263
4const K_MAGIC_1274126177: i64 = 1274126177
5// nx_gfx_iso.nx -- Diablo-generation graphics primitives, sovereign integer / no-float, packed-RGB i64 fb.
6// The look of that era = (1) ISOMETRIC projection: 2:1 diamond floor tiles + 2.5D cube walls; (2) SHADED
7// sprites: figures with light-from-above gradients + soft drop shadows, not flat blobs; (3) DYNAMIC RADIAL
8// LIGHTING: the dark dungeon lit only within a torch radius, warm falloff -> ambient gloom. Everything here
9// operates on a caller-provided framebuffer (fb: *i64, W, H) so it composes with our existing raster + nx_png.
10// license_tier: ORIGINAL
11const TW: i64 = 32 // tile width (iso diamond)
12const TH: i64 = 16 // tile height (2:1)
13
14func gx_rgb(r: i64, g: i64, b: i64) -> i64 { return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16) }
15func gx_cr(c: i64) -> i64 { return c & 255 }
16func gx_cg(c: i64) -> i64 { return (c >> 8) & 255 }
17func gx_cb(c: i64) -> i64 { return (c >> 16) & 255 }
18func gx_cl8(v: i64) -> i64 { if v < 0 { return 0 } if v > 255 { return 255 } return v }
19
20// scale a colour by f/256 (lighting + shading)
21func gx_shade(c: i64, f: i64) -> i64 { return gx_rgb((gx_cr(c)*f)>>8, (gx_cg(c)*f)>>8, (gx_cb(c)*f)>>8) }
22// scale per-channel (warm/cool tint): fr,fg,fb each /256
23func gx_tint(c: i64, fr: i64, fg: i64, fb: i64) -> i64 { return gx_rgb(gx_cl8((gx_cr(c)*fr)>>8), gx_cl8((gx_cg(c)*fg)>>8), gx_cl8((gx_cb(c)*fb)>>8)) }
24// lerp t/256 from c1 to c2
25func gx_mix(c1: i64, c2: i64, t: i64) -> i64 {
26 let r: i64 = gx_cr(c1) + (((gx_cr(c2)-gx_cr(c1))*t)>>8)
27 let g: i64 = gx_cg(c1) + (((gx_cg(c2)-gx_cg(c1))*t)>>8)
28 let b: i64 = gx_cb(c1) + (((gx_cb(c2)-gx_cb(c1))*t)>>8)
29 return gx_rgb(r, g, b)
30}
31
32func gx_px(fb: *i64, W: i64, H: i64, x: i64, y: i64, c: i64) -> i64 {
33 if x < 0 { return 0 } if y < 0 { return 0 } if x >= W { return 0 } if y >= H { return 0 }
34 fb[y*W + x] = c; return 0
35}
36func gx_blend(fb: *i64, W: i64, H: i64, x: i64, y: i64, c: i64, a: i64) -> i64 {
37 if x < 0 { return 0 } if y < 0 { return 0 } if x >= W { return 0 } if y >= H { return 0 }
38 let o: i64 = fb[y*W + x]
39 fb[y*W + x] = gx_mix(o, c, a); return 0
40}
41func gx_hspan(fb: *i64, W: i64, H: i64, x0: i64, x1: i64, y: i64, c: i64) -> i64 {
42 if y < 0 { return 0 } if y >= H { return 0 }
43 var x: i64 = x0; if x < 0 { x = 0 }
44 var xe: i64 = x1; if xe >= W { xe = W - 1 }
45 while x <= xe { fb[y*W + x] = c; x = x + 1 }
46 return 0
47}
48func gx_fill(fb: *i64, W: i64, H: i64, c: i64) -> i64 { var p: i64 = 0; while p < W*H { fb[p] = c; p = p + 1 } return 0 }
49
50// ---- isometric projection: tile (tx,ty) -> screen TOP vertex of its diamond ----
51func gx_isox(tx: i64, ty: i64, ox: i64) -> i64 { return ox + (tx - ty)*(TW/2) }
52func gx_isoy(tx: i64, ty: i64, oy: i64) -> i64 { return oy + (tx + ty)*(TH/2) }
53
54// shaded diamond floor tile; (cx,sy) = top vertex. Light from above -> brighter top, darker bottom.
55func gx_iso_tile(fb: *i64, W: i64, H: i64, cx: i64, sy: i64, col: i64) -> i64 {
56 var dy: i64 = 0
57 while dy < TH {
58 var hw: i64 = dy*2 + 1; if dy >= TH/2 { hw = (TH - dy)*2 + 1 } // +1 overlap -> no inter-tile gaps
59 let f: i64 = 230 - dy*3 // subtle top-light gradient
60 gx_hspan(fb, W, H, cx - hw, cx + hw, sy + dy, gx_shade(col, f))
61 dy = dy + 1
62 }
63 return 0
64}
65// 1px darker rim on a tile's lower edges -> grid definition
66func gx_iso_tile_rim(fb: *i64, W: i64, H: i64, cx: i64, sy: i64, col: i64) -> i64 {
67 let rim: i64 = gx_shade(col, 150)
68 var i: i64 = 0
69 while i <= TW/2 {
70 gx_px(fb, W, H, cx - i, sy + TH/2 + (i/2), rim)
71 gx_px(fb, W, H, cx + i, sy + TH/2 + (i/2), rim)
72 i = i + 1
73 }
74 return 0
75}
76
77// ---- PROCEDURAL SURFACE TEXTURE (integer hash grain: flat blobs -> detailed stone, deterministic, NO float/assets) ----
78func gx_hash(x: i64, y: i64) -> i64 {
79 var h: i64 = x*K_MAGIC_374761393 + y*K_MAGIC_668265263
80 h = (h ^ (h >> 13)) * K_MAGIC_1274126177
81 h = h ^ (h >> 16)
82 if h < 0 { h = 0 - h }
83 return h & 255
84}
85// textured floor tile: per-pixel stone grain modulates the top-light gradient (vs gx_iso_tile's flat gradient spans).
86func gx_iso_tile_tex(fb: *i64, W: i64, H: i64, cx: i64, sy: i64, col: i64) -> i64 {
87 var dy: i64 = 0
88 while dy < TH {
89 var hw: i64 = dy*2 + 1; if dy >= TH/2 { hw = (TH - dy)*2 + 1 }
90 let basef: i64 = 230 - dy*3
91 var x: i64 = cx - hw
92 while x <= cx + hw {
93 let g: i64 = (gx_hash(x, sy+dy) % 44) - 22 // stone grain -22..+21 per pixel
94 gx_px(fb, W, H, x, sy+dy, gx_shade(col, basef + g))
95 x = x + 1
96 }
97 dy = dy + 1
98 }
99 return 0
100}
101
102// 2.5D cube (wall / pillar): two shaded side faces hanging from the top diamond's lower edges, then the top.
103func gx_iso_cube(fb: *i64, W: i64, H: i64, cx: i64, sy: i64, ht: i64, topcol: i64) -> i64 {
104 let lcol: i64 = gx_shade(topcol, 120) // left face (shadow side)
105 let rcol: i64 = gx_shade(topcol, 180) // right face
106 var x: i64 = 0 - TW/2
107 while x <= 0 { // left face columns
108 let ey: i64 = sy + TH/2 + ((x + TW/2)*1)/2 // diamond lower-left edge y at this column
109 var y: i64 = ey
110 while y < ey + ht { gx_px(fb, W, H, cx + x, y, gx_shade(lcol, 256 - (y-ey))); y = y + 1 }
111 x = x + 1
112 }
113 x = 0
114 while x <= TW/2 { // right face columns
115 let ey: i64 = sy + TH - ((x)*1)/2
116 var y: i64 = ey
117 while y < ey + ht { gx_px(fb, W, H, cx + x, y, gx_shade(rcol, 256 - (y-ey))); y = y + 1 }
118 x = x + 1
119 }
120 gx_iso_tile(fb, W, H, cx, sy, topcol) // bright top
121 return 0
122}
123
124// textured 2.5D cube: per-pixel stone grain + darker mortar courses (brick blocks) on each side face; textured top.
125func gx_isc_f(row: i64, g: i64) -> i64 { // vertical shade + grain + mortar course, clamped
126 var f: i64 = 256 - row + g
127 if (row % 6) == 0 { f = f - 60 } // mortar line every 6 rows -> stone-block courses
128 if f > 256 { f = 256 } if f < 40 { f = 40 }
129 return f
130}
131func gx_iso_cube_tex(fb: *i64, W: i64, H: i64, cx: i64, sy: i64, ht: i64, topcol: i64) -> i64 {
132 let lcol: i64 = gx_shade(topcol, 120)
133 let rcol: i64 = gx_shade(topcol, 180)
134 var x: i64 = 0 - TW/2
135 while x <= 0 {
136 let ey: i64 = sy + TH/2 + ((x + TW/2)*1)/2
137 var y: i64 = ey
138 while y < ey + ht { let g: i64 = (gx_hash(cx+x, y) % 40) - 20; gx_px(fb, W, H, cx + x, y, gx_shade(lcol, gx_isc_f(y-ey, g))); y = y + 1 }
139 x = x + 1
140 }
141 x = 0
142 while x <= TW/2 {
143 let ey: i64 = sy + TH - ((x)*1)/2
144 var y: i64 = ey
145 while y < ey + ht { let g: i64 = (gx_hash(cx+x, y) % 40) - 20; gx_px(fb, W, H, cx + x, y, gx_shade(rcol, gx_isc_f(y-ey, g))); y = y + 1 }
146 x = x + 1
147 }
148 gx_iso_tile_tex(fb, W, H, cx, sy, topcol)
149 return 0
150}
151
152// BLOOM (atmospheric glow): extract bright pixels -> separable integer box-blur -> add back. No float. The classic
153// post-effect that reads as "modern" -- the torch/gold/lit surfaces bleed a soft warm halo into the dark.
154func gx_bloom(fb: *i64, W: i64, H: i64, thresh: i64, gain: i64) -> i64 {
155 let glow: *i64 = sys_mmap(W*H*8) as *i64
156 let tmp: *i64 = sys_mmap(W*H*8) as *i64
157 var p: i64 = 0
158 while p < W*H { let c: i64 = fb[p]; if gx_cr(c)+gx_cg(c)+gx_cb(c) > thresh { glow[p] = c } else { glow[p] = 0 } p = p + 1 }
159 var pass: i64 = 0
160 while pass < 2 {
161 var y: i64 = 0
162 while y < H { // horizontal blur glow -> tmp (radius 4)
163 var x: i64 = 0
164 while x < W {
165 var rs: i64=0; var gs: i64=0; var bs: i64=0; var n: i64=0; var dx: i64=0-4
166 while dx <= 4 { let xx: i64=x+dx; if xx>=0 { if xx<W { let c: i64=glow[y*W+xx]; rs=rs+gx_cr(c); gs=gs+gx_cg(c); bs=bs+gx_cb(c); n=n+1 } } dx=dx+1 }
167 tmp[y*W+x] = gx_rgb(rs/n, gs/n, bs/n); x = x + 1
168 }
169 y = y + 1
170 }
171 var y2: i64 = 0
172 while y2 < H { // vertical blur tmp -> glow
173 var x2: i64 = 0
174 while x2 < W {
175 var rs2: i64=0; var gs2: i64=0; var bs2: i64=0; var n2: i64=0; var dy: i64=0-4
176 while dy <= 4 { let yy: i64=y2+dy; if yy>=0 { if yy<H { let c: i64=tmp[yy*W+x2]; rs2=rs2+gx_cr(c); gs2=gs2+gx_cg(c); bs2=bs2+gx_cb(c); n2=n2+1 } } dy=dy+1 }
177 glow[y2*W+x2] = gx_rgb(rs2/n2, gs2/n2, bs2/n2); x2 = x2 + 1
178 }
179 y2 = y2 + 1
180 }
181 pass = pass + 1
182 }
183 var q: i64 = 0
184 while q < W*H {
185 let c: i64=fb[q]; let g: i64=glow[q]
186 fb[q] = gx_rgb(gx_cl8(gx_cr(c) + ((gx_cr(g)*gain)>>8)), gx_cl8(gx_cg(c) + ((gx_cg(g)*gain)>>8)), gx_cl8(gx_cb(c) + ((gx_cb(g)*gain)>>8)))
187 q = q + 1
188 }
189 return 0
190}
191
192// filled ellipse (cross-multiplied test, no sqrt)
193func gx_ellipse(fb: *i64, W: i64, H: i64, cx: i64, cy: i64, rx: i64, ry: i64, col: i64) -> i64 {
194 let rx2: i64 = rx*rx; let ry2: i64 = ry*ry; let lim: i64 = rx2*ry2
195 var dy: i64 = 0 - ry
196 while dy <= ry {
197 var dx: i64 = 0 - rx
198 while dx <= rx { if dx*dx*ry2 + dy*dy*rx2 <= lim { gx_px(fb, W, H, cx+dx, cy+dy, col) } dx = dx + 1 }
199 dy = dy + 1
200 }
201 return 0
202}
203func gx_ellipse_blend(fb: *i64, W: i64, H: i64, cx: i64, cy: i64, rx: i64, ry: i64, col: i64, a: i64) -> i64 {
204 let rx2: i64 = rx*rx; let ry2: i64 = ry*ry; let lim: i64 = rx2*ry2
205 var dy: i64 = 0 - ry
206 while dy <= ry {
207 var dx: i64 = 0 - rx
208 while dx <= rx { if dx*dx*ry2 + dy*dy*rx2 <= lim { gx_blend(fb, W, H, cx+dx, cy+dy, col, a) } dx = dx + 1 }
209 dy = dy + 1
210 }
211 return 0
212}
213
214// vertical gradient rect
215func gx_vgrad(fb: *i64, W: i64, H: i64, x0: i64, y0: i64, x1: i64, y1: i64, ctop: i64, cbot: i64) -> i64 {
216 var y: i64 = y0
217 while y < y1 {
218 var t: i64 = 0; if y1 > y0 + 1 { t = ((y - y0)*256)/(y1 - y0) }
219 gx_hspan(fb, W, H, x0, x1 - 1, y, gx_mix(ctop, cbot, t))
220 y = y + 1
221 }
222 return 0
223}
224
225// shaded humanoid sprite standing at feet (fx,fy): soft drop shadow, shaded torso (light upper-left),
226// shaded head, a highlight. Recognizable figure, not a flat rect.
227// phase 0 = idle; phase>0 animates a walk cycle (body bob + leg scissor) from a tick counter.
228func gx_sprite_humanoid(fb: *i64, W: i64, H: i64, fx: i64, fy: i64, bodycol: i64, skincol: i64, phase: i64) -> i64 {
229 let white: i64 = gx_rgb(255,255,255)
230 let dark: i64 = gx_shade(bodycol, 120)
231 let lite: i64 = gx_mix(bodycol, white, 85)
232 let ph: i64 = phase & 31
233 var bob: i64 = ph; if ph >= 16 { bob = 32 - ph } // triangle 0..16..0
234 bob = bob >> 3 // 0..2 px body bob
235 var legoff: i64 = 2; if ph >= 16 { legoff = 0 - 2 }
236 if phase == 0 { legoff = 0; bob = 0 }
237 let fyb: i64 = fy - bob // bobbing upper body
238 gx_ellipse_blend(fb, W, H, fx, fy, 13, 5, gx_rgb(0,0,0), 150) // soft drop shadow (ground)
239 // legs (scissoring) -- bottoms at the ground (fy), tops at the waist
240 gx_vgrad(fb, W, H, fx-6+legoff, fyb-13, fx-1+legoff, fy, gx_mix(dark, white, 30), gx_shade(dark, 150))
241 gx_vgrad(fb, W, H, fx+1-legoff, fyb-13, fx+6-legoff, fy, gx_shade(dark, 200), gx_shade(dark, 120))
242 // torso (fyb-41..fyb-13): broad shoulders tapering to the waist, light from upper-left
243 var yy: i64 = fyb - 41
244 while yy < fyb - 13 {
245 let prog: i64 = ((yy - (fyb-41))*256)/28 // 0 shoulders -> 256 waist
246 let hw: i64 = 12 - (prog*5)/256
247 let col: i64 = gx_mix(lite, dark, prog)
248 var dx: i64 = 0 - hw
249 while dx <= hw {
250 var cc: i64 = col
251 if dx > hw - 3 { cc = gx_shade(col, 135) } // right-side shadow
252 if dx < 0 - hw + 3 { cc = gx_mix(col, white, 70) } // left highlight
253 gx_px(fb, W, H, fx + dx, yy, cc); dx = dx + 1
254 }
255 yy = yy + 1
256 }
257 // belt
258 gx_hspan(fb, W, H, fx-7, fx+7, fyb-14, gx_shade(gx_rgb(120,90,40), 220))
259 gx_hspan(fb, W, H, fx-7, fx+7, fyb-13, gx_rgb(120,90,40))
260 // head (shaded sphere)
261 let hy: i64 = fyb - 49
262 gx_ellipse(fb, W, H, fx, hy, 6, 7, gx_shade(skincol, 150))
263 gx_ellipse(fb, W, H, fx-1, hy-1, 5, 6, skincol)
264 gx_ellipse(fb, W, H, fx-2, hy-2, 2, 2, gx_mix(skincol, white, 110)) // forehead highlight
265 // helmet / hood cap
266 gx_ellipse(fb, W, H, fx, hy-4, 6, 4, gx_shade(bodycol, 95))
267 gx_ellipse(fb, W, H, fx-2, hy-5, 2, 2, gx_mix(bodycol, white, 50))
268 return 0
269}
270
271// ---- THE signature pass: dynamic radial lighting. Darkens the whole fb by distance to a light, smooth
272// quadratic falloff to an ambient floor, with a warm torch tint near the centre. Call once per light pass. ----
273func gx_light_radial(fb: *i64, W: i64, H: i64, lx: i64, ly: i64, radius: i64, ambient: i64) -> i64 {
274 let r2: i64 = radius*radius
275 let scale: i64 = ((256 - ambient)*256)/r2 // precomputed once -> no per-pixel divide
276 var y: i64 = 0
277 while y < H {
278 var x: i64 = 0
279 while x < W {
280 let dx: i64 = x - lx; let dy: i64 = y - ly; let d2: i64 = dx*dx + dy*dy
281 var b: i64 = ambient
282 if d2 < r2 { b = ambient + (((r2 - d2)*scale)>>8) }
283 let src: i64 = fb[y*W + x]
284 if b >= 250 { fb[y*W + x] = src } else {
285 // warm tint: boost R, slight G, damp B as it brightens above ambient
286 let warm: i64 = (b - ambient); var wr: i64 = b + warm/3; if wr > 256 { wr = 256 }
287 fb[y*W + x] = gx_tint(src, wr, b, (b*3)/4 + ambient/4)
288 }
289 x = x + 1
290 }
291 y = y + 1
292 }
293 return 0
294}
295// additive coloured point light (item glow / spell) -- brightens toward a tint without darkening elsewhere.
296func gx_light_add(fb: *i64, W: i64, H: i64, lx: i64, ly: i64, radius: i64, tint: i64) -> i64 {
297 let r2: i64 = radius*radius
298 var y: i64 = ly - radius; if y < 0 { y = 0 }
299 let ye: i64 = ly + radius
300 while y < ye { if y < H {
301 var x: i64 = lx - radius; if x < 0 { x = 0 }
302 let xe: i64 = lx + radius
303 while x < xe { if x < W {
304 let dx: i64 = x - lx; let dy: i64 = y - ly; let d2: i64 = dx*dx + dy*dy
305 if d2 < r2 { let k: i64 = ((r2 - d2)*256)/r2
306 let s: i64 = fb[y*W + x]
307 fb[y*W + x] = gx_rgb(gx_cl8(gx_cr(s) + (gx_cr(tint)*k)>>8), gx_cl8(gx_cg(s) + (gx_cg(tint)*k)>>8), gx_cl8(gx_cb(s) + (gx_cb(tint)*k)>>8)) }
308 x = x + 1
309 } }
310 y = y + 1
311 } }
312 return 0
313}