Skip to main content

kvas_engine/
water.rs

1//! Voxel water: an animated water surface built from wave functions, drawn either as stacked
2//! voxel columns (blocky, Minecraft-like) or as a smooth semi-realistic sheet.
3//!
4//! The surface is one `DynamicMesh` rebuilt every frame from a handful of travelling waves plus
5//! noise ripples, so it costs a single draw call. Transparency, reflections and colour come from
6//! the entity's `MeshRenderer` (set sensibly the first time the component is added).
7//!
8//! ```
9//! use kvas_engine::{prelude::*, scene::Scene, water::VoxelWater};
10//! let mut scene = Scene::default();
11//! let lake = scene.spawn("Lake");
12//! scene.world.insert(lake, VoxelWater::default());
13//! kvas_engine::water::update_water(&mut scene.world, 1.0);
14//! assert!(scene.world.has::<DynamicMesh>(lake));
15//! ```
16
17use crate::components::*;
18use crate::mesh::{MeshData, Vertex};
19use crate::noise::{Noise, NoiseKind};
20use crate::reflect::Field;
21use glam::{Mat4, Vec3};
22use kvas_ecs::World;
23use serde::{Deserialize, Serialize};
24use std::sync::Arc;
25
26crate::choice_enum! {
27    /// How the water surface is shaped: stepped voxel columns, smooth, or smooth with sharp crests.
28    WaterStyle { Voxel, Smooth, Realistic } default Smooth
29}
30
31/// An animated body of water centred on the entity.
32#[derive(Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct VoxelWater {
35    /// Look of the surface.
36    pub style: WaterStyle,
37    /// Cells along X and Z (the surface is `cells * cell` metres wide).
38    pub cells: i32,
39    /// Edge length of one cell in metres.
40    pub cell: f32,
41    /// Wave height (crest to rest level) in metres.
42    pub wave_height: f32,
43    /// How fast waves travel, 1 = normal.
44    pub speed: f32,
45    /// Distance between wave crests in metres.
46    pub wavelength: f32,
47    /// Compass direction the waves travel towards, in degrees.
48    pub direction: f32,
49    /// Small noise ripples on top of the main waves, 0..1.
50    pub ripples: f32,
51    /// Water depth in metres below the surface (voxel style draws the block sides).
52    pub depth: f32,
53    /// Deterministic ripple seed.
54    pub seed: i32,
55    /// Whether rigid bodies float in this water.
56    pub physics: bool,
57    /// Lift relative to weight when fully submerged: 1 hovers, above 1 floats up, below 1 sinks slowly.
58    pub buoyancy: f32,
59    /// Water resistance slowing submerged bodies.
60    pub drag: f32,
61}
62
63impl Default for VoxelWater {
64    fn default() -> Self {
65        Self { style: WaterStyle::Smooth, cells: 48, cell: 0.5, wave_height: 0.18, speed: 1.0, wavelength: 6.0, direction: 30.0, ripples: 0.5, depth: 1.5, seed: 11, physics: true, buoyancy: 1.7, drag: 2.5 }
66    }
67}
68
69impl Component for VoxelWater {
70    fn fields(&mut self) -> Vec<Field<'_>> {
71        vec![
72            Field::choice("style", &mut self.style),
73            Field::i32("cells", &mut self.cells),
74            Field::f32("cell", &mut self.cell),
75            Field::f32("wave_height", &mut self.wave_height),
76            Field::f32("speed", &mut self.speed),
77            Field::f32("wavelength", &mut self.wavelength),
78            Field::f32("direction", &mut self.direction),
79            Field::f32("ripples", &mut self.ripples),
80            Field::f32("depth", &mut self.depth),
81            Field::i32("seed", &mut self.seed),
82            Field::bool("physics", &mut self.physics),
83            Field::f32("buoyancy", &mut self.buoyancy),
84            Field::f32("drag", &mut self.drag),
85        ]
86    }
87}
88
89impl VoxelWater {
90    /// Surface height (metres above the rest level) at a local position and time.
91    pub fn height_at(&self, noise: &Noise, x: f32, z: f32, t: f32) -> f32 {
92        let k = std::f32::consts::TAU / self.wavelength.max(0.5);
93        let dir = self.direction.to_radians();
94        let main = Vec3::new(dir.cos(), 0.0, dir.sin());
95        let crest = if self.style == WaterStyle::Realistic { 1.35 } else { 1.0 };
96        let mut h = 0.0;
97        // Four waves around the main direction with different lengths, speeds and phases.
98        for (i, (rot, len, amp)) in [(0.0, 1.0, 1.0), (0.45, 0.62, 0.55), (-0.7, 0.4, 0.3), (1.1, 0.27, 0.16)].into_iter().enumerate() {
99            let a = dir + rot;
100            let d = Vec3::new(a.cos(), 0.0, a.sin());
101            let kk = k / len;
102            let w = (9.81f32 * kk).sqrt() * self.speed;
103            let phase = kk * (d.x * x + d.z * z) - w * t + i as f32 * 1.7;
104            // Sharpen crests for the realistic look: |sin| shaped peaks and flatter troughs.
105            let s = phase.sin();
106            let shaped = if crest > 1.0 { 1.0 - (1.0 - (s * 0.5 + 0.5)).powf(crest) * 2.0 } else { s };
107            h += shaped * amp;
108        }
109        let _ = main;
110        h *= self.wave_height / 1.8;
111        h + noise.sample3(x * 0.7, z * 0.7, t * 0.6 * self.speed) * self.wave_height * self.ripples * 0.35
112    }
113
114    /// Builds the surface mesh for time `t`.
115    pub fn build_mesh(&self, t: f32) -> MeshData {
116        let n = self.cells.clamp(4, 160) as usize;
117        let cell = self.cell.max(0.05);
118        let noise = Noise::new(NoiseKind::Perlin).with_seed(self.seed as u32).with_octaves(2);
119        let half = n as f32 * cell * 0.5;
120        let mut m = MeshData::default();
121        match self.style {
122            WaterStyle::Voxel => {
123                // One block height step = a fraction of the wave height, so the stairs stay visible.
124                let step = (self.wave_height * 0.5).max(cell * 0.5);
125                let heights: Vec<f32> = (0..n * n)
126                    .map(|i| {
127                        let (cx, cz) = ((i % n) as f32 + 0.5, (i / n) as f32 + 0.5);
128                        let h = self.height_at(&noise, cx * cell - half, cz * cell - half, t);
129                        (h / step).round() * step
130                    })
131                    .collect();
132                let at = |x: i64, z: i64| -> Option<f32> {
133                    (x >= 0 && z >= 0 && x < n as i64 && z < n as i64).then(|| heights[z as usize * n + x as usize])
134                };
135                let bottom = -self.depth.max(cell);
136                for z in 0..n as i64 {
137                    for x in 0..n as i64 {
138                        let h = heights[z as usize * n + x as usize];
139                        let (x0, x1) = (x as f32 * cell - half, (x + 1) as f32 * cell - half);
140                        let (z0, z1) = (z as f32 * cell - half, (z + 1) as f32 * cell - half);
141                        quad(&mut m, [Vec3::new(x0, h, z0), Vec3::new(x0, h, z1), Vec3::new(x1, h, z1), Vec3::new(x1, h, z0)], Vec3::Y, h);
142                        // Side walls where the neighbour is lower or missing (outer edge goes down to the bottom).
143                        for (dx, dz, nrm) in [(1, 0, Vec3::X), (-1, 0, Vec3::NEG_X), (0, 1, Vec3::Z), (0, -1, Vec3::NEG_Z)] {
144                            let low = at(x + dx, z + dz).unwrap_or(bottom);
145                            if low >= h {
146                                continue;
147                            }
148                            let (a, b) = match (dx, dz) {
149                                (1, 0) => (Vec3::new(x1, 0.0, z1), Vec3::new(x1, 0.0, z0)),
150                                (-1, 0) => (Vec3::new(x0, 0.0, z0), Vec3::new(x0, 0.0, z1)),
151                                (0, 1) => (Vec3::new(x1, 0.0, z1), Vec3::new(x0, 0.0, z1)),
152                                _ => (Vec3::new(x0, 0.0, z0), Vec3::new(x1, 0.0, z0)),
153                            };
154                            quad(&mut m, [a.with_y(low), b.with_y(low), b.with_y(h), a.with_y(h)], nrm, h);
155                        }
156                    }
157                }
158            }
159            WaterStyle::Smooth | WaterStyle::Realistic => {
160                let stride = n + 1;
161                // One height evaluation per vertex (normals come from neighbouring heights), spread
162                // over the CPU cores in bands of rows.
163                let mut heights = vec![0.0f32; stride * stride];
164                let workers = std::thread::available_parallelism().map_or(1, |p| p.get()).min(8);
165                let rows_per = stride.div_ceil(if n >= 64 { workers } else { 1 });
166                std::thread::scope(|scope| {
167                    for (band, chunk) in heights.chunks_mut(rows_per * stride).enumerate() {
168                        let noise = &noise;
169                        scope.spawn(move || {
170                            for (r, row) in chunk.chunks_mut(stride).enumerate() {
171                                let z = (band * rows_per + r) as f32 * cell - half;
172                                for (x, h) in row.iter_mut().enumerate() {
173                                    *h = self.height_at(noise, x as f32 * cell - half, z, t);
174                                }
175                            }
176                        });
177                    }
178                });
179                let at = |x: usize, z: usize| heights[z.min(n) * stride + x.min(n)];
180                m.vertices.reserve(stride * stride);
181                for z in 0..=n {
182                    for x in 0..=n {
183                        let (px, pz) = (x as f32 * cell - half, z as f32 * cell - half);
184                        let dx = (at(x + 1, z) - at(x.saturating_sub(1), z)) / (2.0 * cell);
185                        let dz = (at(x, z + 1) - at(x, z.saturating_sub(1))) / (2.0 * cell);
186                        let normal = Vec3::new(-dx, 1.0, -dz).normalize();
187                        m.vertices.push(Vertex { pos: [px, at(x, z), pz], normal: normal.into(), uv: [px * 0.1, pz * 0.1] });
188                    }
189                }
190                for z in 0..n {
191                    for x in 0..n {
192                        let i = (z * stride + x) as u32;
193                        let s = stride as u32;
194                        m.indices.extend([i, i + s, i + 1, i + 1, i + s, i + s + 1]);
195                    }
196                }
197            }
198        }
199        m
200    }
201}
202
203fn quad(m: &mut MeshData, corners: [Vec3; 4], normal: Vec3, tag: f32) {
204    let base = m.vertices.len() as u32;
205    for (i, c) in corners.iter().enumerate() {
206        m.vertices.push(Vertex { pos: (*c).into(), normal: normal.into(), uv: [tag, i as f32] });
207    }
208    m.indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
209}
210
211/// Rebuilds every water surface for time `time` (seconds). Cheap enough to run every frame.
212pub fn update_water(world: &mut World, time: f32) {
213    for e in world.entities_with::<VoxelWater>() {
214        let Some(water) = world.get::<VoxelWater>(e).cloned() else { continue };
215        let version = world.get::<DynamicMesh>(e).map_or(0, |d| d.version) + 1;
216        world.insert(e, DynamicMesh { data: Arc::new(water.build_mesh(time)), version });
217        let fresh = !world.has::<MeshRenderer>(e);
218        if fresh {
219            world.insert(e, MeshRenderer::default());
220        }
221        let m = world.get_mut::<MeshRenderer>(e).unwrap();
222        m.cast_shadows = false;
223        // Smooth and realistic water use the built-in water shader unless the user picked another one.
224        if water.style != WaterStyle::Voxel && (m.shader.is_empty() || m.shader == "builtin:water") {
225            m.shader = "builtin:water".into();
226        } else if water.style == WaterStyle::Voxel && m.shader == "builtin:water" {
227            m.shader.clear();
228        }
229        if fresh || m.color == MeshRenderer::default().color {
230            m.color = match water.style {
231                WaterStyle::Voxel => [0.2, 0.55, 0.85, 0.72],
232                WaterStyle::Smooth => [0.12, 0.42, 0.6, 0.62],
233                WaterStyle::Realistic => [0.05, 0.28, 0.42, 0.55],
234            };
235            m.roughness = 0.04;
236            m.reflectivity = if water.style == WaterStyle::Realistic { 0.85 } else { 0.55 };
237        }
238    }
239}
240
241/// Floats rigid bodies in water: each dynamic body gets an upward force proportional to how much of
242/// it is under the moving surface, plus water drag. Call once per fixed physics step.
243pub fn apply_buoyancy(world: &mut World, time: f32, dt: f32) {
244    use crate::hierarchy;
245    let waters: Vec<(Mat4, VoxelWater)> = world
246        .entities_with::<VoxelWater>()
247        .into_iter()
248        .filter_map(|e| Some((hierarchy::global_matrix(world, e), world.get::<VoxelWater>(e).filter(|w| w.physics)?.clone())))
249        .collect();
250    if waters.is_empty() {
251        return;
252    }
253    for e in world.entities_with::<RigidBody>() {
254        let (mass, velocity) = match world.get::<RigidBody>(e) {
255            Some(b) if b.kind == BodyKind::Dynamic => (b.mass.max(0.001), b.velocity),
256            _ => continue,
257        };
258        let m = hierarchy::global_matrix(world, e);
259        let pos = m.w_axis.truncate();
260        let scale = m.to_scale_rotation_translation().0;
261        let radius = world.get::<Collider>(e).map_or(0.5, |c| c.size.max_element().max(0.05) * 0.5 * scale.max_element());
262        for (wm, water) in &waters {
263            let local = wm.inverse().transform_point3(pos);
264            let half = water.cells.clamp(4, 160) as f32 * water.cell.max(0.05) * 0.5;
265            if local.x.abs() > half || local.z.abs() > half {
266                continue;
267            }
268            let noise = Noise::new(NoiseKind::Perlin).with_seed(water.seed as u32).with_octaves(2);
269            let surface = wm.w_axis.y + water.height_at(&noise, local.x, local.z, time);
270            let depth = surface - (pos.y - radius);
271            if depth <= 0.0 {
272                continue;
273            }
274            let fraction = (depth / (2.0 * radius)).clamp(0.0, 1.0);
275            if let Some(body) = world.get_mut::<RigidBody>(e) {
276                body.impulse.y += mass * 9.81 * fraction * water.buoyancy * dt;
277                // Water resistance, stronger the deeper the body sits; keeps floating objects from bouncing forever.
278                let k = (water.drag * fraction * dt).min(0.9);
279                body.impulse -= velocity * mass * k;
280            }
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::{runtime::Runtime, scene::Scene};
289
290    #[test]
291    fn bodies_float_instead_of_sinking() {
292        let mut scene = Scene::default();
293        let lake = scene.spawn("Lake");
294        scene.world.insert(lake, VoxelWater { wave_height: 0.05, ..Default::default() });
295        let ball = scene.spawn("Ball");
296        scene.world.insert(ball, Transform::at(Vec3::new(0.0, 2.0, 0.0)));
297        scene.world.insert(ball, Collider { shape: ShapeKind::Sphere, ..Default::default() });
298        scene.world.insert(ball, RigidBody { mass: 1.0, ..Default::default() });
299        let mut runtime = Runtime::new(scene, None);
300        for _ in 0..360 {
301            runtime.tick(1.0 / 60.0);
302        }
303        let y = runtime.scene.world.get::<Transform>(ball).unwrap().position.y;
304        assert!(y > -0.6 && y < 1.0, "ball should bob near the surface, was at {y}");
305    }
306}