1use crate::components::*;
7use crate::mesh::{MeshData, Vertex};
8use crate::noise::{Noise, NoiseKind};
9use crate::procedural::signature;
10use crate::reflect::Field;
11use glam::Vec3;
12use kvas_ecs::World;
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15pub use crate::voxel_mesh::mesh_grid_greedy;
16
17const FACES: [(Vec3, Vec3, Vec3); 6] = [
18 (Vec3::X, Vec3::Y, Vec3::Z),
19 (Vec3::NEG_X, Vec3::Z, Vec3::Y),
20 (Vec3::Y, Vec3::Z, Vec3::X),
21 (Vec3::NEG_Y, Vec3::X, Vec3::Z),
22 (Vec3::Z, Vec3::X, Vec3::Y),
23 (Vec3::NEG_Z, Vec3::Y, Vec3::X),
24];
25
26pub fn mesh_grid(
30 dim: [usize; 3],
31 cell: f32,
32 origin: Vec3,
33 threshold: f32,
34 shrink: bool,
35 density: impl Fn(usize, usize, usize) -> f32,
36) -> MeshData {
37 if !shrink { return mesh_grid_greedy(dim, cell, origin, threshold, density); }
38 let filled = |x: i64, y: i64, z: i64| -> f32 {
39 if x < 0 || y < 0 || z < 0 || x >= dim[0] as i64 || y >= dim[1] as i64 || z >= dim[2] as i64 {
40 return 0.0;
41 }
42 density(x as usize, y as usize, z as usize)
43 };
44 let mut m = MeshData::default();
45 for z in 0..dim[2] as i64 {
46 for y in 0..dim[1] as i64 {
47 for x in 0..dim[0] as i64 {
48 let d = filled(x, y, z);
49 if d <= threshold {
50 continue;
51 }
52 let center = origin + (Vec3::new(x as f32, y as f32, z as f32) + 0.5) * cell;
53 let half = cell * 0.5 * if shrink { 0.35 + 0.65 * d.clamp(0.0, 1.0) } else { 1.0 };
54 for (n, u, v) in FACES {
55 if !shrink {
56 let o = n.as_ivec3();
57 if filled(x + o.x as i64, y + o.y as i64, z + o.z as i64) > threshold {
58 continue;
59 }
60 }
61 let base = m.vertices.len() as u32;
62 for (su, sv, uvc) in [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)] {
63 let _ = uvc;
64 let p = center + n * half + u * (half * su) + v * (half * sv);
65 m.vertices.push(Vertex { pos: p.into(), normal: n.into(), uv: [d.clamp(0.0, 1.0), 0.5] });
66 }
67 m.indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
68 }
69 }
70 }
71 }
72 m
73}
74
75#[derive(Clone, Serialize, Deserialize)]
79#[serde(default)]
80pub struct VoxelSmoke {
81 pub grid: i32,
83 pub cell: f32,
85 pub rise: f32,
87 pub turbulence: f32,
89 pub emit: f32,
91 pub dissipation: f32,
93 pub threshold: f32,
95 pub seed: i32,
97 #[serde(skip)]
98 #[doc(hidden)]
99 pub state: Option<SmokeState>,
100 #[serde(skip)]
101 #[doc(hidden)]
102 pub sig: u64,
103}
104
105impl Default for VoxelSmoke {
106 fn default() -> Self {
107 Self {
108 grid: 20,
109 cell: 0.22,
110 rise: 3.0,
111 turbulence: 2.2,
112 emit: 3.5,
113 dissipation: 0.28,
114 threshold: 0.07,
115 seed: 3,
116 state: None,
117 sig: 0,
118 }
119 }
120}
121
122impl Component for VoxelSmoke {
123 fn fields(&mut self) -> Vec<Field<'_>> {
124 vec![
125 Field::i32("grid", &mut self.grid),
126 Field::f32("cell", &mut self.cell),
127 Field::f32("rise", &mut self.rise),
128 Field::f32("turbulence", &mut self.turbulence),
129 Field::f32("emit", &mut self.emit),
130 Field::f32("dissipation", &mut self.dissipation),
131 Field::f32("threshold", &mut self.threshold),
132 Field::i32("seed", &mut self.seed),
133 ]
134 }
135}
136
137#[doc(hidden)]
139#[derive(Clone)]
140pub struct SmokeState {
141 n: usize,
142 density: Vec<f32>,
143 scratch: Vec<f32>,
144 time: f32,
145 version: u64,
146}
147
148impl SmokeState {
149 fn at(&self, x: i64, y: i64, z: i64) -> f32 {
150 let n = self.n as i64;
151 if x < 0 || y < 0 || z < 0 || x >= n || y >= n || z >= n {
152 return 0.0;
153 }
154 self.density[(z as usize * self.n + y as usize) * self.n + x as usize]
155 }
156
157 fn sample(&self, p: Vec3) -> f32 {
158 let (f, i) = (p - p.floor(), p.floor());
159 let (x, y, z) = (i.x as i64, i.y as i64, i.z as i64);
160 let l = |a: f32, b: f32, t: f32| a + (b - a) * t;
161 l(
162 l(l(self.at(x, y, z), self.at(x + 1, y, z), f.x), l(self.at(x, y + 1, z), self.at(x + 1, y + 1, z), f.x), f.y),
163 l(l(self.at(x, y, z + 1), self.at(x + 1, y, z + 1), f.x), l(self.at(x, y + 1, z + 1), self.at(x + 1, y + 1, z + 1), f.x), f.y),
164 f.z,
165 )
166 }
167
168 fn step(&mut self, s: &VoxelSmoke, dt: f32) {
170 let n = self.n;
171 let noise = Noise::new(NoiseKind::Perlin).with_seed(s.seed as u32).with_octaves(2);
172 self.time += dt;
173 let t = self.time * 0.6;
174 let source = (n as f32 * 0.5, 1.0, n as f32 * 0.5);
175 for z in 0..n {
176 for y in 0..n {
177 for x in 0..n {
178 let p = Vec3::new(x as f32, y as f32, z as f32);
179 let flow = Vec3::new(
180 noise.sample3(p.x * 0.18, p.y * 0.18 - t, p.z * 0.18 + 5.0),
181 0.0,
182 noise.sample3(p.x * 0.18 + 9.0, p.y * 0.18 - t, p.z * 0.18),
183 ) * s.turbulence
184 + Vec3::Y * s.rise * (0.6 + 0.4 * (y as f32 / n as f32));
185 let back = p + Vec3::splat(0.5) - flow * dt;
186 let mut d = self.sample(back - Vec3::splat(0.5)) * (1.0 - s.dissipation * dt).max(0.0);
187 let dx = p.x + 0.5 - source.0;
188 let dz = p.z + 0.5 - source.2;
189 if y < 2 && dx * dx + dz * dz < (n as f32 * 0.12).powi(2) + 1.0 {
190 d += s.emit * dt * (1.0 - y as f32 * 0.4);
191 }
192 self.scratch[(z * n + y) * n + x] = d.clamp(0.0, 1.0);
193 }
194 }
195 }
196 std::mem::swap(&mut self.density, &mut self.scratch);
197 }
198
199 fn mesh(&self, s: &VoxelSmoke) -> MeshData {
200 let n = self.n;
201 let origin = Vec3::new(-(n as f32) * 0.5 * s.cell, 0.0, -(n as f32) * 0.5 * s.cell);
202 mesh_grid([n, n, n], s.cell, origin, s.threshold, true, |x, y, z| self.density[(z * n + y) * n + x])
203 }
204}
205
206pub fn update_smoke(world: &mut World, dt: f32, simulate: bool) {
208 for e in world.entities_with::<VoxelSmoke>() {
209 let s = world.get_mut::<VoxelSmoke>(e).unwrap();
210 let sig = signature(&(s.grid, s.seed));
211 if s.state.is_none() || s.sig != sig {
212 let n = s.grid.clamp(6, 40) as usize;
213 let mut st = SmokeState { n, density: vec![0.0; n * n * n], scratch: vec![0.0; n * n * n], time: 0.0, version: 0 };
214 let params = s.clone();
215 for _ in 0..90 {
216 st.step(¶ms, 1.0 / 30.0); }
218 s.state = Some(st);
219 s.sig = sig;
220 }
221 let mut st = s.state.take().unwrap();
222 if simulate && dt > 0.0 {
223 st.step(s, dt.min(0.05));
224 }
225 st.version += 1;
226 let (mesh, version) = (st.mesh(s), st.version);
227 s.state = Some(st);
228 world.insert(e, DynamicMesh { data: Arc::new(mesh), version });
229 if !world.has::<MeshRenderer>(e) {
230 world.insert(e, MeshRenderer::default());
231 }
232 let m = world.get_mut::<MeshRenderer>(e).unwrap();
233 m.shader = "builtin:smoke".into();
234 m.cast_shadows = false;
235 if m.color[3] > 0.95 {
236 m.color = [0.78, 0.78, 0.8, 0.9];
237 }
238 }
239}
240
241#[derive(Clone, Serialize, Deserialize)]
246#[serde(default)]
247pub struct VoxelClouds {
248 pub size: Vec3,
250 pub cell: f32,
252 pub coverage: f32,
254 pub scale: f32,
256 pub seed: i32,
258 pub drift: Vec3,
260 pub density: f32,
262 pub refraction: f32,
264 #[serde(skip)]
265 #[doc(hidden)]
266 pub sig: u64,
267}
268
269impl Default for VoxelClouds {
270 fn default() -> Self {
271 Self {
272 size: Vec3::new(240.0, 36.0, 240.0),
273 cell: 3.5,
274 coverage: 0.36,
275 scale: 3.2,
276 seed: 7,
277 drift: Vec3::new(2.0, 0.0, 0.6),
278 density: 0.8,
279 refraction: 0.25,
280 sig: 0,
281 }
282 }
283}
284
285impl Component for VoxelClouds {
286 fn fields(&mut self) -> Vec<Field<'_>> {
287 vec![
288 Field::vec3("size", &mut self.size),
289 Field::f32("cell", &mut self.cell),
290 Field::f32("coverage", &mut self.coverage),
291 Field::f32("scale", &mut self.scale),
292 Field::i32("seed", &mut self.seed),
293 Field::vec3("drift", &mut self.drift),
294 Field::f32("density", &mut self.density),
295 Field::f32("refraction", &mut self.refraction),
296 ]
297 }
298}
299
300pub const CLOUD_VOLUME_PREFIX: &str = "cloudvol:";
302
303#[derive(Clone, Copy, Debug, PartialEq)]
305pub struct CloudVolume {
306 pub size: Vec3,
308 pub cell: f32,
310 pub coverage: f32,
312 pub scale: f32,
314 pub seed: i32,
316}
317
318impl CloudVolume {
319 pub fn of(c: &VoxelClouds) -> Self {
321 Self { size: c.size, cell: c.cell, coverage: c.coverage, scale: c.scale, seed: c.seed }
322 }
323
324 pub fn key(&self) -> String {
326 format!(
327 "{CLOUD_VOLUME_PREFIX}{},{},{},{},{},{},{}",
328 self.size.x, self.size.y, self.size.z, self.cell, self.coverage, self.scale, self.seed
329 )
330 }
331
332 pub fn parse(key: &str) -> Option<CloudVolume> {
334 let n: Vec<&str> = key.strip_prefix(CLOUD_VOLUME_PREFIX)?.split(',').collect();
335 if n.len() != 7 {
336 return None;
337 }
338 let real = |i: usize| n[i].parse::<f32>().ok().filter(|v| v.is_finite());
339 Some(CloudVolume {
340 size: Vec3::new(real(0)?, real(1)?, real(2)?),
341 cell: real(3)?,
342 coverage: real(4)?,
343 scale: real(5)?,
344 seed: n[6].parse().ok()?,
345 })
346 }
347
348 pub fn dims(&self) -> [u32; 3] {
350 let cell = self.cell.max(0.5);
351 [
352 (self.size.x / cell).ceil().clamp(8.0, 160.0) as u32,
353 (self.size.y / (cell * 0.5)).ceil().clamp(6.0, 64.0) as u32,
354 (self.size.z / cell).ceil().clamp(8.0, 160.0) as u32,
355 ]
356 }
357
358 pub fn generate(&self) -> ([u32; 3], Vec<u8>) {
360 let dim = self.dims();
361 let base = Noise::new(NoiseKind::Perlin).with_seed(self.seed as u32).with_octaves(5);
362 let erode = Noise::new(NoiseKind::Worley).with_seed((self.seed as u32).wrapping_add(1)).with_octaves(2);
363 let freq = self.scale / 100.0;
364 let threshold = 1.0 - self.coverage.clamp(0.0, 1.0);
365 let mut out = Vec::with_capacity((dim[0] * dim[1] * dim[2]) as usize);
366 for z in 0..dim[2] {
367 for y in 0..dim[1] {
368 for x in 0..dim[0] {
369 let (fx, fy, fz) = (x as f32 / dim[0] as f32, y as f32 / dim[1] as f32, z as f32 / dim[2] as f32);
370 let p = Vec3::new(fx * self.size.x, fy * self.size.y, fz * self.size.z) * freq;
371 let bottom = (fy / 0.18).clamp(0.0, 1.0);
373 let top = (1.0 - (fy - 0.45) / 0.55).clamp(0.0, 1.0).powf(0.8);
374 let shape = bottom * top;
375 let n = base.sample01(p.x, p.y * 1.7, p.z) * 0.8 + erode.sample01(p.x * 3.1, p.y * 3.1, p.z * 3.1) * 0.2;
376 let d = ((n * shape - threshold * 0.55) * 2.6).clamp(0.0, 1.0);
377 out.push((d * 255.0) as u8);
378 }
379 }
380 }
381 (dim, out)
382 }
383}
384
385fn inverted_box(half: Vec3) -> MeshData {
387 let mut m = MeshData::primitive("cube").unwrap_or_default();
388 for v in &mut m.vertices {
389 v.pos = (Vec3::from(v.pos) * half * 2.0).into();
390 }
391 for tri in m.indices.as_chunks_mut::<3>().0.iter_mut() {
392 tri.swap(1, 2);
393 }
394 m
395}
396
397pub fn update_clouds(world: &mut World, dt: f32, simulate: bool) {
399 let _ = (dt, simulate);
400 for e in world.entities_with::<VoxelClouds>() {
401 let c = world.get::<VoxelClouds>(e).unwrap().clone();
402 let volume = CloudVolume::of(&c);
403 let sig = signature(&(c.size.to_array(), c.cell.to_bits(), c.coverage.to_bits(), c.scale.to_bits(), c.seed));
404 if world.get::<VoxelClouds>(e).unwrap().sig != sig || !world.has::<DynamicMesh>(e) {
405 world.insert(e, DynamicMesh { data: Arc::new(inverted_box(c.size * 0.5)), version: sig });
406 world.get_mut::<VoxelClouds>(e).unwrap().sig = sig;
407 }
408 if !world.has::<MeshRenderer>(e) {
409 world.insert(e, MeshRenderer::default());
410 }
411 let m = world.get_mut::<MeshRenderer>(e).unwrap();
412 m.shader = "builtin:clouds".into();
413 m.texture = volume.key();
414 m.cast_shadows = false;
415 m.color = [1.0, 1.0, 1.0, 0.9]; let half = c.size * 0.5;
417 m.user = [half.x, half.y, half.z, c.density, c.drift.x, c.drift.y, c.drift.z, c.refraction];
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn a_single_voxel_is_a_cube_and_touching_voxels_hide_their_shared_faces() {
427 let one = mesh_grid([1, 1, 1], 1.0, Vec3::ZERO, 0.5, false, |_, _, _| 1.0);
428 assert_eq!(one.indices.len() / 3, 12);
429 let two = mesh_grid([2, 1, 1], 1.0, Vec3::ZERO, 0.5, false, |_, _, _| 1.0);
430 assert_eq!(two.indices.len() / 3, 12, "coplanar outer faces merge into six quads");
431 }
432
433 #[test]
434 fn solid_block_only_pays_for_its_surface() {
435 let n = 10;
436 let m = mesh_grid([n, n, n], 1.0, Vec3::ZERO, 0.5, false, |_, _, _| 1.0);
437 assert_eq!(m.indices.len() / 3, 12, "the entire shell merges into six quads");
438 }
439
440 #[test]
441 fn smoke_rises_and_fades() {
442 let mut s = VoxelSmoke::default();
443 s.dissipation = 0.5;
444 let mut st = SmokeState { n: 20, density: vec![0.0; 8000], scratch: vec![0.0; 8000], time: 0.0, version: 0 };
445 for _ in 0..180 {
446 st.step(&s, 1.0 / 30.0);
447 }
448 let n = st.n;
449 let mass_at = |y0: usize, y1: usize| {
450 (0..n)
451 .flat_map(|z| (y0..y1).flat_map(move |y| (0..n).map(move |x| (x, y, z))))
452 .map(|(x, y, z)| st.density[(z * n + y) * n + x])
453 .sum::<f32>()
454 };
455 assert!(mass_at(0, 4) > 1.0, "smoke exists near the source");
456 assert!(mass_at(8, 20) > 0.5, "and has risen into the upper grid");
457 assert!(st.density.iter().all(|d| (0.0..=1.0).contains(d)));
458 }
459
460 #[test]
461 fn cloud_volume_has_clouds_and_gaps_and_roundtrips_its_key() {
462 let vol = CloudVolume::of(&VoxelClouds { size: Vec3::new(120.0, 30.0, 120.0), cell: 6.0, ..Default::default() });
463 assert_eq!(CloudVolume::parse(&vol.key()), Some(vol));
464 let (dim, data) = vol.generate();
465 assert_eq!(data.len(), (dim[0] * dim[1] * dim[2]) as usize);
466 let filled = data.iter().filter(|&&d| d > 40).count() as f32 / data.len() as f32;
467 assert!(filled > 0.03 && filled < 0.7, "a mix of cloud and clear sky: {filled}");
468 let layer = |y: u32| {
470 (0..dim[2])
471 .flat_map(|z| (0..dim[0]).map(move |x| (x, z)))
472 .map(|(x, z)| data[((z * dim[1] + y) * dim[0] + x) as usize] as u32)
473 .sum::<u32>()
474 };
475 assert!(layer(0) < layer(dim[1] / 3), "bottom layer is thinner than the body");
476 }
477
478 #[test]
479 fn cloud_box_faces_inwards() {
480 let m = inverted_box(Vec3::new(5.0, 2.0, 5.0));
481 let tri = &m.indices[0..3];
482 let p = |i: u32| Vec3::from(m.vertices[i as usize].pos);
483 let n = (p(tri[1]) - p(tri[0])).cross(p(tri[2]) - p(tri[0]));
484 let center_to_face = p(tri[0]);
485 assert!(n.dot(center_to_face) < 0.0, "winding faces the centre of the box");
486 }
487}