code wiki / (root) / nx_gfx_iso.nx

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}