diff --git a/src/gfx.rs b/src/gfx.rs index c58d3af..ec5612c 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -622,19 +622,27 @@ fn gen_atlas() -> Image { _ => shade((24, 50, 88), d), }) }); - // lava: glowing orange with dark crust plates (4x4 plates, seamless at the cell border) + // lava: molten orange with hot yellow streaks and darker red troughs, no black crust. + // Seamless (period 16) so it can flow across the tiles cv.cell(T_LAVA + v, |x, y| { - let crust = rnd((x / 4 + (y / 4) % 2) % 4, y / 4, v + 130); - let edge = rnd(x, y, v + 131) * 0.25; - if crust + edge > 0.85 { - Some(shade((60, 22, 14), (rnd(x, y, v + 132) * 14.0) as i32)) - } else if crust + edge > 0.7 { - Some(c(200, 60, 20)) - } else if hash(x, y, v + 133) % 11 == 0 { - Some(c(255, 230, 120)) + let tau = std::f32::consts::TAU; + let (fx, fy) = (x as f32 / 16.0, y as f32 / 16.0); + let o = v as f32 * 1.3; + let heat = (tau * (fx + fy * 2.0) + o).sin() * 0.45 + + (tau * (fx * 3.0 - fy) + (tau * fy * 2.0 + o).sin() * 1.2).sin() * 0.35 + + (tau * (fy * 3.0 + fx * 2.0) + o * 2.0).sin() * 0.2; + let d = (rnd(x, y, v + 134) * 16.0) as i32 - 8; + Some(if heat > 0.62 { + shade((255, 228, 120), d / 2) + } else if heat > 0.3 { + shade((255, 170, 55), d) + } else if heat > -0.35 { + shade((240, 110, 25), d) + } else if heat > -0.7 { + shade((200, 64, 18), d) } else { - Some(shade((250, 130, 30), (rnd(x, y, v + 134) * 30.0) as i32 - 15)) - } + shade((150, 40, 14), d) + }) }); // knight fortress walls: pale stone blocks with blue-grey mortar cv.cell(T_FORTWALL + v, |x, y| { diff --git a/src/render3d.rs b/src/render3d.rs index bc24c56..058fe48 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -219,9 +219,9 @@ void main() { float a = color0.a; if (a > 96.0 && a < 192.0) { bool lava = a < 128.0; - float sp = lava ? 0.55 : 1.6; - float amp = lava ? 0.018 : 0.028; - w.y += amp * (sin(w.x * 1.7 + Time * sp) * 0.6 + sin(w.z * 2.3 - Time * sp * 1.3) * 0.4); + float sp = lava ? 0.8 : 1.6; + float amp = lava ? 0.04 : 0.05; + w.y += amp * (sin(w.x * 1.3 + w.z * 0.4 + Time * sp) * 0.6 + sin(w.z * 1.9 - w.x * 0.5 - Time * sp * 1.3) * 0.4); } wpos = w.xyz; wnorm = normal.xyz; @@ -250,20 +250,27 @@ void main() { float wave = 0.0; bool liquid = color.a > 0.38 && color.a < 0.75; if (liquid) { - // flowing texture: sampled in world space inside the tile's (seamless) atlas cell + // flowing texture: sampled in world space inside the tile's (seamless) atlas cell, moving + // along the river (its direction comes in the normal's x/z, see liquid_flow) bool lava = color.a < 0.5; - vec2 flow = lava ? vec2(0.035, 0.015) : vec2(0.11, 0.045); - vec2 p = wpos.xz + flow * Time; - p += (lava ? 0.03 : 0.05) * vec2(sin(wpos.z * 3.0 + Time * (lava ? 0.4 : 1.1)), sin(wpos.x * 3.0 + Time * (lava ? 0.3 : 0.9))); + vec2 dir = wnorm.xz / max(length(wnorm.xz), 0.001); + float speed = lava ? 0.16 : 0.32; + float ts = lava ? 0.8 : 1.6; // wave tempo: lava rolls slower + // swell: broad moving bands; their slope shades the surface and bends the texture + float ph1 = wpos.x * 1.3 + wpos.z * 0.4 + Time * ts; + float ph2 = wpos.z * 1.9 - wpos.x * 0.5 - Time * ts * 1.3; + vec2 slope = vec2(cos(ph1) * 1.3 * 0.6 - cos(ph2) * 0.5 * 0.4, cos(ph1) * 0.4 * 0.6 + cos(ph2) * 1.9 * 0.4); + vec2 p = wpos.xz - dir * speed * Time + slope * (lava ? 0.05 : 0.07); vec2 cell = floor(uv * AtlasSize / 16.0) * 16.0; tuv = (cell + fract(p) * 16.0) / AtlasSize; - wave = sin(wpos.x * 5.0 + wpos.z * 3.0 + Time * (lava ? 0.7 : 2.2)) * sin(wpos.z * 4.0 - wpos.x * 2.0 + Time * (lava ? 0.5 : 1.7)); + float ripple = sin(wpos.x * 5.0 + wpos.z * 3.0 + Time * ts * 1.4) * sin(wpos.z * 4.0 - wpos.x * 2.0 + Time * ts); + wave = dot(slope, vec2(0.45, 0.35)) * 0.6 + ripple * 0.4; } vec4 tex = texture2D(Texture, tuv); if (tex.a < 0.5) discard; // see-through units (invisibility): every other pixel left out if (color.a < 0.37 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard; - vec3 n = normalize(wnorm); + vec3 n = liquid ? vec3(0.0, 1.0, 0.0) : normalize(wnorm); vec3 light = color.rgb; for (int i = 0; i < 24; i++) { if (float(i) >= LightCount) break; @@ -274,9 +281,15 @@ void main() { float ndl = max(dot(n, d / max(dist, 0.001)), 0.0) * 0.8 + 0.2; light += LightCol[i].rgb * LightCol[i].a * att * ndl; } - // light rippling over the surface: glints on water, a slow pulse of the glow on lava - if (liquid) light *= 1.0 + wave * (color.a < 0.5 ? 0.12 : 0.1); - gl_FragColor = vec4(tex.rgb * min(light, vec3(1.7)), 1.0); + // light rippling over the surface: glints on water; lava glows by itself and pulses + if (liquid) { + if (color.a < 0.5) { + light = max(light, vec3(1.45, 1.3, 1.2)) * (1.0 + wave * 0.18); + } else { + light *= 1.0 + wave * 0.28; + } + } + gl_FragColor = vec4(tex.rgb * min(light, vec3(1.9)), 1.0); } "#; @@ -466,7 +479,7 @@ fn amb_at(f: &[f32], x: f32, z: f32) -> f32 { // ---- 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) +const LAVA_LIGHT: Vec3 = Vec3::new(1.05, 0.45, 0.08); // 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). @@ -507,7 +520,7 @@ fn lava_field(map: &Map) -> Vec { 0.0 } else { let k = (1.0 - d as f32 / max as f32).clamp(0.0, 1.0); - k * k + k.powf(1.6) } }) .collect() @@ -528,6 +541,19 @@ fn lava_at(f: &[f32], x: f32, z: f32) -> f32 { a * (1.0 - fz) + b * fz } +// the direction a river flows at a tile, carried to the shader in the surface normal's x/z +// (y stays 1 so the normal still points up): along the longer run of river tiles around it +fn liquid_flow(map: &Map, x: i32, z: i32) -> Vec3 { + let river = |dx: i32, dz: i32| map.get(x + dx, z + dz).map_or(false, |t| t.river().is_some()) as i32; + let along_x: i32 = (-3..=3).map(|d| river(d, 0)).sum(); + let along_z: i32 = (-3..=3).map(|d| river(0, d)).sum(); + if along_x > along_z { + vec3(0.5, 1.0, 0.0) + } else { + vec3(0.0, 1.0, 0.5) + } +} + // a surface colour with the lava light added fn lava_lit(base: Vec3, glow: f32) -> Vec3 { base + LAVA_LIGHT * glow @@ -550,7 +576,7 @@ const LIQ_D: f32 = 0.3; // depth of the river surface below the floor // vertex alpha that marks river surfaces for the lit shader (waves and flowing texture) const LIQ_WATER_A: f32 = 0.6; const LIQ_LAVA_A: f32 = 0.42; -const LIQ_SKIRT: f32 = 0.06; // river banks reach a bit below the surface, so waves leave no gap +const LIQ_SKIRT: f32 = 0.1; // river banks reach a bit below the surface, so waves leave no gap const DECK_Y: f32 = 0.03; // height of bridge decks const OUTLINE_C: [u8; 4] = [8, 5, 6, 150]; @@ -673,7 +699,7 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T // (its centre, robust even with low precision uvs) [(la + lb) * 0.5; 4], [lc; 4], - [Vec3::Y; 4], + [liquid_flow(map, x, z); 4], ); let (fa, fb) = cell_uv(T_FACE_EARTH); for (d, &(dx, dz)) in DIR4.iter().enumerate() { @@ -1108,7 +1134,7 @@ impl R3d { for x in (ccx - r).max(0)..(ccx + r).min(mw() as i32) { if x % 3 == 0 && g.map.t[idx(x, z)].kind == Kind::Lava { let fl = 0.8 + 0.2 * (t * 2.0 + (x * 7 + z * 3) as f32).sin(); - push(vec3(x as f32 + 0.5, 0.3, z as f32 + 0.5), 4.0, vec3(1.0, 0.45, 0.12), 0.9 * fl, 0.0); + push(vec3(x as f32 + 0.5, 0.3, z as f32 + 0.5), 5.0, vec3(1.0, 0.45, 0.12), 1.3 * fl, 0.0); } } } @@ -2645,7 +2671,7 @@ fn draw_parts_solid(g: &Game) { let life = (p.t / p.max).clamp(0.0, 1.0); let k = 1.0 - life * life; let bob = (p.t * 3.0 + p.x * 2.0).sin() * 0.01; - let mid = vec3(p.x, -LIQ_D + 0.04 + bob, p.y); // above the swell of the waves + let mid = vec3(p.x, -LIQ_D + 0.07 + bob, p.y); // above the swell of the waves let dir = vec3(p.vx, 0.0, p.vy).normalize_or_zero(); let half = dir * (0.17 * k); let wood = vec3(0.45, 0.31, 0.18);