From 8fcff33fd2cffb454efa35fe51492285e719c7ec Mon Sep 17 00:00:00 2001 From: Nils van Lueck Date: Thu, 3 Sep 2026 03:56:22 +0200 Subject: [PATCH] feat(water): improve waves, shores, and water-body rendering - Add depth-aware refraction, reflections, foam, rain, and river flow - Bake shoreline distance and assumed depth into water meshes - Preserve depth prepasses and disable foliage alpha-to-coverage --- STATUS.md | 51 ++- crates/app/src/main.rs | 5 + crates/app/src/settings.rs | 6 +- crates/content/src/water.rs | 212 ++++++++++- crates/route-editor/src/main.rs | 2 + crates/world-render/src/lib.rs | 30 +- crates/world-render/src/water.rs | 57 ++- crates/world-render/src/water.wgsl | 575 ++++++++++++++++++++++++----- 8 files changed, 788 insertions(+), 150 deletions(-) diff --git a/STATUS.md b/STATUS.md index d82d7e3..a2a4566 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2384,13 +2384,50 @@ Every simplification is marked with a `ponytail:` comment at the code site, with members become islands the surface goes around. The surfaces are cut to the terrain tiles and laid over the raw elevation data at the level the shoreline samples give, so a lake settles flat and a river follows its fall, and an - embankment across a valley holds the water back like a dam. The shader - (`world_render::water`) makes the waves out of the wind and the rain, the - depth-coloured body out of the shoreline level minus the bed, and the - reflection out of the sky the atmosphere already computes. Clicking a surface - in the editor selects its body — waterline outlined, name editable, centre and - delete in the panel; the shape itself is the file's or a fresh import's. - Not yet: a hand-editable water level, and vertex handles like the fields have. + embankment across a valley holds the water back like a dam. Every vertex + carries two numbers and the shader (`world_render::water`) makes the whole + look out of them and the weather: the water column under it, which grades + the body from murky shallows to a dark deep and decides what shows + through; and how far the waterline is, which is how much room a wave had + to grow in and where the bank's own band lies. Where a DGM1 models the + water surface rather than the bed — the usual German delivery, and every + river in one — there is no column to measure, so one is assumed from the + distance to the bank; without it a river draws as a dead flat, translucent + sheet, because a wave with no water under it dies in the shallows exactly + as it should. The surface is ten octaves + of directional waves from a lake's swell down to a hand's-breadth ripple, + each one grown by the wind it needs (a ripple takes a breath, a swell a + gale), bent by the octaves above it and let through a noise of its own, so + the crests wander and break instead of weaving a corduroy; every octave + that falls below the pixel it would need is not dropped but folded into + the roughness (α² = α₀² + 2σ²), which is what makes a far lake glitter + rather than alias. **What is under and over the surface is the picture + itself**: the material is opaque with specular transmission, so Bevy + draws it after the opaque world with a copy of the frame bound, and the + bed is read out of that copy along the refracted ray — bent by the waves, + absorbed by Beer's law over the column (red first), scattered back as the + water's own blue-green — while the reflection is a screen-space march of + the mirrored ray against the depth prepass into the same copy, so the + bank, its trees and the train stand in the water where they belong and + wobble with it; where a ray leaves the screen or the surface is too + rough to mirror sharply, the atmosphere's probe takes over. Both + programs put a `DepthPrepass` on their camera for this (about 1.5 ms on + the demo), and the loader no longer switches foliage from its alpha mask + to alpha-to-coverage, because under MSAA Bevy's prepass has no discard + for that mode and every leaf card would write its whole quad into the + depth. On top of that: foam where a wave runs out on the bank and spume + streaks out on the open water from about eight metres a second, the + sun's own glitter path, rain rings, and a river that runs — nothing says + which body is a river, but a lake's surface is level and a river's + follows the fall of the valley, so the mesh's own normal gives Manning's + formula the gradient and the wave field is carried downstream at the + speed that comes out. Clicking a surface in the editor selects its body + — waterline outlined, name editable, centre and delete in the panel; the + shape itself is the file's or a fresh import's. Not yet: a hand-editable + water level, vertex handles like the fields have, real wave height (the + mesh is flat; the surface is all normal), and the cloud deck in the + reflection — it is drawn in the transparent phase, after the copy the + water reads. - **Areas and bands onto a fourth splat channel** — `terrain::splat_weights` already carries `[grass, rock, gravel, 1.0]`, the fourth component is free. Roads (`highway=*` buffered by their width), farmland and meadow paint into it instead of diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index bc7039b..a439d16 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -27,6 +27,7 @@ mod walk; mod world; use ai_driver::AiDriver; +use bevy::core_pipeline::prepass::DepthPrepass; use bevy::ecs::resource::IsResource; use bevy::mesh::{Indices, PrimitiveTopology}; use bevy::pbr::{DistanceFog, FogFalloff}; @@ -1179,6 +1180,10 @@ fn setup( ..default() }, sky::camera_settings(), + // The water reads its reflections out of the depth the world was + // drawn with (`world_render::water`); without the prepass it falls + // back to mirroring the sky alone. The upscalers want it too. + DepthPrepass, // Near-field extinction (`feed_sky`): the atmosphere's own haze term // carries the colour and the distance, but a planetary medium's LUTs // do not resolve 300 m of fog. This is what closes it. diff --git a/crates/app/src/settings.rs b/crates/app/src/settings.rs index c281dd1..576e961 100644 --- a/crates/app/src/settings.rs +++ b/crates/app/src/settings.rs @@ -23,7 +23,7 @@ use bevy::anti_alias::dlss::{ }; use bevy::anti_alias::fxaa::{Fxaa, Sensitivity}; use bevy::anti_alias::smaa::{Smaa, SmaaPreset}; -use bevy::core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass}; +use bevy::core_pipeline::prepass::MotionVectorPrepass; use bevy::ecs::system::{EntityCommands, SystemParam}; use bevy::light::DirectionalLightShadowMap; use bevy::post_process::bloom::Bloom; @@ -442,7 +442,9 @@ pub fn apply_upscaling( camera.remove::(); #[cfg(feature = "dlss")] camera.remove::(); - camera.remove::<(DepthPrepass, MotionVectorPrepass, TemporalJitter, MipBias)>(); + // The depth prepass stays: the water's reflections read it, upscaler + // or not (`world_render::water`). + camera.remove::<(MotionVectorPrepass, TemporalJitter, MipBias)>(); } } diff --git a/crates/content/src/water.rs b/crates/content/src/water.rs index 6243848..0848677 100644 --- a/crates/content/src/water.rs +++ b/crates/content/src/water.rs @@ -16,6 +16,12 @@ //! raised to the shoreline level, so the lake stays visible instead of //! drowning under its own ground. //! +//! Each vertex carries what the renderer needs of that: the water column under +//! it, and how far its waterline is. In the first case there is no column to +//! measure — the ground *is* the water — so one is assumed from the distance +//! to the bank ([`assumed_depth`]), because the one thing known about the bed +//! of any body is that it falls away from the edge. +//! //! Everything is computed as a pure function of position, so the pieces two //! neighbouring tiles cut out of one polygon meet without a seam, and an //! embankment carrying the line across a valley holds the water back exactly @@ -55,12 +61,15 @@ pub const LIFT: f64 = 0.12; /// climbs from one end to the other, is clamped only at its lowest reach. const LEVEL_PERCENTILE: f64 = 0.05; -/// The column assumed where the elevation data has no word [m]. A module -/// without a DGM would otherwise draw every body as flat as the ground it -/// lies on — a dull green sheet — where "a few metres of water under it" is -/// the honest guess. +/// The column assumed where the elevation data has no word about it [m]. A +/// module without a DGM would otherwise draw every body as flat as the ground +/// it lies on — a dull green sheet — where "a few metres of water under it" +/// is the honest guess. Also the deepest the guess in [`assumed_depth`] goes. pub(crate) const NOMINAL_DEPTH: f64 = 2.5; +/// How far from the bank the assumed column reaches its full depth [m]. +const PROFILE_REACH: f64 = 40.0; + /// Target edge length of a water surface's mesh [m]. The waves are the /// shader's business; the geometry only has to follow the fall of a river, /// and the coarsest grid that does that is the cheapest. @@ -70,6 +79,13 @@ const TARGET_EDGE: f64 = 16.0; /// a tile-wide river at [`TARGET_EDGE`] needs no more. const MAX_LEVELS: u32 = 3; +/// How far from the waterline the distance to it is still measured [m]. +/// Everything beyond is open water as far as the shader is concerned: the +/// longest wave it carries has all the room it wants, and no shore is near +/// enough to break on. Also the cell size of the lookup that finds it, so a +/// larger reach costs a longer list per cell and nothing else. +const SHORE_REACH: f64 = 48.0; + /// The bodies of water of a line, indexed by the terrain tiles they touch. #[derive(Debug, Clone, Default)] pub struct Waters { @@ -240,7 +256,12 @@ pub struct WaterPatch { /// the tile boundaries it crosses. pub uvs: Vec<[f32; 2]>, /// Per-vertex data: `r` carries the depth of the water column under the - /// vertex [m] — what the shader makes its shore-to-deep colours of. + /// vertex [m] — what the shader makes its shore-to-deep colours of — and + /// `g` how far the waterline is [m], out to [`SHORE_REACH`]. The depth + /// says how dark the body is; the distance says how much room a wave had + /// to grow in, and where the shore's own band of foam and stirred sand + /// lies. Both are pure functions of position, so a body cut across two + /// tiles carries the same numbers on either side of the seam. pub colors: Vec<[f32; 4]>, pub indices: Vec, /// The bodies that went into this patch, in line order — what a click on @@ -303,6 +324,10 @@ pub(crate) fn patches( .unwrap_or(fallback - NOMINAL_DEPTH); (bottom.max(level) + LIFT, bottom) }; + // The whole waterline of the body, islands included — not the piece + // this tile cut out of it, or every tile boundary would read as a + // shore. + let shore = Shore::of(water); // The outline and the islands are each cut to the tile, and an outer // piece takes the island pieces it holds as its holes. let hole_pieces: Vec> = water @@ -320,7 +345,7 @@ pub(crate) fn patches( .cloned() .collect(); add_piece( - &mut patch, &piece, &holes, water, level, zone, surface, sampler, frame, + &mut patch, &piece, &holes, water, &shore, level, zone, surface, sampler, frame, ); } } @@ -340,6 +365,7 @@ fn add_piece( ring: &[DVec2], holes: &[Vec], water: &Water, + shore: &Shore, level: f64, zone: u8, surface: impl Fn(DVec2, &mut Sampler<'_>) -> (f64, f64), @@ -361,15 +387,16 @@ fn add_piece( for p in &points { let (height, bottom) = surface(*p, sampler); - let depth = (level - bottom).max(0.0); + let edge = shore.distance(*p); + let depth = (level - bottom).max(0.0).max(assumed_depth(edge)); let (lat, lon) = geo::from_utm(p.x, p.y, zone); let world = geo::to_ecef(lat, lon, height); patch.positions.push(to_render(frame.to_local(world))); patch.normals.push(normal_at(*p, &surface, sampler)); let offset = *p - centre; patch.uvs.push([offset.x as f32, offset.y as f32]); - // r: the water column in metres. g, b, a spare. - patch.colors.push([depth as f32, 0.0, 0.0, 1.0]); + // r: the water column in metres, g: how far the waterline is. b, a spare. + patch.colors.push([depth as f32, edge as f32, 0.0, 1.0]); } for [a, b, c] in tris { patch @@ -378,6 +405,106 @@ fn add_piece( } } +/// The waterline of one body as a lookup: how far a point on the surface is +/// from the nearest shore, out to [`SHORE_REACH`]. +/// +/// The shader wants that distance per vertex — a wave needs room to grow, and +/// the band where the water works its edge is measured from the waterline, +/// not from the tile it happens to be cut by. Answering it by walking the +/// whole outline would be a lake's ring against a lake's vertices; instead +/// every edge is filed into the cells of a [`SHORE_REACH`]-metre grid, and a +/// point only ever looks at its own cell and the eight around it — which is +/// every edge that could be nearer than the reach. +struct Shore { + edges: HashMap>, +} + +impl Shore { + /// The outline and the islands of one body. Both are shore: an island's + /// bank breaks a wave exactly as the far bank does. + fn of(water: &Water) -> Self { + let mut out = Shore { + edges: HashMap::new(), + }; + out.add(&water.ring); + for hole in &water.holes { + out.add(hole); + } + out + } + + /// Files one closed ring. Edges longer than a cell are cut into pieces + /// first, so a piece's bounding box never spans more than four cells and + /// filing it into every cell of that box stays cheap — and complete: an + /// edge is in every cell it passes through. + fn add(&mut self, ring: &[DVec2]) { + for i in 0..ring.len() { + let (a, b) = (ring[i], ring[(i + 1) % ring.len()]); + let pieces = (a.distance(b) / SHORE_REACH).ceil().max(1.0); + for step in 0..pieces as usize { + let t0 = step as f64 / pieces; + let t1 = (step + 1) as f64 / pieces; + let edge = [a.lerp(b, t0), a.lerp(b, t1)]; + let (lo, hi) = (edge[0].min(edge[1]), edge[0].max(edge[1])); + let (kx0, ky0) = key(lo, SHORE_REACH); + let (kx1, ky1) = key(hi, SHORE_REACH); + for ky in ky0..=ky1 { + for kx in kx0..=kx1 { + self.edges.entry((kx, ky)).or_default().push(edge); + } + } + } + } + } + + /// How far `p` is from the waterline [m], capped at [`SHORE_REACH`]. + fn distance(&self, p: DVec2) -> f64 { + let (kx, ky) = key(p, SHORE_REACH); + let mut best = SHORE_REACH * SHORE_REACH; + for ky in ky - 1..=ky + 1 { + for kx in kx - 1..=kx + 1 { + let Some(edges) = self.edges.get(&(kx, ky)) else { + continue; + }; + for [a, b] in edges { + best = best.min(distance_squared(p, *a, *b)); + } + } + } + best.sqrt().min(SHORE_REACH) + } +} + +/// The column to assume this far from the bank [m]. +/// +/// German DGM1 usually models the *water surface*, not the bed: sample a river +/// and the height that comes back is the height of the water, so the measured +/// column is zero along its whole length. Left at that, a river draws as a +/// murky, translucent, dead flat sheet — the shader takes its waves, its +/// colour and its opacity from the column, and a wave that has no water under +/// it dies in the shallows exactly as it should. +/// +/// So where nothing was measured, something is assumed, and the one thing +/// known about the bed of any body is that it falls away from the bank. This +/// is that profile: nothing at the waterline, [`NOMINAL_DEPTH`] once +/// [`PROFILE_REACH`] from it. Where the data does model a bed, its number is +/// deeper than the guess and stands. +fn assumed_depth(shore: f64) -> f64 { + let t = (shore / PROFILE_REACH).clamp(0.0, 1.0); + NOMINAL_DEPTH * t * t * (3.0 - 2.0 * t) +} + +/// Square of the distance from `p` to the segment `a b`. +fn distance_squared(p: DVec2, a: DVec2, b: DVec2) -> f64 { + let along = b - a; + let len = along.length_squared(); + if len < 1e-12 { + return (p - a).length_squared(); + } + let t = ((p - a).dot(along) / len).clamp(0.0, 1.0); + (p - (a + along * t)).length_squared() +} + /// Triangulates a ring with holes: each island is **bridged** into the /// outline — the rightmost island point is joined to the waterline with a /// keyhole the eye cannot see — and the one ring that leaves is ear-clipped @@ -834,17 +961,76 @@ mod tests { // + 46 geoid + lift. let y = patch.positions[0][1]; assert!((y - (146.0 + LIFT as f32)).abs() < 0.01, "{y}"); - // Flat ground at the level: no column to colour. + // Flat ground at the level: nothing was measured, so the column is + // the assumed profile — nothing at the bank, deepening away from it. + // A body drawn with a column of zero everywhere is a wet meadow. + let (shallowest, deepest) = patch.colors.iter().fold((f32::MAX, 0.0f32), |acc, c| { + (acc.0.min(c[0]), acc.1.max(c[0])) + }); + assert!(shallowest < 1e-4, "{shallowest}"); assert!( - patch.colors.iter().all(|c| c[0] < 1e-4), - "{:?}", - patch.colors[0] + (deepest - assumed_depth(100.0) as f32).abs() < 0.01, + "{deepest}" ); // Every index addresses a vertex that exists. let count = patch.positions.len() as u32; assert!(patch.indices.iter().all(|i| *i < count)); } + /// Every vertex knows how far its waterline is — the shader grows its + /// waves in the room that distance gives them and lays the band of foam + /// where it runs out. + #[test] + fn a_vertex_knows_how_far_the_shore_is() { + let tile = (1171, 11249); + let min = DVec2::new(tile.0 as f64 * 512.0, tile.1 as f64 * 512.0); + let patches = patch_of( + &[source(min.x + 100.0, min.y + 100.0, 200.0, "See")], + tile, + |_, _| 100.0, + ); + let patch = &patches[0]; + // The corners of the outline are vertices, and they are on the water's + // edge. + let nearest = patch.colors.iter().fold(f32::MAX, |m, c| m.min(c[1])); + assert!(nearest < 1e-3, "{nearest}"); + // The middle of a 200 m square is a hundred metres from every bank — + // past the reach, so it reads as open water. + let furthest = patch.colors.iter().fold(0.0f32, |m, c| m.max(c[1])); + assert!((furthest - SHORE_REACH as f32).abs() < 1e-3, "{furthest}"); + // And nothing in a square body is further from a bank than half its + // side, whatever the mesh looks like. + assert!(patch.colors.iter().all(|c| c[1] <= 100.0)); + } + + /// A tile seam is not a shore: the distance is measured against the body's + /// own outline, so the water does not calm down and foam up where the + /// terrain streaming happened to cut it. + #[test] + fn a_tile_seam_is_no_waterline() { + let tile = (1171, 11249); + let min = DVec2::new(tile.0 as f64 * 512.0, tile.1 as f64 * 512.0); + // 200 m square from 452 to 652 east of the tile corner: the seam at + // 512 cuts 60 m off it, well inside the body. + let body = source(min.x + 452.0, min.y + 100.0, 200.0, "See"); + let patches = patch_of(&[body], tile, |_, _| 100.0); + let patch = &patches[0]; + // `uv` is the offset from the body's centre in metres, so the seam + // lies at u = 512 − 552. + let on_seam: Vec = patch + .uvs + .iter() + .zip(&patch.colors) + .filter(|(uv, _)| (uv[0] + 40.0).abs() < 0.5 && uv[1].abs() < 40.0) + .map(|(_, c)| c[1]) + .collect(); + assert!(!on_seam.is_empty(), "no vertex on the seam"); + assert!( + on_seam.iter().all(|d| *d > 40.0), + "the seam reads as a bank: {on_seam:?}" + ); + } + /// A DGM that models the bed: the surface is raised to the shoreline, so /// the lake stays visible, and the column carries the depth the shader /// makes its colours of. diff --git a/crates/route-editor/src/main.rs b/crates/route-editor/src/main.rs index 9e6a856..afc6920 100644 --- a/crates/route-editor/src/main.rs +++ b/crates/route-editor/src/main.rs @@ -721,6 +721,8 @@ fn setup( ..default() }, sky::camera_settings(), + // The water reflects the module out of this (`world_render::water`). + bevy::core_pipeline::prepass::DepthPrepass, PrimaryEguiContext, )); // The same sky the simulator draws, over the same module: the sun where the diff --git a/crates/world-render/src/lib.rs b/crates/world-render/src/lib.rs index 14e83a8..be5c2aa 100644 --- a/crates/world-render/src/lib.rs +++ b/crates/world-render/src/lib.rs @@ -1162,10 +1162,6 @@ pub struct TextureMips { struct PendingMaterial { material: Handle, - /// Foliage: switch it from a hard mask to alpha-to-coverage once it is - /// there. A leaf's edge is then anti-aliased against the sky instead of - /// stepping from texel to texel, which is what a canopy is mostly made of. - cutout: bool, frames: u32, } @@ -1183,17 +1179,26 @@ struct PendingTexture { impl TextureMips { /// Queues every texture of a material, once the material itself is there. pub fn enqueue(&mut self, material: &Handle) { - self.queue(material, false); + self.queue(material); } /// The same for a cut-out material — foliage, hair, a chain-link fence. - /// Its mip chain keeps the coverage of its alpha, and its edges are - /// resolved by the sample mask rather than by a hard test. + /// Its mip chain keeps the coverage of its alpha, which the material's own + /// mask cutoff decides. + /// + /// This used to switch the material from its hard mask to + /// alpha-to-coverage as well, for a leaf edge anti-aliased against the + /// sky. That is off: under MSAA, Bevy's depth prepass has no discard for + /// alpha-to-coverage (the pass has no colour target for the coverage to go + /// to), so every leaf card wrote its whole quad into the depth, and the + /// sky behind it came out black. The water reads that depth for its + /// reflections (`water.rs`), and a lake with the trees of its bank in it + /// is worth more than the edge of a leaf. pub fn enqueue_cutout(&mut self, material: &Handle) { - self.queue(material, true); + self.queue(material); } - fn queue(&mut self, material: &Handle, cutout: bool) { + fn queue(&mut self, material: &Handle) { if self .materials .iter() @@ -1203,7 +1208,6 @@ impl TextureMips { } self.materials.push(PendingMaterial { material: material.clone(), - cutout, frames: 0, }); } @@ -1250,12 +1254,6 @@ pub fn mip_textures( for image in &queued { textures.enqueue_image(image, &entry.material, cutoff); } - if entry.cutout - && matches!(material.alpha_mode, AlphaMode::Mask(_)) - && let Some(mut material) = materials.get_mut(&entry.material) - { - material.alpha_mode = AlphaMode::AlphaToCoverage; - } } None => { entry.frames += 1; diff --git a/crates/world-render/src/water.rs b/crates/world-render/src/water.rs index d75a0e1..24c8703 100644 --- a/crates/world-render/src/water.rs +++ b/crates/world-render/src/water.rs @@ -2,11 +2,25 @@ //! with a shader of their own. //! //! One material for every body of water there is. What differs between a -//! mill pond and the Rhine is the wind over it, the rain on it, and how deep -//! the bed lies below the surface — and all three arrive without a byte of -//! per-water data: the depth is baked into the mesh's vertex colours by -//! `content::water`, the weather rides the same uniform every other surface -//! reads (`crate::weather`), and the waves are the shader's own business. +//! mill pond and the Rhine is the wind over it, the rain on it, how deep the +//! bed lies below the surface and how far the bank is — and all of it arrives +//! without a byte of per-water data: the depth and the distance to the +//! waterline are baked into the mesh's vertex colours by `content::water`, +//! the fall of a river is in its normals, the weather rides the same uniform +//! every other surface reads (`crate::weather`), and the waves are the +//! shader's own business. +//! +//! The material is *opaque with specular transmission*, not alpha-blended. +//! Bevy then draws it in its own phase after the opaque world, with a copy of +//! the finished picture bound as the "transmission texture": what lies under +//! the surface is read back out of that copy along the refracted ray, and +//! attenuated on the way up through the column the mesh carries — a real +//! refraction of a real bed, with Beer's law between. The same copy is what +//! the shader marches its reflection rays into when the camera also has a +//! [`DepthPrepass`](bevy::core_pipeline::prepass::DepthPrepass): the bank, +//! the trees on it and the train stand in the water where they ought to, +//! and only where a ray leaves the screen does the sky's environment probe +//! take over. Both programs put the prepass on their camera for this. //! //! **Multiplayer.** Nothing here is state. The uniform is a function of //! [`Sky`](crate::sky::Sky), which is a function of the scenario clock, and @@ -39,14 +53,9 @@ impl MaterialExtension for WaterExt { "embedded://world_render/water.wgsl".into() } - // The ground shows through where the water is shallow, so a shore reads - // as a shore instead of as a hard edge where two opaque planes meet. - fn alpha_mode() -> Option { - Some(AlphaMode::Blend) - } - - // A surface that blends has no business in the depth prepass — what it - // hides behind it is meant to stay visible — and a horizontal plane casts + // What is under the surface has to be in the picture the surface reads + // its refraction from, so the water stays out of the prepass — Bevy keeps + // transmissive materials out of it anyway — and a horizontal plane casts // no shadow worth the fill rate. fn enable_prepass() -> bool { false @@ -79,10 +88,20 @@ impl WaterMaterials { .get_or_insert_with(|| { materials.add(WaterMaterial { base: StandardMaterial { - // The shader writes the colour from the water column; this - // is only what it starts from. + // The shader writes every one of these per fragment, from + // the water column and the weather; what matters here is + // that they are *on*. A specular transmission above zero + // puts the material into the transmissive phase with the + // picture of the world bound, and a finite attenuation + // distance turns Beer's law on for the way through the + // column. base_color: Color::srgb(0.07, 0.12, 0.11), perceptual_roughness: 0.08, + specular_transmission: 0.9, + thickness: 1.0, + ior: 1.333, + attenuation_distance: 1.5, + attenuation_color: Color::srgb(0.3, 0.6, 0.55), ..default() }, extension: WaterExt { @@ -157,15 +176,16 @@ pub fn mesh(patch: &WaterPatch) -> Mesh { mod tests { use super::*; - /// The water carries its depth to the shader in the vertex colour, and - /// its mesh keeps the attribute set the shader reads. + /// The water carries its depth and its distance from the bank to the + /// shader in the vertex colour, and its mesh keeps the attribute set the + /// shader reads. #[test] fn a_patch_is_a_coloured_surface() { let patch = WaterPatch { positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], normals: vec![[0.0, 1.0, 0.0]; 3], uvs: vec![[0.0, 0.0]; 3], - colors: vec![[1.5, 0.0, 0.0, 1.0]; 3], + colors: vec![[1.5, 7.0, 0.0, 1.0]; 3], indices: vec![0, 1, 2], sources: vec![0], }; @@ -177,5 +197,6 @@ mod tests { panic!("colors"); }; assert!((colors[0][0] - 1.5).abs() < 1e-6, "{:?}", colors[0]); + assert!((colors[0][1] - 7.0).abs() < 1e-6, "{:?}", colors[0]); } } diff --git a/crates/world-render/src/water.wgsl b/crates/world-render/src/water.wgsl index 579ba2f..5edbea9 100644 --- a/crates/world-render/src/water.wgsl +++ b/crates/world-render/src/water.wgsl @@ -3,21 +3,51 @@ // (see `content::water`). The mesh is only the surface, standing where the // elevation data put it; everything that makes it read as water is here. // -// Three ingredients, layered: +// The mesh brings two numbers per vertex, and both of them are pure functions +// of where the vertex is — the vertex colour carries the depth of the water +// column in metres (`r`) and how far the waterline is (`g`). What is made of +// them, in layers: // -// 1. The body. The vertex colour carries the depth of the water column in -// metres — what the shoreline level made of the bed. Shallow water is -// murky and translucent, showing the ground beneath; deep water goes -// dark and opaque. That is the shore reading as a shore, for free. -// 2. The waves. A handful of directional waves — wind-aligned, spread -// around it, each running at its own deep-water phase speed — tilt the -// normal, and they die out in the shallows. The PBR path does the rest: -// the atmosphere's environment probe gives the sky its reflection, the -// sun draws the glitter, the fresnel makes it all angle-dependent. -// 3. The weather. Rain rings its drops into the surface (the same +// 1. The surface. Ten octaves of directional waves, from a lake's swell down +// to a hand's-breadth ripple, running with the wind — the long ones along +// it, the short ones spread around it — each one grown by as much wind, +// room and water as it has, and each one drawn only while it is bigger +// than the pixel that would have to hold it. What falls below that is +// not dropped but folded into the roughness, which is why a lake a +// kilometre off glitters instead of crawling with aliasing. +// 2. What is under it. The material is opaque with *specular transmission*: +// Bevy draws it after the opaque world with a copy of the picture bound, +// and the bed is read back out of that copy along the refracted ray — +// bent by the waves, attenuated by Beer's law over the column the mesh +// carries. A shallow shore shows its sand through green water; a lake +// past a few metres shows nothing but the water itself. +// 3. What is over it. The same copy is what the reflection rays march into, +// against the depth the world was drawn with: the bank, the trees on it +// and the train stand in the water where they ought to, wobbling with +// the waves. Where a ray leaves the screen, or the surface is too rough +// to mirror anything sharply, the atmosphere's environment probe takes +// over — the sky, the sun's own glitter, the fresnel between them. +// 4. The shore. Where the water runs out, the wave leaves a band of foam and +// stirred sand behind it — torn up by a noise and breathing with the +// swell, because a waterline is not a stroke around a polygon. In a gale +// the crests break out on the open water too. +// 5. The weather. Rain rings its drops into the surface (the same // `ripple_slope` the wet ground uses), a cloud's shadow lies on it like // it lies on the fields, and a dying wind calms it towards glass. // +// A sixth thing falls out of the mesh alone: a river runs. Nothing says which +// body is one, but a lake's surface is level where a river's follows the fall +// of the valley, so the surface normal the tiles were built with is the +// gradient, and Manning's formula turns it into a current the whole wave field +// is carried downstream at. +// +// The waves run in the mesh's own `uv` — metres east and north of the body's +// centre — rather than in world coordinates. Two tiles of one lake then agree +// on every crest across the seam between them, and the floating origin can +// rebase under the camera without the surface changing pattern under it. The +// weather's own effects (the rain rings, the cloud shadow) stay in world +// coordinates, where the ground has them too. +// // The time comes from the view's own globals, so nothing is uploaded per // frame; the uniform only changes when the weather does. @@ -28,8 +58,16 @@ forward_io::{VertexOutput, FragmentOutput}, pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, pbr_types::PbrInput, - mesh_view_bindings::globals, + mesh_view_bindings::{globals, view, view_transmission_texture, view_transmission_sampler}, + view_transformations::depth_ndc_to_view_z, + utils::interleaved_gradient_noise, } +#ifdef DEPTH_PREPASS +#import bevy_pbr::prepass_utils::prepass_depth +#endif +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping::approximate_inverse_tone_mapping +#endif #import world_render::weather::{Weather, ripple_slope, cloud_shade, noise21} /// The weather, in the layout `WeatherParams` (Rust) writes. @@ -43,116 +81,465 @@ struct Water { // `sqrt(0.02 / 0.16)` in the reflectance parametrisation of `StandardMaterial` // — the same number the wet ground polishes itself towards. const WATER_REFLECTANCE = 0.354; +const WATER_F0 = 0.02; + +/// Refractive index of water. +const WATER_IOR = 1.333; + +/// Octaves of the wave spectrum, longest first. Ten of them at [`RATIO`] reach +/// from a lake's swell to the ripple a puff of wind raises. +const OCTAVES = 10u; + +/// The longest wave the field carries [m]. +const LONGEST = 34.0; + +/// Wavelength from one octave to the next. +const RATIO = 0.62; + +/// Gradient one octave contributes at a full sea, rise over run. Ten of them +/// out of phase make an rms slope of about 0.2 rad, and a fifth of that in a +/// light breeze — which is roughly the mean square slope Cox and Munk read off +/// the sun's glitter from an aeroplane in 1954, and it is what decides whether +/// a surface reads as water or as a sheet of glass. +const OCTAVE_SLOPE = 0.09; -// Seven waves over two decades of wavelength [m]: (wavelength, amplitude at a -// fresh breeze [m], angle off the wind [rad]). The short ones travel steeper -// angles — a real water surface is a spectrum, not a washboard, and it is the -// crossing of the short waves that makes a reflection break up. -const WAVES = array, 7>( - vec3(31.0, 0.062, 0.00), - vec3(17.0, 0.041, 0.55), - vec3(9.0, 0.026, -0.80), - vec3(5.0, 0.016, 0.35), - vec3(2.8, 0.010, -0.45), - vec3(1.5, 0.006, 0.95), - vec3(0.8, 0.003, -1.10), -); - -/// How the surface tilts at `xz` (world metres), from the waves. +/// Steps a reflection ray takes across the screen before it is given up, and +/// how many times a hit is halved to find the surface exactly. +const MIRROR_STEPS = 20u; +const MIRROR_REFINE = 4u; + +/// What the wave field is doing at one point. +struct Surface { + /// −∇h: how the normal leans, from the octaves coarse enough to draw. + slope: vec2, + /// Variance of the gradient of everything too fine to draw — the sharpness + /// the reflection has to give up in exchange. + lost: f32, + /// 0 in a trough … 1 on a crest, weighted by the octaves' amplitudes. + crest: f32, + /// How much sea there is at all, 0 = glass … 1 = whipped up. + energy: f32, +} + +/// How fast the water runs here, and which way [m/s]. /// -/// Only the slope is wanted, not the height — the mesh is flat, and a tilted -/// normal is what moves a reflection. The amplitude grows with the wind and -/// dies in shallow water, so a pond stays a mirror and a shore stays a shore -/// whatever the weather is doing offshore. -fn wave_slope(xz: vec2, depth: f32) -> vec2 { - let wind = water.weather.wind.xy; - let speed = length(wind); - // No wind: no direction to spread around, and nothing to spread. - if speed < 0.05 { +/// Nothing tells the shader whether a body is a lake or a river — but the +/// mesh's own normal does. `content::water` never lets a surface sink below +/// the level its shoreline settled at, and a lake's level is level: its normal +/// stands straight up. A river's does not, because there the surface follows +/// the fall of the valley, and how far the normal leans is the gradient the +/// water runs down. +/// +/// Manning's formula turns that gradient into a speed, with the roughness of a +/// natural bed and the depth for the hydraulic radius: a metre and a half of +/// water on a fall of one in a thousand runs at a good metre a second, which +/// is what a German lowland river does. +fn flow(normal: vec3, depth: f32) -> vec2 { + // For a height field, the normal is `(−∂h/∂x, 1, −∂h/∂z)` — so this is the + // way downhill, and its length is the fall. + let downhill = vec2(normal.x, normal.z) / max(normal.y, 0.05); + // Elevation data is never quite flat, not even under a lake. Below a tenth + // of a per mille nothing is running; that is the DGM's own noise. + let grade = length(downhill) - 1.0e-4; + if grade <= 0.0 || depth < 0.05 { return vec2(0.0); } - let dir = wind / speed; - // The wind's fetch — waves need room to grow — is folded into a saturating - // power law: a breeze raises ripples quickly, a storm adds little beyond. - let grown = pow(clamp(speed / 9.0, 0.0, 1.0), 1.4); - let shore = smoothstep(0.0, 0.6, depth); - let amp = grown * shore; - if amp <= 0.001 { - return vec2(0.0); + let speed = min(28.0 * pow(min(depth, 3.0), 0.667) * sqrt(grade), 3.0); + return normalize(downhill) * speed; +} + +/// The wave field at `p` (metres in the body's own frame), for water `depth` +/// metres deep and `shore` metres from the waterline, running at `stream`. +/// `footprint` is how much ground one pixel covers here — the resolution the +/// field is drawn to. +fn waves(p: vec2, depth: f32, shore: f32, stream: vec2, footprint: f32) -> Surface { + let time = globals.time; + let wind = water.weather.wind.xy; + let speed = length(wind); + let current = length(stream); + // A river makes its own chop, whatever the air above it is doing. + let stir = max(speed, current * 1.6); + // The waves run with the wind. With no wind there is no direction to run + // in; the current gives one, and failing that any fixed one will do, + // because there is nothing left to see anyway. + var dir = vec2(0.786, 0.618); + if speed > 0.05 { + dir = wind / speed; + } else if current > 0.05 { + dir = stream / current; } + let across = vec2(-dir.y, dir.x); + // Sea state. The square root is the energy: a breeze already covers the + // water in ripples, and more wind mostly makes them longer, not steeper. + // Never quite zero — perfectly still water is a thing photographs almost + // never show. + let sea = sqrt(clamp(stir / 7.0, 0.04, 1.0)); + // The cat's paws that walk over a lake: the short waves are what a gust + // raises and a lull lays flat again. A gust is a streak, long along the + // wind and narrow across it, and it drifts downwind. + let blown_by = p - wind * time * 0.6; + let gust = 0.5 + 0.9 * noise21(vec2(dot(blown_by, dir) * 0.006, dot(blown_by, across) * 0.02)); + // Ten sines summed on a straight coordinate make a washboard, whatever + // their directions are. Bending the ground they are read from — a slow + // noise, drifting with the water — is what turns their crests into the + // wandering, interrupted lines a lake actually shows. + let warp = vec2( + noise21(p * 0.021 - stream * time * 0.02), + noise21(p * 0.021 + 41.7 - stream * time * 0.02), + ) - 0.5; + let at = p + warp * 9.0; + + var out: Surface; + out.slope = vec2(0.0); + out.lost = 0.0; + out.crest = 0.0; + out.energy = sea; + var weight = 1.0e-6; + var wavelength = LONGEST; + // What every octave so far has pushed the ground under the next one by. + var bend = vec2(0.0); + for (var i = 0u; i < OCTAVES; i++) { + // 1 for the longest octave, 0 for the shortest. + let band = f32(OCTAVES - 1u - i) / f32(OCTAVES - 1u); + let k = 6.2831853 / wavelength; + + // What lets this octave grow: the wind — a long wave needs far more of + // it than a ripple does — the room between here and the shore, and the + // water under it, since a wave feels the bed at a fraction of its own + // length and dies as it shoals. + let blown = pow(sea, mix(0.45, 2.6, band)); + let room = smoothstep(wavelength * 0.22, wavelength * 0.95, shore); + let bed = smoothstep(wavelength * 0.015, wavelength * 0.14, depth); + let amp = OCTAVE_SLOPE * blown * room * bed * mix(1.0, gust, 1.0 - band); - var slope = vec2(0.0); - for (var i = 0u; i < 7u; i++) { - let wave = WAVES[i]; - let wavenumber = 6.2831853 / wave.x; - let s = sin(wave.z); - let c = cos(wave.z); - let k = vec2(dir.x * c - dir.y * s, dir.x * s + dir.y * c) * wavenumber; - // Deep-water dispersion: long waves run faster, as they do on a lake. - let phase_speed = sqrt(9.81 / wavenumber); - let phase = dot(k, xz) - phase_speed * wavenumber * globals.time; - slope += k * (wave.y * amp * cos(phase)); + // Below the pixel it would have to fit in, an octave is not drawn but + // remembered: its gradient goes into the roughness instead of into the + // normal. That is the whole difference between a far lake that + // glitters and one that boils. + let seen = smoothstep(footprint * 1.0, footprint * 4.0, wavelength); + out.lost += (1.0 - seen) * amp * amp * 0.5; + if seen > 0.002 { + // Spread around the wind: the long waves run with it, within ten + // degrees or so, the short ones cross it at up to fifty — and it is + // that crossing that breaks a reflection. + let angle = mix(0.9, 0.2, band) * (fract(f32(i) * 0.61803) - 0.5) * 2.0; + let s = sin(angle); + let c = cos(angle); + let along = vec2(dir.x * c - dir.y * s, dir.x * s + dir.y * c); + // Waves come in groups — a few crests together, then a smooth + // patch — so every octave is let through a noise of its own, + // three or four wavelengths across and travelling with the water. + let drift = at - stream * time; + let groups = 0.45 + 1.1 * noise21(drift * (0.3 / wavelength) + f32(i) * 31.0); + // And a crest is not a straight line across a lake. A phase + // shifted by a noise a couple of wavelengths wide lets every + // octave's crests wander, break and pick up again — without it, + // ten straight wave trains cross into a woven cloth. + let wander = noise21(drift * (0.36 / wavelength) + f32(i) * 7.3) * 6.2831853; + // Deep-water dispersion — a long wave runs faster than a short one + // — over a pattern that travels with the current. + let omega = sqrt(9.81 * k); + let phase = k * dot(along, at + bend - stream * time) - omega * time + wander; + // Not a sine: water stands on sharp crests and lies in long + // troughs, and the difference is what a photograph shows. + let bulge = exp(1.4 * (sin(phase) - 1.0)); + let drawn = amp * groups; + out.slope -= along * (drawn * seen * 1.9 * cos(phase) * bulge); + out.crest += bulge * drawn / k; + weight += drawn / k; + } + // Short waves ride on the long ones and are dragged about by them. + // Reading each octave at a point the ones above it have pushed aside + // is what turns a sum of sines into a sea instead of a corduroy. + bend += out.slope * wavelength * 0.22; + wavelength *= RATIO; } - return slope; + out.crest /= weight; + return out; } /// The whole look, laid over a finished `PbrInput` before it is lit. `column` -/// is the water depth under the fragment, in metres, from the vertex colour. -fn water_pbr(column: f32, input: PbrInput) -> PbrInput { +/// is the water depth under the fragment and `edge` its distance from the +/// waterline, both in metres, both from the vertex colour; `uv` is where the +/// fragment lies in the body's own frame, in metres east and north of its +/// centre. +fn water_pbr(column: f32, edge: f32, uv: vec2, input: PbrInput) -> PbrInput { var out = input; let depth = max(column, 0.0); + let shore = max(edge, 0.0); + // The body's frame, turned to lie parallel to the render axes (+x east, + // +z south) so a wind vector means the same thing in it. + let here = vec2(uv.x, -uv.y); let xz = out.world_position.xz; + let time = globals.time; + let wind = length(water.weather.wind.xy); - // --- The body -------------------------------------------------------------- - // Murky green-brown in the shallows — the colour of a German river over - // its sandy bed — going near-black with depth. `exp` is how light dies in - // water; the rate is tuned so a metre-deep ditch still shows its bottom - // and a lake past three metres reads as deep. - var murky = vec3(0.040, 0.062, 0.048); - let deep = vec3(0.003, 0.012, 0.015); - let clear = 1.0 - exp(-depth * 0.30); - // A ripple's crest catches the sky and comes back lighter — a large-scale - // dapple keeps a wide surface from looking like one painted colour. - let dapple = noise21(xz * 0.11) * 0.35 + noise21(xz * 0.43) * 0.15; - murky = murky * (1.0 + dapple); - let color = mix(murky, deep, clear); - - // The ground shows through only where the column is thin — the shore, - // where a hard edge would read as a filled polygon. Deeper in, the water - // is opaque: its reflection is, too, and a blend would dim the sky it - // mirrors along with the body. - let depth_factor = 1.0 - exp(-depth * 0.55); - out.material.base_color = vec4(color, mix(0.82, 0.97, depth_factor)); + // How much ground one pixel covers here [m] — the finest the surface can + // be drawn to before it turns into noise. + let pixel = fwidth(here); + let footprint = max(max(abs(pixel.x), abs(pixel.y)), 0.008); - // --- The microsurface ------------------------------------------------------ - // Glassy when the wind is gone, rougher as it picks up: the micro-facets - // a wave spectrum leaves behind blur the reflection into the horizon haze - // every sailor knows. - let wind = length(water.weather.wind.xy); - out.material.perceptual_roughness = mix(0.055, 0.22, clamp(wind / 14.0, 0.0, 1.0)); - out.material.metallic = 0.0; - out.material.reflectance = vec3(WATER_REFLECTANCE, WATER_REFLECTANCE, WATER_REFLECTANCE); + let stream = flow(out.world_normal, depth); + let sea = waves(here, depth, shore, stream, footprint); // --- The surface ----------------------------------------------------------- - var slope = wave_slope(xz, depth); + var slope = sea.slope; // Rain rings the surface where it lands, whatever the wind is doing. - slope += ripple_slope(xz, globals.time, water.weather.state.z); - out.N = normalize(vec3(out.N.x + slope.x, out.N.y, out.N.z + slope.y)); + slope += ripple_slope(xz, time, water.weather.state.z); + var normal = normalize(vec3(out.N.x + slope.x, out.N.y, out.N.z + slope.y)); - // The clouds shade the water like they shade everything else. - let shade = cloud_shade(xz, water.weather, globals.time); - out.diffuse_occlusion *= shade; - out.specular_occlusion *= shade; + // --- The microsurface ------------------------------------------------------ + // Glassy when the wind is gone, rougher as it picks up, and rougher again + // for every octave of waves too small to draw at this distance: + // α² = α₀² + 2σ², the usual way a distribution of normals is folded into a + // reflection lobe. + let calm = mix(0.04, 0.13, clamp(wind / 14.0, 0.0, 1.0)); + let alpha = sqrt(calm * calm * calm * calm + 2.0 * sea.lost); + var roughness = clamp(sqrt(alpha), 0.03, 0.5); + + // --- What comes up through it ---------------------------------------------- + // Two things happen to light in water: it is absorbed, and it is scattered + // back out. Absorption is Beer's law on the way down and up, red first — + // the attenuation the transmission applies over the column. Scatter is + // what makes deep water a colour of its own instead of black: the diffuse + // part of the material, which is what is left once the transmission has + // taken its share. + // + // `clear` is how much of what lies under the surface still reaches the eye + // unscattered: everything at the waterline, a sixth past five metres. + let clear = exp(-depth * 0.35); + // A bank is more than thin water. It is where the weed grows, where the + // wave stirs the sand up and where what the water carries settles out — + // turbid, and green-brown with it. + let margin = (1.0 - smoothstep(1.0, 16.0, shore)) * (0.55 + 0.6 * noise21(here * 0.09)); + let transmission = mix(0.55, 0.94, clear) * (1.0 - margin * 0.3); + // The albedo the scatter is worth: two to seven per cent, blue-green in + // deep water, grey-green over a sandy shallow, brown at a weedy edge. Bevy + // draws the diffuse part as `base × (1 − transmission)` and tints the + // transmitted light with `base × transmission`, so the base colour is the + // scatter divided back out — which leaves the bed seen through thin water + // nearly untinted (the column's own tint is the attenuation), and through + // deep water dimmed to almost nothing, as it is. + let scatter = mix( + mix(vec3(0.022, 0.058, 0.066), vec3(0.036, 0.050, 0.040), clear), + vec3(0.062, 0.070, 0.038), + margin * 0.7, + ); + var color = min(scatter / max(1.0 - transmission, 0.06), vec3(1.0)); + // A wave is not lit like the flat it stands on: a crest is thin water with + // the sky behind it, passing the light instead of stopping it, and a + // trough lies in the shade of the crests around it. + let lifted = clamp(length(sea.slope) * 2.5, 0.0, 1.0); + color *= 1.0 + (sea.crest - 0.45) * 0.6 * lifted; + color += vec3(0.030, 0.075, 0.058) * smoothstep(0.35, 1.0, sea.crest) * lifted; + + // --- The shore ------------------------------------------------------------- + // The band the water works: foam where a wave runs out. It breathes with + // the swell rather than lying on the polygon's edge like a drawn line, it + // comes in stretches — a bank foams where the wave runs in and stays quiet + // in the lee — and two noises tear up what is left of it, because a + // waterline is ragged. + let surge = mix(0.5, 1.0, sea.crest); + let reach = (0.4 + 2.6 * sea.energy) * surge; + let stretch = smoothstep(0.3, 0.8, noise21(here * 0.02 + 13.0)); + let torn = noise21(here * 0.5 - water.weather.wind.xy * time * 0.05) * 0.6 + + noise21(here * 1.9) * 0.4; + let run = 1.0 - smoothstep(0.0, reach, shore); + var foam = clamp(run * run * (0.25 + stretch) * torn * 2.2, 0.0, 1.0) + * smoothstep(0.15, 0.7, sea.energy); + // Out on the open water a gale breaks the crests too. From about eight + // metres a second the tops blow off and leave the streaks of spume that + // are a storm's whole signature on a lake — patches drifting downwind, + // and it is the patches rather than the crests they sit on that still + // read from half a kilometre away. + let spume = noise21(here * 0.11 - water.weather.wind.xy * time * 0.25) * 0.6 + + noise21(here * 0.6 - water.weather.wind.xy * time * 0.3) * 0.4; + foam = max( + foam, + smoothstep(0.30, 0.68, spume * 0.6 + sea.crest * 0.4) + * smoothstep(8.0, 16.0, wind), + ); + color = mix(color, vec3(0.70, 0.745, 0.755), foam); + roughness = mix(roughness, 0.62, foam); + // Foam is bubbles, not water: it lies on the surface, flattens it, and + // nothing shows through it. + normal = normalize(mix(normal, vec3(0.0, 1.0, 0.0), foam * 0.7)); + + out.N = normal; + out.material.base_color = vec4(color, 1.0); + out.material.perceptual_roughness = roughness; + out.material.metallic = 0.0; + out.material.reflectance = vec3(WATER_REFLECTANCE, WATER_REFLECTANCE, WATER_REFLECTANCE); + out.material.specular_transmission = transmission * (1.0 - foam); + out.material.diffuse_transmission = 0.0; + out.material.ior = WATER_IOR; + // The refracted ray is followed down through the column to where it meets + // the bed, and the picture is read there — which is what a refraction of + // a bed is. Capped, because past a couple of metres the column has taken + // the bed out of the picture anyway, and a long ray only ever wanders off + // the screen. + out.material.thickness = clamp(depth, 0.03, 1.6); + // Beer's law per metre: red goes first, green and blue last, and a lake + // is the colour of what is left. + out.material.attenuation_color = vec4(0.30, 0.64, 0.58, 1.0); + out.material.attenuation_distance = 1.4; + + // The clouds shade the water like they shade everything else — and so does + // the bank, where nothing better is known. Trees, reeds and the ground + // behind them stand exactly where the water at the edge of a body would + // otherwise mirror the bright horizon; the reflection rays find them + // where they can, and this is what stands in where they cannot. + let open = smoothstep(0.0, 11.0, shore); + let shade = cloud_shade(xz, water.weather, time); + out.diffuse_occlusion *= shade * mix(0.8, 1.0, open); + out.specular_occlusion *= shade * mix(0.5, 1.0, open); return out; } +/// Projects a point on a reflection ray into the screen. `xy` is where on the +/// viewport (0…1), `z` the depth the ray has there, `w` = 1 while the point is +/// in front of the camera and on the screen at all. +fn on_screen(p: vec3) -> vec4 { + let clip = view.clip_from_world * vec4(p, 1.0); + if clip.w <= 0.01 { + return vec4(0.0); + } + let ndc = clip.xyz / clip.w; + if max(abs(ndc.x), abs(ndc.y)) > 1.0 { + return vec4(0.0); + } + return vec4(ndc.xy * vec2(0.5, -0.5) + 0.5, ndc.z, 1.0); +} + +#ifdef DEPTH_PREPASS +/// The depth the world was drawn with at a point of the viewport (0…1). +fn world_depth(uv: vec2) -> f32 { + return prepass_depth(vec4(view.viewport.xy + uv * view.viewport.zw, 0.0, 0.0), 0u); +} + +/// Whether a point on the ray has gone behind the world — and not so far +/// behind that it has passed through a pole or a tree and out the back: a +/// hit is a surface within a distance that grows with the ray, since a far +/// pixel is a large piece of world. +fn behind(screen: vec4, t: f32) -> bool { + let scene = world_depth(screen.xy); + if scene <= screen.z { + return false; + } + let gap = depth_ndc_to_view_z(scene) - depth_ndc_to_view_z(screen.z); + return gap < max(1.2, t * 0.14); +} +#endif + +/// Where a reflection ray from `origin` along `dir` meets the world on the +/// screen: `xy` the viewport point to read the picture at, `w` how far to +/// believe it (0 = it met nothing, or nothing the screen can say). +fn trace(origin: vec3, dir: vec3, frag_coord: vec2) -> vec4 { +#ifdef DEPTH_PREPASS + // Each pixel starts its march a different fraction of a step in, so the + // steps of neighbouring pixels do not line up into bands. +#ifdef TEMPORAL_JITTER + let jitter = interleaved_gradient_noise(frag_coord, globals.frame_count); +#else + let jitter = interleaved_gradient_noise(frag_coord, 0u); +#endif + var step = 0.35; + var t = step * (0.3 + jitter); + var before = 0.0; + var hit = -1.0; + for (var i = 0u; i < MIRROR_STEPS; i++) { + let screen = on_screen(origin + dir * t); + if screen.w == 0.0 { + // Off the screen: nothing more to be found there. + return vec4(0.0); + } + if behind(screen, t) { + hit = t; + break; + } + before = t; + step *= 1.32; + t += step; + } + if hit < 0.0 { + return vec4(0.0); + } + // The surface lies between the last free step and the first hit; halve + // the interval a few times to land on it. + var lo = before; + var hi = hit; + for (var i = 0u; i < MIRROR_REFINE; i++) { + let mid = (lo + hi) * 0.5; + let screen = on_screen(origin + dir * mid); + if screen.w > 0.0 && behind(screen, mid) { + hi = mid; + } else { + lo = mid; + } + } + let screen = on_screen(origin + dir * hi); + // Trust fades at the edge of the screen, where the reflection would be cut + // off with a straight line, and for rays that come back towards the + // camera, which only ever find the near side of things. + let ndc = abs(screen.xy * 2.0 - 1.0); + let edge = smoothstep(1.0, 0.8, max(ndc.x, ndc.y)); + let away = smoothstep(-0.25, 0.15, dot(dir, normalize(origin - view.world_position))); + return vec4(screen.xy, 0.0, edge * away * screen.w); +#else + return vec4(0.0); +#endif +} + +/// The world's reflection in the surface, ready to add to the lit colour: +/// `rgb` the reflected picture weighted by fresnel and by how far it is to be +/// believed, `a` that belief — what the environment probe's specular is +/// stood down by. +fn mirror_world(input: PbrInput, frag_coord: vec2) -> vec4 { + // A rough surface's reflection is the blur the probe already is; the + // march only pays off while the mirror is sharp. + let sharp = 1.0 - smoothstep(0.18, 0.42, input.material.perceptual_roughness); + // Foam is not a mirror. + let glassy = input.material.specular_transmission; + if sharp * glassy <= 0.002 { + return vec4(0.0); + } + var dir = reflect(-input.V, input.N); + // A wave's face can lean far enough that a grazing view reflects into the + // water — which is the bed in the picture, and not what is wanted. + dir.y = max(dir.y, 0.03); + dir = normalize(dir); + let found = trace(input.world_position.xyz, dir, frag_coord); + if found.w <= 0.0 { + return vec4(0.0); + } + let full = vec2(textureDimensions(view_transmission_texture)); + let at = (view.viewport.xy + found.xy * view.viewport.zw) / full; + var picture = textureSampleLevel(view_transmission_texture, view_transmission_sampler, at, 0.0); +#ifdef TONEMAP_IN_SHADER + picture = approximate_inverse_tone_mapping(picture, view.color_grading); +#endif + // The same weight the probe's specular carries: Schlick's fresnel between + // the water's 2 % and the grazing mirror. + let cos_theta = clamp(dot(input.N, input.V), 0.0, 1.0); + let fresnel = WATER_F0 + (1.0 - WATER_F0) * pow(1.0 - cos_theta, 5.0); + let belief = found.w * sharp * clamp(glassy / 0.3, 0.0, 1.0); + return vec4(picture.rgb * fresnel * belief, belief); +} + @fragment fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput { var pbr_input = pbr_input_from_standard_material(in, is_front); - pbr_input = water_pbr(in.color.r, pbr_input); + pbr_input = water_pbr(in.color.r, in.color.g, in.uv, pbr_input); + + // Where the world is found in the surface, the sky's probe steps back by + // as much; the picture is already lit and exposed, so it goes in after + // the lighting, on the same footing. + let mirror = mirror_world(pbr_input, in.position.xy); + pbr_input.specular_occlusion *= 1.0 - mirror.a; var out: FragmentOutput; out.color = apply_pbr_lighting(pbr_input); + out.color = vec4(out.color.rgb + mirror.rgb, out.color.a); out.color = main_pass_post_lighting_processing(pbr_input, out.color); return out; }