diff --git a/src/gfx.rs b/src/gfx.rs index a2f123f..c58d3af 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -609,9 +609,12 @@ fn gen_atlas() -> Image { }); // ---- campaign terrain for v in 0..4u32 { - // water: dark blue with lighter wave crests + // water: dark blue with lighter wave crests. Seamless (period 16): the shader lets it flow + // across the tiles, sampled in world space cv.cell(T_WATER + v, |x, y| { - let wave = ((x as f32 * 0.8 + (y as f32 * 0.9 + v as f32 * 1.7).sin() * 2.2) as i32 + y * 2) % 9; + let tau = std::f32::consts::TAU; + let bend = ((y as f32 + v as f32 * 3.0) * tau / 16.0).sin() * 1.6 + ((x as f32) * tau / 8.0).sin() * 0.6; + let wave = (x + y * 2 + bend.round() as i32).rem_euclid(8); let d = (rnd(x, y, v + 120) * 10.0) as i32 - 5; Some(match wave { 0 => shade((70, 120, 170), d), @@ -619,9 +622,9 @@ fn gen_atlas() -> Image { _ => shade((24, 50, 88), d), }) }); - // lava: glowing orange with dark crust plates + // lava: glowing orange with dark crust plates (4x4 plates, seamless at the cell border) cv.cell(T_LAVA + v, |x, y| { - let crust = rnd(x / 3 + (y / 3) % 2, y / 3, v + 130); + 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)) diff --git a/src/render3d.rs b/src/render3d.rs index 68079c9..bc24c56 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -210,9 +210,19 @@ varying mediump vec3 wnorm; uniform mat4 Model; uniform mat4 Projection; +uniform highp float Time; void main() { vec4 w = Model * vec4(position, 1.0); + // river surfaces (marked by the vertex alpha, see LIQ_WATER_A / LIQ_LAVA_A) swell gently; + // lava rolls slower and flatter than water + 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); + } wpos = w.xyz; wnorm = normal.xyz; color = color0 / 255.0; @@ -232,12 +242,27 @@ uniform sampler2D Texture; uniform vec4 LightPos[24]; uniform vec4 LightCol[24]; uniform float LightCount; +uniform highp float Time; // same precision as in the vertex shader (required for linking) +uniform vec2 AtlasSize; void main() { - vec4 tex = texture2D(Texture, uv); + vec2 tuv = uv; + 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 + 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 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)); + } + vec4 tex = texture2D(Texture, tuv); if (tex.a < 0.5) discard; // see-through units (invisibility): every other pixel left out - if (color.a < 0.99 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard; + if (color.a < 0.37 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard; vec3 n = normalize(wnorm); vec3 light = color.rgb; for (int i = 0; i < 24; i++) { @@ -249,6 +274,8 @@ 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); } "#; @@ -262,6 +289,8 @@ fn lit_material() -> Material { UniformDesc::new("LightPos", UniformType::Float4).array(MAX_LIGHTS), UniformDesc::new("LightCol", UniformType::Float4).array(MAX_LIGHTS), UniformDesc::new("LightCount", UniformType::Float1), + UniformDesc::new("Time", UniformType::Float1), + UniformDesc::new("AtlasSize", UniformType::Float2), ], textures: vec![], }, @@ -518,6 +547,10 @@ fn solid_at(map: &Map, x: i32, z: i32) -> bool { const OUTLINE_W: f32 = 0.035; 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 DECK_Y: f32 = 0.03; // height of bridge decks const OUTLINE_C: [u8; 4] = [8, 5, 6, 150]; @@ -633,10 +666,12 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T 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 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) }; + let lc = if lava { col(vec3(1.0, 0.95, 0.9), LIQ_LAVA_A) } else { col(lava_lit(Vec3::splat(0.25 + a * 0.9), lg), LIQ_WATER_A) }; 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)], + // the shader samples the flowing texture in world space; the uv only names the cell + // (its centre, robust even with low precision uvs) + [(la + lb) * 0.5; 4], [lc; 4], [Vec3::Y; 4], ); @@ -655,7 +690,7 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T let inward = vec3(-dx as f32, 0.0, -dz as f32); 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], + [p, q, q - Vec3::Y * (LIQ_D + LIQ_SKIRT), p - Vec3::Y * (LIQ_D + LIQ_SKIRT)], [vec2(fa.x, fa.y), vec2(fb.x, fa.y), vec2(fb.x, fb.y), vec2(fa.x, fb.y)], [bc; 4], [inward; 4], @@ -1159,6 +1194,9 @@ impl R3d { self.lit.set_uniform_array("LightPos", &lp[..MAX_LIGHTS]); self.lit.set_uniform_array("LightCol", &lc[..]); self.lit.set_uniform("LightCount", count); + // wrapped so the shader keeps its precision (the flow speeds are whole tiles per period) + self.lit.set_uniform("Time", (time % 1000.0) as f32); + self.lit.set_uniform("AtlasSize", vec2(ATLAS_W, ATLAS_H)); gl_use_material(&self.lit); for cz in 0..self.ncz { for cx in 0..self.ncx { @@ -2607,7 +2645,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.025 + bob, p.y); + let mid = vec3(p.x, -LIQ_D + 0.04 + 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);