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:
1 parent
ee54a98063
commit
f4622d285d
1 file changed
+133
-10
+133
-10
@@ -435,6 +435,75 @@ fn amb_at(f: &[f32], x: f32, z: f32) -> f32 {
|
|||||||
0.15 + 0.45 * (a * (1.0 - fz) + b * fz)
|
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);
|
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
|
// 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 mut mb = MB::new();
|
||||||
let ov = MB::new(); // textured overlays (currently unused)
|
let ov = MB::new(); // textured overlays (currently unused)
|
||||||
let mut ln = MB::new();
|
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 lava = river == Kind::Lava;
|
||||||
let (la, lb) = cell_uv(if lava { T_LAVA } else { T_WATER } + (t.var as u32 & 3));
|
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 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(
|
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)],
|
[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)],
|
[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)),
|
_ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)),
|
||||||
};
|
};
|
||||||
let inward = vec3(-dx as f32, 0.0, -dz as f32);
|
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(
|
mb.quad(
|
||||||
[p, q, q - Vec3::Y * LIQ_D, p - Vec3::Y * LIQ_D],
|
[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)],
|
[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 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 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 (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 {
|
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)]
|
[vec2(ba.x, ba.y), vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y)]
|
||||||
} else {
|
} 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 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 ao = 1.0 - 0.17 * solid_n as f32;
|
||||||
let a = amb_at(amb, vx as f32, vz as f32) * ao;
|
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(
|
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)],
|
[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);
|
ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
||||||
}
|
}
|
||||||
// room decor
|
// 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
|
// wear and tear: grit and cracks on plain floors
|
||||||
if matches!(t.kind, Kind::Floor) {
|
if matches!(t.kind, Kind::Floor) {
|
||||||
let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519);
|
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,
|
Vec3::Y,
|
||||||
n,
|
n,
|
||||||
(ua, ub),
|
(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
|
// outlines on the wall top: north/west always, south/east where no wall continues
|
||||||
let tl = vec3(xf, WALL_H, zf);
|
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),
|
(ua, ub),
|
||||||
|p, nrm| {
|
|p, nrm| {
|
||||||
let h = (p.y / WALL_H).clamp(0.0, 1.0);
|
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 max_lights: usize,
|
||||||
pub render_div: u32, // 3D resolution divisor (pixel look)
|
pub render_div: u32, // 3D resolution divisor (pixel look)
|
||||||
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
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
|
// 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);
|
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
|
h
|
||||||
}
|
}
|
||||||
|
|
||||||
impl R3d {
|
impl R3d {
|
||||||
pub fn new() -> 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
|
// Changing the wall detail rebuilds all chunks
|
||||||
@@ -907,6 +990,7 @@ impl R3d {
|
|||||||
fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
|
fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
|
||||||
self.ensure_chunks();
|
self.ensure_chunks();
|
||||||
let mut amb: Option<Vec<f32>> = None;
|
let mut amb: Option<Vec<f32>> = None;
|
||||||
|
let mut lava_fresh = false;
|
||||||
for cz in 0..self.ncz {
|
for cz in 0..self.ncz {
|
||||||
for cx in 0..self.ncx {
|
for cx in 0..self.ncx {
|
||||||
if !Self::chunk_visible(m, cx, cz) {
|
if !Self::chunk_visible(m, cx, cz) {
|
||||||
@@ -916,7 +1000,13 @@ impl R3d {
|
|||||||
let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
|
let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
|
||||||
if ch.sig != sig {
|
if ch.sig != sig {
|
||||||
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
|
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.mesh = m;
|
||||||
ch.overlay = o;
|
ch.overlay = o;
|
||||||
ch.lines = l;
|
ch.lines = l;
|
||||||
@@ -1282,6 +1372,14 @@ impl R3d {
|
|||||||
} else {
|
} else {
|
||||||
col(Vec3::splat(a), alpha)
|
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
|
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
||||||
// the unit no longer cut into the sprite
|
// the unit no longer cut into the sprite
|
||||||
let base = vec3(e.disp_x, bob, e.disp_y) + toward * 0.45 + r * (lunge * fwd);
|
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(16, 200.0), 3);
|
||||||
assert_eq!(effective_div(2, 100.0), 1);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user