~/bend-docscommunity

render.bend source

render.bend on the hub · documented module

import Base# Minecraft-clone software raycaster (256x256 Image quadtree, depth 8).# Pure math + shading + image building live here. Voxel access comes in# via closed templates (~get, ~color_of) supplied by game.bend, so this# file has no imports besides Base (no circular imports, no Arrays).type Cam is Data:  C{px: F32, py: F32, pz: F32, yaw: F32, pitch: F32, fov: F32}type Ray is Data:  R{ox: F32, oy: F32, oz: F32, dx: F32, dy: F32, dz: F32}# Camera constructor helper.def Render.cam_from(px: F32, py: F32, pz: F32, yaw: F32, pitch: F32, fov: F32) -> Cam:  C{px, py, pz, yaw, pitch, fov}# Division guard: replace 0.0 with a small epsilon (keeps math total).def Render.nonzero_go(x: F32, z: Bool) -> F32:  match z:    case False{}:      x    case True{}:      0.0001def Render.nonzero(+x: F32) -> F32:  Render.nonzero_go(x, F32.is_eq(x, 0.0))# Extract byte k (0n=B, 8n=G, 16n=R) from a packed 0xRRGGBB color.def Render.chan(c: U32, k: Nat) -> U32:  U32.and(U32.shrn(c, k), 255)# Pack R/G/B bytes into 0xRRGGBB.def Render.pack(r: U32, g: U32, b: U32) -> U32:  hi = U32.shln(r, 16n)  md = U32.shln(g, 8n)  U32.or(hi, U32.or(md, b))# Lerp one channel a -> b by t (t in 0..1).def Render.mix_chan(a: U32, b: U32, t: F32) -> U32:  +af = U32.to_f32(a)  bf = U32.to_f32(b)  F32.to_u32(F32.clamp((af + ((bf - af : F32) * t : F32) : F32), 0.0, 255.0))# Lerp packed colors c0 -> c1 by t.def Render.mix_rgb(+c0: U32, +c1: U32, +t: F32) -> U32:  r g = Render.mix_chan(Render.chan(c0, 16n), Render.chan(c1, 16n), t) Render.mix_chan(Render.chan(c0, 8n), Render.chan(c1, 8n), t)  b = Render.mix_chan(Render.chan(c0, 0n), Render.chan(c1, 0n), t)  Render.pack(r, g, b)# Scale one channel by brightness b.def Render.shade_chan(c: U32, k: Nat, b: F32) -> U32:  F32.to_u32(F32.clamp((U32.to_f32(Render.chan(c, k)) * b : F32), 0.0, 255.0))# Distance fog: brightness 1.0 -> 0.2 over ~64 steps, applied per channel.def Render.shade(+c: U32, steps: U32) -> U32:  +br = F32.clamp((1.0 - (U32.to_f32(steps) * 0.012 : F32) : F32), 0.2, 1.0)  r g = Render.shade_chan(c, 16n, br) Render.shade_chan(c, 8n, br)  b = Render.shade_chan(c, 0n, br)  Render.pack(r, g, b)# Sky gradient: day (horizon 0x87CEEB -> zenith 0x3B7DDD) lerped to# night 0x0B1026 by a daylight factor from time (day cycle 0..1).def Render.sky(dx: F32, dy: F32, dz: F32, time: F32) -> U32:  up = F32.clamp(dy, 0.0, 1.0)  day = Render.mix_rgb(8900331, 3898845, up)  ang = (time * 6.2831853 : F32)  e = F32.sin(ang)  dl = F32.clamp(((e + 0.25 : F32) * 2.0 : F32), 0.0, 1.0)  Render.mix_rgb(725030, day, dl)# Primary ray through pixel (u, v) of a w-by-h frame.# NDC = (u/w*2-1)*aspect etc., pinhole with tan(fov/2), then yaw (Y)# and pitch (X) rotation, normalized. All divisions epsilon-guarded.def Render.pixel_ray(cam: Cam, u: F32, v: F32, w: U32, h: U32) -> Ray:  C{px, py, pz, +yaw, +pitch, fov} = cam  +ws = Render.nonzero(U32.to_f32(w))  +hs = Render.nonzero(U32.to_f32(h))  aspect = (ws / hs : F32)  qx = (u / ws : F32)  qy = (v / hs : F32)  nx = (((qx * 2.0 : F32) - 1.0 : F32) * aspect : F32)  ny = (1.0 - (qy * 2.0 : F32) : F32)  +t = F32.tan((fov / 2.0 : F32))  +cx = (nx * t : F32)  +cy = (ny * t : F32)  +cz = (0.0 - 1.0 : F32)  +sp = F32.sin(pitch)  +cp = F32.cos(pitch)  +sa = F32.sin(yaw)  +ca = F32.cos(yaw)  a1 = (cy * cp : F32)  a2 = (cz * sp : F32)  y1 = (a1 - a2 : F32)  b1 = (cy * sp : F32)  b2 = (cz * cp : F32)  +z1 = (b1 + b2 : F32)  c1 = (cx * ca : F32)  c2 = (z1 * sa : F32)  +x2 = (c1 + c2 : F32)  d1 = (z1 * ca : F32)  d2 = (cx * sa : F32)  +z2 = (d1 - d2 : F32)  +y2 = y1  e1 = (x2 * x2 : F32)  e2 = (y2 * y2 : F32)  e3 = (z2 * z2 : F32)  +len = Render.nonzero(F32.sqrt((e1 + (e2 + e3 : F32) : F32)))  dx = (x2 / len : F32)  dy = (y2 / len : F32)  dz = (z2 / len : F32)  R{px, py, pz, dx, dy, dz}# Crosshair test: within 2px of frame center.def Render.is_cross(+x: U32, +y: U32, +w: U32, +h: U32) -> Bool:  +cx = U32.div(w, 2)  +cy = U32.div(h, 2)  nx = Bool.and(U32.is_ge(x, U32.sub(cx, 2)), U32.is_le(x, U32.add(cx, 2)))  ny = Bool.and(U32.is_ge(y, U32.sub(cy, 2)), U32.is_le(y, U32.add(cy, 2)))  Bool.and(nx, ny)# Fixed-step raymarcher (0.25 voxels/step). fuel bounds the steps, so it# is termination-safe. Hit => fog-shaded palette color, miss => sky.# NOTE: ~get must answer 0 for out-of-bounds coords (including the huge# U32 values that negative F32 positions convert to).def Render.march(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, fuel: Nat, bid: U32, empty: Bool, px: F32, py: F32, pz: F32, +dx: F32, +dy: F32, +dz: F32, +steps: U32, +time: F32) -> U32:  match fuel empty:    case 0n False{}:      Render.sky(dx, dy, dz, time)    case 0n True{}:      Render.sky(dx, dy, dz, time)    case 1n+f False{}:      Render.shade(color_of(bid), steps)    case 1n+f True{}:      +nx = (px + (dx * 0.25 : F32) : F32)      +ny = (py + (dy * 0.25 : F32) : F32)      +nz = (pz + (dz * 0.25 : F32) : F32)      +nb = get(F32.to_u32(F32.floor(nx)), F32.to_u32(F32.floor(ny)), F32.to_u32(F32.floor(nz)))      Render.march(~get, ~color_of, f, nb, U32.is_zero(nb), nx, ny, nz, dx, dy, dz, (steps + 1 : U32), time)# Entry point game.bend calls: march a ray, return final U32 color.def Render.trace(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, fuel: Nat, ox: F32, oy: F32, oz: F32, dx: F32, dy: F32, dz: F32, time: F32) -> U32:  Render.march(~get, ~color_of, fuel, 0, True{}, ox, oy, oz, dx, dy, dz, 0, time)# One framebuffer pixel: ray + trace + white crosshair dot.def Render.pixel_go(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, r: Ray, +x: U32, +y: U32, +w: U32, +h: U32, +time: F32) -> U32:  match r:    case R{ox, oy, oz, dx, dy, dz}:      t = Render.trace(~get, ~color_of, 64n, ox, oy, oz, dx, dy, dz, time)      Bool.pick(U32, Render.is_cross(x, y, w, h), 16777215, t)def Render.pixel(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, cam: Cam, +x: U32, +y: U32, +w: U32, +h: U32, +time: F32) -> U32:  Render.pixel_go(~get, ~color_of, Render.pixel_ray(cam, U32.to_f32(x), U32.to_f32(y), w, h), x, y, w, h, time)# Quadtree tile builder: size is depth (0n => one Pix). The 4 quadrants# are built with parallel lets; size shrinks first, so it terminates.def Render.build(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, +size: Nat, +cam: Cam, +x0: U32, +y0: U32, +w: U32, +h: U32, +time: F32) -> Image:  match size:    case 0n:      Pix{Render.pixel(~get, ~color_of, cam, x0, y0, w, h, time)}    case 1n+s:      +half = U32.pow(2, s)      +x1 = (x0 + half : U32)      +y1 = (y0 + half : U32)      tl tr = Render.build(~get, ~color_of, s, cam, x0, y0, w, h, time) Render.build(~get, ~color_of, s, cam, x1, y0, w, h, time)      bl br = Render.build(~get, ~color_of, s, cam, x0, y1, w, h, time) Render.build(~get, ~color_of, s, cam, x1, y1, w, h, time)      Qua{tl, tr, bl, br}# Full 256x256 frame (depth 8).def Render.frame(~get: U32 -> U32 -> U32 -> U32, ~color_of: U32 -> U32, cam: Cam, time: F32) -> Image:  Render.build(~get, ~color_of, 8n, cam, 0, 0, 256, 256, time)# ---- Additive pure helpers (appended; old defs untouched) ----def Render.face_br05(n: Nat) -> F32:  match n:    case 0n:      0.7    case 1n+p:      1.0def Render.face_br04(n: Nat) -> F32:  match n:    case 0n:      0.7    case 1n+p:      Render.face_br05(p)def Render.face_br03(n: Nat) -> F32:  match n:    case 0n:      0.8    case 1n+p:      Render.face_br04(p)def Render.face_br02(n: Nat) -> F32:  match n:    case 0n:      0.8    case 1n+p:      Render.face_br03(p)def Render.face_br01(n: Nat) -> F32:  match n:    case 0n:      0.55    case 1n+p:      Render.face_br02(p)def Render.face_br00(n: Nat) -> F32:  match n:    case 0n:      1.0    case 1n+p:      Render.face_br01(p)def Render.face_br(face: U32) -> F32:  Render.face_br00(U32.to_nat(face))def Render.face_shade(+color: U32, face: U32) -> U32:  +br = Render.face_br(face)  r g = Render.shade_chan(color, 16n, br) Render.shade_chan(color, 8n, br)  b = Render.shade_chan(color, 0n, br)  Render.pack(r, g, b)def Render.sun_dir(time: F32) -> F32 & F32 & F32:  +ang = (time * 6.2831853 : F32)  sx = F32.cos(ang)  sy = F32.sin(ang)  (sx, (sy, 0.0))def Render.sun_hit(dx: F32, dy: F32, dz: F32, sx: F32, sy: F32, sz: F32) -> Bool:  a1 = (dx * sx : F32)  a2 = (dy * sy : F32)  a3 = (dz * sz : F32)  d12 = (a1 + a2 : F32)  dot = (d12 + a3 : F32)  F32.is_ge(dot, 0.9995)def Render.sun_pick(hit: Bool, sky: U32) -> U32:  match hit:    case True{}:      16777215    case False{}:      skydef Render.sun_finish(sy: F32, sz: F32, sx: F32, +dx: F32, +dy: F32, +dz: F32, time: F32) -> U32:  hit = Render.sun_hit(dx, dy, dz, sx, sy, sz)  sky = Render.sky(dx, dy, dz, time)  Render.sun_pick(hit, sky)def Render.sun_mid(inner: F32 & F32, sx: F32, +dx: F32, +dy: F32, +dz: F32, time: F32) -> U32:  (sy, sz) = inner  Render.sun_finish(sy, sz, sx, dx, dy, dz, time)def Render.sun_split(sun: F32 & F32 & F32, +dx: F32, +dy: F32, +dz: F32, +time: F32) -> U32:  (sx, inner) = sun  Render.sun_mid(inner, sx, dx, dy, dz, time)def Render.sun_color(+dx: F32, +dy: F32, +dz: F32, +time: F32) -> U32:  Render.sun_split(Render.sun_dir(time), dx, dy, dz, time)def Render.cloud_pick(hit: Bool) -> Bool:  match hit:    case True{}:      True{}    case False{}:      False{}def Render.cloud_hit(x: F32, z: F32, time: F32) -> Bool:  t8 = (time * 8.0 : F32)  xs = (x + t8 : F32)  sc = (xs * 0.15 : F32)  u = F32.to_u32(sc)  m = U32.mod(u, 7)  Render.cloud_pick(U32.is_lt(m, 2))def Render.water_tint(+color: U32) -> U32:  r = Render.shade_chan(color, 16n, 0.7)  g = Render.shade_chan(color, 8n, 0.85)  b = Render.shade_chan(color, 0n, 1.0)  Render.pack(r, g, b)def Render.mix_hurt(hit: Bool, +color: U32) -> U32:  match hit:    case True{}:      Render.mix_rgb(color, 16711680, 0.5)    case False{}:      colordef Render.hurt_flash(+color: U32, ticks: U32) -> U32:  Render.mix_hurt(U32.is_gt(ticks, 0), color)def Render.vig_pick(edge: Bool, +color: U32) -> U32:  match edge:    case True{}:      r = Render.shade_chan(color, 16n, 0.75)      g = Render.shade_chan(color, 8n, 0.75)      b = Render.shade_chan(color, 0n, 0.75)      Render.pack(r, g, b)    case False{}:      colordef Render.vignette(+x: U32, +y: U32, +w: U32, +h: U32, +color: U32) -> U32:  +qx = U32.div(w, 4)  +qy = U32.div(h, 4)  rx = U32.mul(qx, 3)  ry = U32.mul(qy, 3)  lx = U32.is_lt(x, qx)  rxb = U32.is_ge(x, rx)  ty = U32.is_lt(y, qy)  by = U32.is_ge(y, ry)  e1 = Bool.or(lx, rxb)  e2 = Bool.or(ty, by)  edge = Bool.or(e1, e2)  Render.vig_pick(edge, color)def Render.particle04(n: Nat) -> U32:  match n:    case 0n:      16711935    case 1n+p:      16777215def Render.particle03(n: Nat) -> U32:  match n:    case 0n:      16753920    case 1n+p:      Render.particle04(p)def Render.particle02(n: Nat) -> U32:  match n:    case 0n:      3355443    case 1n+p:      Render.particle03(p)def Render.particle01(n: Nat) -> U32:  match n:    case 0n:      4197597    case 1n+p:      Render.particle02(p)def Render.particle00(n: Nat) -> U32:  match n:    case 0n:      11184810    case 1n+p:      Render.particle01(p)def Render.particle(+kind: U32, +life: U32) -> U32:  +base = Render.particle00(U32.to_nat(kind))  lf = U32.to_f32(life)  t = (lf / Render.nonzero(20.0) : F32)  f = (1.0 - t : F32)  +br = F32.clamp(f, 0.0, 1.0)  r g = Render.shade_chan(base, 16n, br) Render.shade_chan(base, 8n, br)  b = Render.shade_chan(base, 0n, br)  Render.pack(r, g, b)def Render.shade_face(color: U32, steps: U32, face: U32) -> U32:  Render.face_shade(Render.shade(color, steps), face)