From f4622d285dcc3df961d936b8518dd78ec9f4e2a4 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 14:53:16 +0100 Subject: [PATCH] Light the surroundings of lava in orange up to ten tiles Co-Authored-By: Claude Opus 5.5 --- src/render3d.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git a/src/render3d.rs b/src/render3d.rs index ff85847..68079c9 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -435,6 +435,75 @@ fn amb_at(f: &[f32], x: f32, z: f32) -> f32 { 0.15 + 0.45 * (a * (1.0 - fz) + b * fz) } +// ---- lava light: an orange glow up to LAVA_REACH tiles around lava, fading with the distance +const LAVA_REACH: f32 = 10.0; +const LAVA_LIGHT: Vec3 = Vec3::new(0.62, 0.27, 0.05); // added light at full strength (next to the lava) + +// glow per tile: 1 on lava, falling to 0 at LAVA_REACH tiles. The light spreads over open ground +// (walls are lit on their faces but do not let it through). +fn lava_field(map: &Map) -> Vec { + use std::cmp::Reverse; + use std::collections::BinaryHeap; + const STEP: u32 = 10; // distances in tenths of a tile + let max = (LAVA_REACH * STEP as f32) as u32; + let mut dist = vec![u32::MAX; n()]; + let mut heap = BinaryHeap::new(); + for (i, t) in map.t.iter().enumerate() { + if t.river() == Some(Kind::Lava) { + dist[i] = 0; + heap.push(Reverse((0u32, i))); + } + } + while let Some(Reverse((d, i))) = heap.pop() { + if d > dist[i] || (d > 0 && map.t[i].solid()) { + continue; // stale entry, or a wall: lit, but it does not pass the light on + } + let (x, y) = xy(i); + for (k, &(dx, dy)) in DIR8.iter().enumerate() { + let (nx, ny) = (x + dx, y + dy); + if !inb(nx, ny) { + continue; + } + let nd = d + if k >= 4 { 14 } else { STEP }; + let ni = idx(nx, ny); + if nd <= max && nd < dist[ni] { + dist[ni] = nd; + heap.push(Reverse((nd, ni))); + } + } + } + dist.iter() + .map(|&d| { + if d == u32::MAX { + 0.0 + } else { + let k = (1.0 - d as f32 / max as f32).clamp(0.0, 1.0); + k * k + } + }) + .collect() +} + +// glow at a point (bilinear between tile centres) +fn lava_at(f: &[f32], x: f32, z: f32) -> f32 { + if f.len() != n() { + return 0.0; + } + let gx = (x - 0.5).clamp(0.0, mw() as f32 - 1.001); + let gz = (z - 0.5).clamp(0.0, mh() as f32 - 1.001); + let (x0, z0) = (gx.floor() as i32, gz.floor() as i32); + let (fx, fz) = (gx - x0 as f32, gz - z0 as f32); + let g = |x: i32, z: i32| f[idx(x.min(mw() as i32 - 1), z.min(mh() as i32 - 1))]; + let a = g(x0, z0) * (1.0 - fx) + g(x0 + 1, z0) * fx; + let b = g(x0, z0 + 1) * (1.0 - fx) + g(x0 + 1, z0 + 1) * fx; + a * (1.0 - fz) + b * fz +} + +// a surface colour with the lava light added +fn lava_lit(base: Vec3, glow: f32) -> Vec3 { + base + LAVA_LIGHT * glow +} + const LIGHT_DIR: Vec3 = Vec3::new(0.45, 0.8, 0.35); // fog of war: units in unexplored tiles, and enemies out of sight, are not drawn @@ -524,7 +593,7 @@ fn block_shell(mk: &mut MB, map: &Map, x: i32, z: i32, n: usize, fill: [u8; 4], } } -fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option; 4] { +fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option; 4] { let mut mb = MB::new(); let ov = MB::new(); // textured overlays (currently unused) let mut ln = MB::new(); @@ -563,7 +632,8 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall let lava = river == Kind::Lava; let (la, lb) = cell_uv(if lava { T_LAVA } else { T_WATER } + (t.var as u32 & 3)); let a = amb_at(amb, xf + 0.5, zf + 0.5); - let lc = if lava { col(vec3(1.0, 0.95, 0.9), 1.0) } else { col(Vec3::splat(0.25 + a * 0.9), 1.0) }; + let lg = lava_at(glow, xf + 0.5, zf + 0.5); + let lc = if lava { col(vec3(1.0, 0.95, 0.9), 1.0) } else { col(lava_lit(Vec3::splat(0.25 + a * 0.9), lg), 1.0) }; mb.quad( [vec3(xf, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf + 1.0), vec3(xf, -LIQ_D, zf + 1.0)], [vec2(la.x, la.y), vec2(lb.x, la.y), vec2(lb.x, lb.y), vec2(la.x, lb.y)], @@ -583,7 +653,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall _ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)), }; let inward = vec3(-dx as f32, 0.0, -dz as f32); - let bc = col(Vec3::splat(if lava { 0.7 } else { 0.18 + a * 0.5 }), 1.0); + let bc = col(if lava { Vec3::splat(0.7) } else { lava_lit(Vec3::splat(0.18 + a * 0.5), lg) }, 1.0); mb.quad( [p, q, q - Vec3::Y * LIQ_D, p - Vec3::Y * LIQ_D], [vec2(fa.x, fa.y), vec2(fb.x, fa.y), vec2(fb.x, fb.y), vec2(fa.x, fb.y)], @@ -597,7 +667,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall let run_z = |dz: i32| map.get(x, z + dz).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge())); let along_x = (run_x(-1) as i32 + run_x(1) as i32) > (run_z(-1) as i32 + run_z(1) as i32); let (ba, bb) = cell_uv(tex); - let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), 1.0); + let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), lg), 1.0); let uv = if along_x { [vec2(ba.x, ba.y), vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y)] } else { @@ -636,7 +706,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall let solid_n = [(vx - 1, vz - 1), (vx, vz - 1), (vx - 1, vz), (vx, vz)].iter().filter(|&&(a, b)| solid_at(map, a, b)).count(); let ao = 1.0 - 0.17 * solid_n as f32; let a = amb_at(amb, vx as f32, vz as f32) * ao; - c[k] = col(Vec3::splat(a), 1.0); + c[k] = col(lava_lit(Vec3::splat(a), lava_at(glow, vx as f32, vz as f32) * ao), 1.0); } mb.quad( [vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)], @@ -652,7 +722,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C); } // room decor - let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0); + let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), lava_at(glow, xf + 0.5, zf + 0.5)), 1.0); // wear and tear: grit and cracks on plain floors if matches!(t.kind, Kind::Floor) { let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519); @@ -710,7 +780,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall Vec3::Y, n, (ua, ub), - |_p, nrm| Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))), + |p, nrm| lava_lit(Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))), lava_at(glow, p.x, p.z) * 0.6), ); // outlines on the wall top: north/west always, south/east where no wall continues let tl = vec3(xf, WALL_H, zf); @@ -754,7 +824,9 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall (ua, ub), |p, nrm| { let h = (p.y / WALL_H).clamp(0.0, 1.0); - Vec3::splat(side_amb * (0.5 + 0.5 * nrm.dot(ld).max(0.0)) * (0.55 + 0.45 * h)) + // the face is lit by the lava in front of it + let lg = lava_at(glow, p.x + out.x * 0.5, p.z + out.z * 0.5); + lava_lit(Vec3::splat(side_amb * (0.5 + 0.5 * nrm.dot(ld).max(0.0)) * (0.55 + 0.45 * h)), lg * (1.0 - 0.35 * h)) }, ); } @@ -833,6 +905,7 @@ pub struct R3d { pub max_lights: usize, pub render_div: u32, // 3D resolution divisor (pixel look) target: Option<(RenderTarget, u32, u32)>, // low resolution render target + lava: Vec, // lava light per tile (see lava_field) } // The 3D resolution setting refers to a window 1080 device pixels high: smaller windows use a @@ -870,12 +943,22 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 { h = (h ^ v).wrapping_mul(1099511628211); } } + // the lava light reaches further: lava and walls up to LAVA_REACH tiles around the chunk + let r = LAVA_REACH as i32; + for z in cz * CHUNK - r..(cz + 1) * CHUNK + r { + for x in cx * CHUNK - r..(cx + 1) * CHUNK + r { + if let Some(t) = map.get(x, z) { + let v = (t.river() == Some(Kind::Lava)) as u64 | (t.solid() as u64) << 1; + h = (h ^ (v + 7)).wrapping_mul(1099511628211); + } + } + } h } impl R3d { pub fn new() -> R3d { - R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, render_div: 1, target: None } + R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, render_div: 1, target: None, lava: Vec::new() } } // Changing the wall detail rebuilds all chunks @@ -907,6 +990,7 @@ impl R3d { fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) { self.ensure_chunks(); let mut amb: Option> = None; + let mut lava_fresh = false; for cz in 0..self.ncz { for cx in 0..self.ncx { if !Self::chunk_visible(m, cx, cz) { @@ -916,7 +1000,13 @@ impl R3d { let ch = &mut self.chunks[(cz * self.ncx + cx) as usize]; if ch.sig != sig { let a = amb.get_or_insert_with(|| ambient_field(&g.map)); - let [m, o, l, k] = build_chunk(&g.map, a, cx, cz, &gfx.atlas, self.wall_sub); + if !lava_fresh { + // the map changed: the lava light is worked out again (once per frame at most) + self.lava = lava_field(&g.map); + lava_fresh = true; + } + let ch = &mut self.chunks[(cz * self.ncx + cx) as usize]; + let [m, o, l, k] = build_chunk(&g.map, a, &self.lava, cx, cz, &gfx.atlas, self.wall_sub); ch.mesh = m; ch.overlay = o; ch.lines = l; @@ -1282,6 +1372,14 @@ impl R3d { } else { col(Vec3::splat(a), alpha) }; + // the lava light colours units near the lava + let c = if white { + c + } else { + let lg = lava_at(&self.lava, e.disp_x, e.disp_y) * 255.0; + let add = |v: u8, k: f32| (v as f32 + k * lg).min(255.0) as u8; + [add(c[0], LAVA_LIGHT.x), add(c[1], LAVA_LIGHT.y), add(c[2], LAVA_LIGHT.z), c[3]] + }; // pulled towards the camera: unchanged on screen (orthographic), but walls behind // the unit no longer cut into the sprite let base = vec3(e.disp_x, bob, e.disp_y) + toward * 0.45 + r * (lunge * fwd); @@ -2744,4 +2842,29 @@ mod tests { assert_eq!(effective_div(16, 200.0), 3); assert_eq!(effective_div(2, 100.0), 1); } + + #[test] + fn lava_lights_ten_tiles_and_fades() { + let mut g = Game::new(3, 64, 64); + // an open hall with one lava tile in the middle and a wall to the west + for y in 10..40 { + for x in 10..40 { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Floor; + t.owner = Owner::None; + } + } + g.map.t[idx(25, 25)].kind = Kind::Lava; + for y in 10..40 { + g.map.t[idx(20, y)].kind = Kind::Rock; + } + let f = lava_field(&g.map); + let at = |x: i32, y: i32| f[idx(x, y)]; + assert_eq!(at(25, 25), 1.0); + assert!(at(26, 25) > at(28, 25) && at(28, 25) > at(32, 25), "fades with the distance"); + assert!(at(34, 25) > 0.0, "nine tiles away still a little light"); + assert_eq!(at(36, 25), 0.0, "nothing beyond ten tiles"); + assert!(at(20, 25) > 0.0, "the wall face is lit"); + assert_eq!(at(19, 25), 0.0, "but no light behind the wall"); + } }