Light the surroundings of lava in orange up to ten tiles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 14:53:16 +01:00
1 parent ee54a98063
commit f4622d285d
1 file changed
+133 -10
+133 -10
View File
@@ -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<f32> {
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<Mesh>; 4] {
fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option<Mesh>; 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<f32>, // 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<Vec<f32>> = 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");
}
}