1use 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 WaterStyle { Voxel, Smooth, Realistic } default Smooth
29}
30
31#[derive(Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct VoxelWater {
35 pub style: WaterStyle,
37 pub cells: i32,
39 pub cell: f32,
41 pub wave_height: f32,
43 pub speed: f32,
45 pub wavelength: f32,
47 pub direction: f32,
49 pub ripples: f32,
51 pub depth: f32,
53 pub seed: i32,
55 pub physics: bool,
57 pub buoyancy: f32,
59 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 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 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 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 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 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 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 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
211pub 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 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
241pub 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 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}