gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel/src/particulas.rs Rust · 488 líneas · 18.4 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! **Motas**: materia que se soltó del mundo y anda por el aire.
2//!
3//! Es la pieza que faltaba para dos cosas distintas que piden lo mismo: el polvo
4//! que levanta un golpe (y se vuelve a posar) y la **gravedad líquida** del guion
5//! —el entorno desprendiéndose hacia arriba como ceniza cuando el héroe está en
6//! crisis—. Las dos son *desplazamiento dinámico de voxels*: sacar celdas sólidas
7//! del mundo, moverlas por su cuenta un rato, y devolverlas o dejarlas ir.
8//!
9//! No hay motor nuevo: cada mota se dibuja como una **caja analítica**
10//! ([`Entity3d`](llimphi_3d::Entity3d)), que el ray-march ya interseca en el mismo
11//! pase que los voxels (con oclusión y sombra). El tope del uniform es 64, así que
12//! [`Enjambre::entidades`] recorta — y ordena por cercanía al ojo cuando se le dice
13//! desde dónde se mira, para que lo que se caiga del cupo sea lo que no se ve.
14//!
15//! Coordenadas: **de grilla** (`0..dim`, sub-voxel), las mismas de `Entity3d` y de
16//! la fauna. Todo es determinista por semilla: mismo desprendimiento, misma deriva.
17
18use llimphi_3d::glam::Vec3;
19use llimphi_3d::{Entity3d, VoxelGrid};
20
21/// Una mota en vuelo: dónde está, hacia dónde va, de qué color era el voxel del
22/// que salió y cuánto le queda de vida.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct Mota {
25    /// Posición en coordenadas de grilla (sub-voxel).
26    pub pos: Vec3,
27    /// Velocidad en voxels por segundo.
28    pub vel: Vec3,
29    /// Color heredado del voxel de origen.
30    pub color: [u8; 3],
31    /// Segundos que le quedan antes de disiparse.
32    pub vida: f32,
33    /// Medio tamaño de su caja (`0.5` = un voxel entero).
34    pub tam: f32,
35}
36
37/// Enjambre de motas con su propia gravedad y su propio roce.
38///
39/// La **gravedad se declara**, no se asume: negativa en `y` es polvo que cae,
40/// positiva es ceniza que sube. Eso es todo lo que separa un efecto del otro.
41#[derive(Clone, Debug)]
42pub struct Enjambre {
43    /// Las motas vivas.
44    pub motas: Vec<Mota>,
45    /// Aceleración constante (voxels/s²). `-9.8` en `y` = caída normal;
46    /// positiva = la gravedad al revés del guion.
47    pub gravedad: Vec3,
48    /// Roce del aire: fracción de velocidad que se pierde por segundo (`0` = vacío,
49    /// `2.0` = frena rápido, como ceniza flotando).
50    pub arrastre: f32,
51    /// Empuje inicial que reciben las motas al desprenderse (voxels/s), radial
52    /// desde el centro del golpe más una pizca de azar.
53    pub impulso: f32,
54    /// Vida base de una mota en segundos (el azar le suma hasta un 60% más).
55    pub vida: f32,
56    /// Si una mota que muere **devuelve su materia** al mundo (polvo que se posa)
57    /// o se disipa sin dejar rastro (ceniza).
58    pub posar: bool,
59    /// Semilla del azar determinista.
60    semilla: u32,
61    /// Contador de tiradas (parte de la semilla de cada mota).
62    tirada: u32,
63}
64
65impl Enjambre {
66    /// Enjambre vacío con gravedad y roce a elección.
67    pub fn nuevo(gravedad: Vec3, arrastre: f32, semilla: u32) -> Self {
68        Self {
69            motas: Vec::new(),
70            gravedad,
71            arrastre,
72            impulso: 2.4,
73            vida: 2.2,
74            posar: false,
75            semilla,
76            tirada: 0,
77        }
78    }
79
80    /// **Polvo**: cae, se frena poco y **devuelve la materia** al posarse. Lo que
81    /// levanta un pisotón o un derrumbe.
82    pub fn polvo(semilla: u32) -> Self {
83        let mut e = Self::nuevo(Vec3::new(0.0, -9.8, 0.0), 0.6, semilla);
84        e.posar = true;
85        e
86    }
87
88    /// **Ceniza que sube** (la "gravedad líquida"): el entorno se desprende hacia
89    /// arriba, flota con mucho roce y se disipa sin volver.
90    pub fn ceniza(semilla: u32) -> Self {
91        let mut e = Self::nuevo(Vec3::new(0.0, 1.6, 0.0), 1.4, semilla);
92        e.impulso = 1.2;
93        e.vida = 3.4;
94        e
95    }
96
97    /// Cuántas motas hay en el aire.
98    pub fn len(&self) -> usize {
99        self.motas.len()
100    }
101
102    /// `true` si no queda ninguna.
103    pub fn is_empty(&self) -> bool {
104        self.motas.is_empty()
105    }
106
107    /// Apaga el efecto (las motas en vuelo desaparecen sin posarse).
108    pub fn limpiar(&mut self) {
109        self.motas.clear();
110    }
111
112    /// Suelta una mota a mano (para efectos guionados, no arrancados del mundo).
113    pub fn soltar(&mut self, pos: Vec3, vel: Vec3, color: [u8; 3]) {
114        self.motas.push(Mota { pos, vel, color, vida: self.vida, tam: 0.5 });
115    }
116
117    /// **Desprende materia del mundo**: saca hasta `cuantos` voxels sólidos de la
118    /// esfera `(centro, radio)` —los **borra del grid**— y los pone a volar con un
119    /// empuje radial. Devuelve cuántos se soltaron.
120    ///
121    /// El muestreo es a paso fijo sobre la lista de candidatos (ordenada por celda),
122    /// así que el hueco queda repartido por toda la esfera y no en una esquina; y es
123    /// determinista: misma semilla y mismo mundo, mismo desprendimiento.
124    ///
125    /// Ojo: esto **cambia el grid**, así que el llamador tiene que sincronizarlo con
126    /// la GPU (`VoxelRenderer::sync`) como con cualquier otra edición.
127    pub fn desprender(
128        &mut self,
129        grid: &mut VoxelGrid,
130        centro: Vec3,
131        radio: f32,
132        cuantos: usize,
133    ) -> usize {
134        if cuantos == 0 || radio <= 0.0 {
135            return 0;
136        }
137        let dim = grid.dim();
138        let lo = |v: f32| (v - radio).floor().max(0.0) as u32;
139        let hi = |v: f32, d: u32| ((v + radio).ceil() as i64).clamp(0, d as i64) as u32;
140        let mut candidatos: Vec<[u32; 3]> = Vec::new();
141        for x in lo(centro.x)..hi(centro.x, dim[0]) {
142            for y in lo(centro.y)..hi(centro.y, dim[1]) {
143                for z in lo(centro.z)..hi(centro.z, dim[2]) {
144                    let c = Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5);
145                    if (c - centro).length() <= radio && grid.is_solid(x as i32, y as i32, z as i32)
146                    {
147                        candidatos.push([x, y, z]);
148                    }
149                }
150            }
151        }
152        if candidatos.is_empty() {
153            return 0;
154        }
155        let paso = (candidatos.len() as f32 / cuantos as f32).max(1.0);
156        let mut sueltas = 0usize;
157        let mut k = 0usize;
158        while sueltas < cuantos {
159            let idx = (k as f32 * paso) as usize;
160            if idx >= candidatos.len() {
161                break;
162            }
163            let [x, y, z] = candidatos[idx];
164            k += 1;
165            let Some(c) = grid.get(x, y, z) else { continue };
166            if c[3] == 0 {
167                continue;
168            }
169            grid.clear(x, y, z);
170            let pos = Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5);
171            let semilla = self.semilla ^ self.tirada.wrapping_mul(0x9E37_79B9) ^ (sueltas as u32);
172            let radial = (pos - centro).normalize_or_zero();
173            let jitter = Vec3::new(
174                azar(semilla, 1) - 0.5,
175                azar(semilla, 2) - 0.5,
176                azar(semilla, 3) - 0.5,
177            );
178            self.motas.push(Mota {
179                pos,
180                vel: (radial + jitter * 0.8) * self.impulso,
181                color: [c[0], c[1], c[2]],
182                vida: self.vida * (1.0 + 0.6 * azar(semilla, 4)),
183                tam: 0.5,
184            });
185            sueltas += 1;
186        }
187        self.tirada = self.tirada.wrapping_add(1);
188        sueltas
189    }
190
191    /// Avanza `dt` segundos **sin mundo**: integra gravedad y roce, y descarta las
192    /// que se quedaron sin vida. Devuelve cuántas murieron.
193    pub fn paso(&mut self, dt: f32) -> usize {
194        let antes = self.motas.len();
195        self.integrar(dt);
196        self.motas.retain(|m| m.vida > 0.0);
197        antes - self.motas.len()
198    }
199
200    /// Avanza `dt` segundos **contra el mundo**: además de la vida, una mota muere
201    /// al meterse en una celda sólida o al salirse del mapa. Si [`posar`](Self::posar)
202    /// está activo, la que muere **devuelve su color** a la última celda libre que
203    /// ocupó — la materia vuelve al mundo en vez de evaporarse.
204    ///
205    /// Devuelve cuántas murieron. El grid queda sucio: sincronizar con la GPU.
206    pub fn paso_en(&mut self, dt: f32, grid: &mut VoxelGrid) -> usize {
207        let dim = grid.dim();
208        let solida = |g: &VoxelGrid, p: Vec3| -> bool {
209            let (x, y, z) = (p.x.floor(), p.y.floor(), p.z.floor());
210            if x < 0.0 || y < 0.0 || z < 0.0 {
211                return true; // el borde de abajo/costado cuenta como suelo
212            }
213            let (x, y, z) = (x as u32, y as u32, z as u32);
214            if x >= dim[0] || y >= dim[1] || z >= dim[2] {
215                return false; // arriba y afuera: sigue volando (y se irá por vida)
216            }
217            g.is_solid(x as i32, y as i32, z as i32)
218        };
219
220        let antes: Vec<(Vec3, [u8; 3])> = self.motas.iter().map(|m| (m.pos, m.color)).collect();
221        self.integrar(dt);
222        let mut muertas: Vec<(Vec3, [u8; 3])> = Vec::new();
223        let mut viva: Vec<Mota> = Vec::with_capacity(self.motas.len());
224        for (m, (prev, color)) in self.motas.iter().zip(&antes) {
225            let choco = solida(grid, m.pos);
226            if m.vida > 0.0 && !choco {
227                viva.push(*m);
228            } else {
229                // La celda donde posarse es la ÚLTIMA libre, no la que la frenó.
230                muertas.push((if choco { *prev } else { m.pos }, *color));
231            }
232        }
233        self.motas = viva;
234        if self.posar {
235            for (p, color) in &muertas {
236                let (x, y, z) = (p.x.floor(), p.y.floor(), p.z.floor());
237                if x < 0.0 || y < 0.0 || z < 0.0 {
238                    continue;
239                }
240                let (x, y, z) = (x as u32, y as u32, z as u32);
241                if x < dim[0] && y < dim[1] && z < dim[2] && !grid.is_solid(x as i32, y as i32, z as i32)
242                {
243                    grid.set(x, y, z, *color);
244                }
245            }
246        }
247        muertas.len()
248    }
249
250    /// Las motas como cajas para el ray-march, recortadas a `tope` (el uniform del
251    /// motor admite 64). Con `ojo` (coords de grilla) se ordenan por cercanía, así
252    /// lo que no entra en el cupo es lo que está más lejos de la cámara.
253    pub fn entidades(&self, tope: usize, ojo: Option<Vec3>) -> Vec<Entity3d> {
254        let mut idx: Vec<usize> = (0..self.motas.len()).collect();
255        if let Some(o) = ojo {
256            idx.sort_by(|&a, &b| {
257                let da = (self.motas[a].pos - o).length_squared();
258                let db = (self.motas[b].pos - o).length_squared();
259                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
260            });
261        }
262        idx.truncate(tope);
263        idx.iter()
264            .map(|&i| {
265                let m = &self.motas[i];
266                // Se achica al final de su vida: desaparecer de golpe se nota.
267                let f = (m.vida / self.vida).clamp(0.15, 1.0);
268                let h = m.tam * f;
269                Entity3d { pos: [m.pos.x, m.pos.y, m.pos.z], half: [h, h, h], color: m.color }
270            })
271            .collect()
272    }
273
274    /// Integración común: gravedad, roce y desgaste de vida.
275    fn integrar(&mut self, dt: f32) {
276        let freno = (1.0 - self.arrastre * dt).clamp(0.0, 1.0);
277        for m in &mut self.motas {
278            m.vel += self.gravedad * dt;
279            m.vel *= freno;
280            m.pos += m.vel * dt;
281            m.vida -= dt;
282        }
283    }
284}
285
286/// **Dispara los sucesos de desmoronamiento** que caen en el intervalo `(t0, t1]`:
287/// cada uno desprende materia del mundo hacia el `enjambre`. Devuelve cuántas motas
288/// nuevas salieron.
289///
290/// El intervalo es semiabierto a propósito: quien filma llama una vez por cuadro con
291/// `(t_anterior, t)`, y así un suceso se dispara **exactamente una vez** aunque el
292/// cuadro dure más que otro. Un salto hacia atrás en el tiempo (rebobinar el preview)
293/// no dispara nada — el mundo ya está desmoronado.
294///
295/// El grid queda sucio: sincronizar con la GPU.
296pub fn disparar_sucesos(
297    sucesos: &[crate::Suceso],
298    t0: f32,
299    t1: f32,
300    grid: &mut VoxelGrid,
301    enjambre: &mut Enjambre,
302) -> usize {
303    let mut n = 0;
304    for s in sucesos {
305        let crate::TipoSuceso::Desmorona { radio, motas } = s.tipo else { continue };
306        if s.t > t0 && s.t <= t1 {
307            let centro = Vec3::new(s.pos[0], s.pos[1], s.pos[2]);
308            n += enjambre.desprender(grid, centro, radio, motas as usize);
309        }
310    }
311    n
312}
313
314/// Azar determinista `0..1` a partir de una semilla y una sal (para no repetir el
315/// mismo número en los tres ejes).
316fn azar(semilla: u32, sal: u32) -> f32 {
317    let mut h = semilla ^ sal.wrapping_mul(0x85EB_CA6B);
318    h ^= h >> 16;
319    h = h.wrapping_mul(0x7FEB_352D);
320    h ^= h >> 15;
321    h = h.wrapping_mul(0x846C_A68B);
322    h ^= h >> 16;
323    (h % 10_000) as f32 / 10_000.0
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    /// Un bloque macizo de 8×4×8 en el piso de un mundo 16³.
331    fn mundo() -> VoxelGrid {
332        let mut g = VoxelGrid::new([16, 16, 16]);
333        for x in 4..12 {
334            for y in 0..4 {
335                for z in 4..12 {
336                    g.set(x, y, z, [120, 100, 80]);
337                }
338            }
339        }
340        g
341    }
342
343    fn solidos(g: &VoxelGrid) -> usize {
344        let d = g.dim();
345        let mut n = 0;
346        for x in 0..d[0] {
347            for y in 0..d[1] {
348                for z in 0..d[2] {
349                    if g.is_solid(x as i32, y as i32, z as i32) {
350                        n += 1;
351                    }
352                }
353            }
354        }
355        n
356    }
357
358    #[test]
359    fn desprender_saca_materia_del_mundo_y_la_pone_a_volar() {
360        let mut g = mundo();
361        let antes = solidos(&g);
362        let mut e = Enjambre::ceniza(7);
363        let sueltas = e.desprender(&mut g, Vec3::new(8.0, 2.0, 8.0), 3.0, 20);
364        assert_eq!(sueltas, 20, "se soltaron las que se pidieron");
365        assert_eq!(e.len(), 20);
366        assert_eq!(solidos(&g), antes - 20, "el mundo perdió exactamente esas celdas");
367        assert!(e.motas.iter().all(|m| m.color == [120, 100, 80]), "heredan el color del voxel");
368    }
369
370    #[test]
371    fn no_se_puede_desprender_del_aire() {
372        let mut g = VoxelGrid::new([8, 8, 8]);
373        let mut e = Enjambre::polvo(1);
374        assert_eq!(e.desprender(&mut g, Vec3::new(4.0, 4.0, 4.0), 3.0, 10), 0);
375        assert!(e.is_empty());
376    }
377
378    #[test]
379    fn la_ceniza_sube_y_el_polvo_cae() {
380        let alto = |mut e: Enjambre| {
381            e.soltar(Vec3::new(8.0, 8.0, 8.0), Vec3::ZERO, [200, 200, 200]);
382            for _ in 0..20 {
383                e.paso(1.0 / 30.0);
384            }
385            e.motas.first().map(|m| m.pos.y).unwrap_or(f32::NAN)
386        };
387        let ceniza = alto(Enjambre::ceniza(3));
388        let polvo = alto(Enjambre::polvo(3));
389        assert!(ceniza > 8.0, "la ceniza sube: {ceniza}");
390        assert!(polvo < 8.0, "el polvo cae: {polvo}");
391    }
392
393    #[test]
394    fn misma_semilla_mismo_desprendimiento() {
395        let tirada = |s: u32| {
396            let mut g = mundo();
397            let mut e = Enjambre::ceniza(s);
398            e.desprender(&mut g, Vec3::new(8.0, 2.0, 8.0), 3.0, 12);
399            for _ in 0..10 {
400                e.paso(0.05);
401            }
402            e.motas.clone()
403        };
404        assert_eq!(tirada(11), tirada(11), "determinista por semilla");
405        assert_ne!(tirada(11), tirada(12), "otra semilla, otra dispersión");
406    }
407
408    #[test]
409    fn el_polvo_devuelve_la_materia_al_posarse() {
410        // Cuatro motas en columnas distintas sobre un piso: al caer, cada una deja
411        // su voxel. La materia se conserva — eso es lo que separa el polvo de la
412        // ceniza (la ceniza se disipa a propósito).
413        let mut g = VoxelGrid::new([16, 16, 16]);
414        for x in 0..16 {
415            for z in 0..16 {
416                g.set(x, 0, z, [80, 80, 80]);
417            }
418        }
419        let antes = solidos(&g);
420        let mut e = Enjambre::polvo(5);
421        for x in [3.5_f32, 6.5, 9.5, 12.5] {
422            e.soltar(Vec3::new(x, 6.0, 8.5), Vec3::ZERO, [200, 40, 40]);
423        }
424        for _ in 0..90 {
425            e.paso_en(1.0 / 30.0, &mut g);
426        }
427        assert!(e.is_empty(), "todas se posaron o se gastaron");
428        assert_eq!(solidos(&g), antes + 4, "las cuatro devolvieron su voxel");
429        assert!(g.is_solid(3, 1, 8), "posada justo encima del piso");
430    }
431
432    #[test]
433    fn la_ceniza_no_deja_rastro() {
434        let mut g = VoxelGrid::new([8, 8, 8]);
435        for x in 0..8 {
436            for z in 0..8 {
437                g.set(x, 0, z, [80, 80, 80]);
438            }
439        }
440        let antes = solidos(&g);
441        let mut e = Enjambre::ceniza(2);
442        e.soltar(Vec3::new(4.5, 4.0, 4.5), Vec3::new(0.0, -3.0, 0.0), [200, 40, 40]);
443        for _ in 0..120 {
444            e.paso_en(1.0 / 30.0, &mut g);
445        }
446        assert!(e.is_empty());
447        assert_eq!(solidos(&g), antes, "la ceniza se disipa sin ensuciar el mundo");
448    }
449
450    #[test]
451    fn un_suceso_de_desmoronamiento_se_dispara_una_sola_vez() {
452        use crate::{Suceso, TipoSuceso};
453        let mut g = mundo();
454        let antes = solidos(&g);
455        let sucesos = vec![
456            Suceso {
457                t: 1.5,
458                pos: [8.0, 2.0, 8.0],
459                tipo: TipoSuceso::Desmorona { radio: 3.0, motas: 10 },
460                nota: "se cae el techo".into(),
461            },
462            // Una marca cualquiera en el mismo instante no debe hacer nada.
463            Suceso { t: 1.5, pos: [8.0, 2.0, 8.0], tipo: TipoSuceso::Marca, nota: String::new() },
464        ];
465        let mut e = Enjambre::ceniza(4);
466        // Cuadro anterior al suceso: nada pasa.
467        assert_eq!(disparar_sucesos(&sucesos, 1.0, 1.4, &mut g, &mut e), 0);
468        assert_eq!(solidos(&g), antes);
469        // El cuadro que cruza su instante lo dispara.
470        assert_eq!(disparar_sucesos(&sucesos, 1.4, 1.6, &mut g, &mut e), 10);
471        assert_eq!(solidos(&g), antes - 10);
472        // Los cuadros siguientes NO lo repiten.
473        assert_eq!(disparar_sucesos(&sucesos, 1.6, 2.0, &mut g, &mut e), 0);
474        assert_eq!(e.len(), 10);
475    }
476
477    #[test]
478    fn las_cajas_respetan_el_tope_y_priorizan_lo_cercano() {
479        let mut e = Enjambre::ceniza(9);
480        for k in 0..200 {
481            e.soltar(Vec3::new(k as f32, 5.0, 5.0), Vec3::ZERO, [10, 20, 30]);
482        }
483        let cajas = e.entidades(64, Some(Vec3::new(0.0, 5.0, 5.0)));
484        assert_eq!(cajas.len(), 64, "no se pasa del cupo del uniform");
485        assert!(cajas[0].pos[0] < cajas[63].pos[0], "primero lo que está cerca del ojo");
486        assert!(cajas.iter().all(|c| c.pos[0] < 64.0), "las 64 más cercanas");
487    }
488}