gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-voxel/src/utileria.rs Rust · 298 siq'ikuna · 12.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! **Utilería**: los objetos con nombre que un guion necesita mostrar y que no son
2//! ni gente, ni fauna, ni mundo — la estrella que la princesa ve aparecer, la flor,
3//! la perla y la pluma del prendedor.
4//!
5//! Se arman con primitivas SDF, igual que los seres, así que entran al MISMO pase
6//! del ray-marcher con la misma respuesta de luz y **gastan del mismo presupuesto**
7//! del uniform: por eso el reparto les reserva su lugar antes de repartir el resto
8//! entre los actores (ver [`reparto_mixto_con`](crate::reparto_mixto_con)).
9//!
10//! Una escena los coloca con [`UtileriaSpec`]: en un punto del mundo o **en la mano**
11//! de uno de sus actores, apareciendo y desapareciendo en instantes del guion.
12//!
13//! Espacio LOCAL: base en `y = 0`, centrado en X/Z, la cara mirando `+Z`. Se arman
14//! en unidades cómodas y se asientan al alto pedido
15//! ([`asentar`](crate::sdf_kit::asentar)).
16
17use std::f32::consts::{PI, TAU};
18
19use llimphi_3d::glam::Vec3;
20use llimphi_3d::{SdfPrim, SdfSer};
21use serde::{Deserialize, Serialize};
22
23use crate::sdf_kit::{asentar, capsula, elipsoide, esfera, tono};
24
25/// Catálogo de utilería.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum Utileria {
28    /// Estrella de cinco puntas, de cara a la cámara.
29    Estrella,
30    /// Flor de cinco pétalos sobre su tallo.
31    Flor,
32    /// Perla: una esfera nacarada.
33    Perla,
34    /// Pluma: el cañón y el vexilo curvado.
35    Pluma,
36    /// Prendedor: «con un verso y una perla, y una pluma y una flor» — las tres
37    /// juntas sobre una base de oro. Va al final: postcard numera las variantes.
38    Prendedor,
39}
40
41impl Utileria {
42    /// Todas (para ciclar en un editor).
43    pub const ALL: [Utileria; 5] =
44        [Utileria::Estrella, Utileria::Flor, Utileria::Perla, Utileria::Pluma, Utileria::Prendedor];
45
46    /// El color de fábrica de la pieza principal.
47    pub fn color(self) -> [f32; 3] {
48        match self {
49            Utileria::Estrella => [1.0, 0.93, 0.62],
50            Utileria::Flor => [0.96, 0.52, 0.66],
51            Utileria::Perla => [0.95, 0.93, 0.90],
52            Utileria::Pluma => [0.97, 0.97, 0.95],
53            Utileria::Prendedor => [0.95, 0.74, 0.22],
54        }
55    }
56
57    /// El alto de fábrica, en metros (= voxels).
58    pub fn alto(self) -> f32 {
59        match self {
60            Utileria::Estrella => 0.6,
61            Utileria::Flor => 0.45,
62            Utileria::Perla => 0.08,
63            Utileria::Pluma => 0.35,
64            Utileria::Prendedor => 0.14,
65        }
66    }
67
68    /// Si brilla con **luz propia**: se ve con su color aunque sea de noche o esté
69    /// en sombra, y en una escena de noche además alumbra lo que tiene cerca
70    /// ([`SceneSpec::luces_en`](crate::SceneSpec::luces_en)). Sólo la estrella.
71    pub fn luz_propia(self) -> bool {
72        matches!(self, Utileria::Estrella)
73    }
74
75    /// La pieza armada, asentada al `alto` dado y pintada de `color`.
76    pub fn sdf(self, alto: f32, color: [f32; 3]) -> SdfSer {
77        let mut prims = match self {
78            Utileria::Estrella => estrella(color),
79            Utileria::Flor => flor(color),
80            Utileria::Perla => vec![esfera(Vec3::new(0.0, 0.5, 0.0), 0.5, color, 0.0)],
81            Utileria::Pluma => pluma(color),
82            Utileria::Prendedor => prendedor(color),
83        };
84        asentar(&mut prims, alto.max(0.01));
85        SdfSer { prims, pos: [0.0; 3], yaw: 0.0 }
86    }
87}
88
89/// Cinco brazos que salen del centro y afinan hacia la punta, en el plano XY: de
90/// cara a `+Z`, que es donde mira la cámara cuando nadie la gira.
91fn estrella(color: [f32; 3]) -> Vec<SdfPrim> {
92    let c = Vec3::new(0.0, 1.0, 0.0);
93    let mut v = vec![esfera(c, 0.30, color, 0.10)];
94    for i in 0..5 {
95        let a = PI * 0.5 + i as f32 * TAU / 5.0;
96        let punta = c + Vec3::new(a.cos(), a.sin(), 0.0) * 1.0;
97        v.push(capsula(c, 0.24, punta, 0.03, color, 0.10));
98    }
99    v
100}
101
102/// Tallo verde, cinco pétalos alrededor de un centro amarillo.
103fn flor(color: [f32; 3]) -> Vec<SdfPrim> {
104    let verde = [0.30, 0.55, 0.25];
105    let c = Vec3::new(0.0, 1.0, 0.04);
106    let mut v = vec![
107        capsula(Vec3::ZERO, 0.03, c - Vec3::new(0.0, 0.05, 0.04), 0.025, verde, 0.0),
108        esfera(c + Vec3::new(0.0, 0.0, 0.03), 0.09, [0.98, 0.82, 0.25], 0.02),
109    ];
110    for i in 0..5 {
111        let a = PI * 0.5 + i as f32 * TAU / 5.0;
112        let p = c + Vec3::new(a.cos(), a.sin(), 0.0) * 0.17;
113        v.push(elipsoide(p, Vec3::new(0.12, 0.12, 0.04), color, 0.04));
114    }
115    v
116}
117
118/// El cañón fino y el vexilo como una lágrima alargada, un poco inclinados.
119fn pluma(color: [f32; 3]) -> Vec<SdfPrim> {
120    let base = Vec3::ZERO;
121    let punta = Vec3::new(0.18, 1.0, 0.0);
122    vec![
123        capsula(base, 0.015, punta, 0.006, tono(color, 0.8), 0.0),
124        elipsoide(Vec3::new(0.12, 0.62, 0.0), Vec3::new(0.10, 0.36, 0.02), color, 0.03),
125    ]
126}
127
128/// El prendedor de frente: una base ovalada de `color` (oro), y encima la flor (un
129/// centro y cuatro pétalos), la perla y la pluma, cada una con su color de fábrica.
130/// Ocho primitivas: lo mismo que una flor suelta.
131fn prendedor(color: [f32; 3]) -> Vec<SdfPrim> {
132    let c = Vec3::new(0.0, 0.5, 0.0);
133    let flor_c = c + Vec3::new(-0.16, 0.08, 0.10);
134    let mut v = vec![elipsoide(c, Vec3::new(0.50, 0.42, 0.08), color, 0.02)];
135    for i in 0..4 {
136        let a = PI * 0.25 + i as f32 * TAU / 4.0;
137        let p = flor_c + Vec3::new(a.cos(), a.sin(), 0.0) * 0.11;
138        v.push(elipsoide(p, Vec3::new(0.09, 0.09, 0.03), Utileria::Flor.color(), 0.03));
139    }
140    v.push(esfera(flor_c + Vec3::new(0.0, 0.0, 0.03), 0.06, [0.98, 0.82, 0.25], 0.01));
141    v.push(esfera(c + Vec3::new(0.20, -0.12, 0.10), 0.09, Utileria::Perla.color(), 0.0));
142    v.push(elipsoide(
143        c + Vec3::new(0.18, 0.18, 0.09),
144        Vec3::new(0.06, 0.24, 0.02),
145        Utileria::Pluma.color(),
146        0.02,
147    ));
148    v
149}
150
151/// Lo que tarda una pieza en aparecer (crece de nada a su tamaño), en segundos.
152pub const APARECER: f32 = 0.4;
153
154/// **Una pieza de utilería puesta en una escena**: cuál, dónde, de qué tamaño y
155/// cuándo está. Si `lleva` nombra a un actor (índice en
156/// [`SceneSpec::actors`](crate::SceneSpec)), la pieza va en su mano y lo sigue; si
157/// no, queda en `pos` (coords de grilla, como los sucesos).
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct UtileriaSpec {
160    pub cosa: Utileria,
161    /// Dónde está, en coords de grilla `[x, y, z]` (las del mundo y los sucesos).
162    #[serde(default)]
163    pub pos: [f32; 3],
164    /// Alto en metros; `0` = el de fábrica de la pieza.
165    #[serde(default)]
166    pub alto: f32,
167    /// Desde qué instante de la escena se ve (aparece creciendo).
168    #[serde(default)]
169    pub desde: f32,
170    /// Hasta qué instante; `None` = hasta el final.
171    #[serde(default)]
172    pub hasta: Option<f32>,
173    /// Giro sobre su eje vertical, en vueltas por segundo (`0` = quieta).
174    #[serde(default)]
175    pub giro: f32,
176    /// El actor que la lleva en la mano (índice en los actores de la escena).
177    #[serde(default)]
178    pub lleva: Option<usize>,
179    /// Color propio; `None` = el de fábrica.
180    #[serde(default)]
181    pub color: Option<[f32; 3]>,
182}
183
184impl UtileriaSpec {
185    /// Una pieza de fábrica parada en `pos`, visible toda la escena.
186    pub fn en(cosa: Utileria, pos: [f32; 3]) -> Self {
187        Self { cosa, pos, alto: 0.0, desde: 0.0, hasta: None, giro: 0.0, lleva: None, color: None }
188    }
189
190    /// El tamaño relativo en `t`: `0` antes de `desde` y después de `hasta`, crece
191    /// en [`APARECER`] segundos al entrar.
192    pub fn escala_en(&self, t: f32) -> f32 {
193        if t < self.desde || self.hasta.is_some_and(|h| t > h) {
194            return 0.0;
195        }
196        let x = ((t - self.desde) / APARECER).clamp(0.0, 1.0);
197        x * x * (3.0 - 2.0 * x)
198    }
199
200    /// La pieza en `t`, posada. `actores` son los de la escena en ese instante,
201    /// en el orden de [`SceneSpec::actors`](crate::SceneSpec): `(pies en coords de
202    /// grilla, rumbo, mano)`, con la mano en el espacio local del actor
203    /// ([`CharSpec::mano`](crate::CharSpec::mano)). La pieza se apoya en la mano por
204    /// su base. `None` si en `t` no está, o si la lleva un actor que no existe.
205    pub fn en_t(&self, t: f32, actores: &[(Vec3, f32, Vec3)]) -> Option<SdfSer> {
206        let k = self.escala_en(t);
207        if k <= 0.0 {
208            return None;
209        }
210        let alto = if self.alto > 0.0 { self.alto } else { self.cosa.alto() } * k;
211        let color = self.color.unwrap_or(self.cosa.color());
212        let giro = t * self.giro * TAU;
213        let (pos, yaw) = match self.lleva {
214            None => (Vec3::from_array(self.pos), giro),
215            Some(i) => {
216                let &(pies, rumbo, mano) = actores.get(i)?;
217                (pies + crate::sdf_kit::rot_y(mano, rumbo), rumbo + giro)
218            }
219        };
220        let mut s = self.cosa.sdf(alto, color);
221        if self.cosa.luz_propia() {
222            // Un canal > 1,5 es lo que el shader lee como luz propia; ×2 lo
223            // cruza sin cambiar el tono (se ve normalizado al canal más alto).
224            for p in &mut s.prims {
225                p.color = p.color.map(|c| c * 2.0);
226            }
227        }
228        Some(s.posado([pos.x, pos.y, pos.z], yaw))
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn cada_pieza_mide_lo_que_se_pide_y_tiene_su_color() {
238        for u in Utileria::ALL {
239            let s = u.sdf(u.alto(), u.color());
240            assert!(!s.prims.is_empty(), "{u:?} vacía");
241            let (lo, hi) = crate::sdf_kit::extension_y(&s.prims);
242            assert!(lo.abs() < 1e-3, "{u:?}: la base no está en el piso ({lo})");
243            assert!((hi - lo - u.alto()).abs() < 1e-3, "{u:?}: mide {} y no {}", hi - lo, u.alto());
244            assert!(s.prims.iter().any(|p| p.color == u.color()), "{u:?}: sin su color");
245            // Una pieza es chica: no se come el presupuesto de un actor.
246            assert!(s.prims.len() <= 8, "{u:?}: {} primitivas", s.prims.len());
247        }
248    }
249
250    #[test]
251    fn la_estrella_aparece_creciendo_y_se_va_cuando_dice_el_guion() {
252        let mut e = UtileriaSpec::en(Utileria::Estrella, [10.0, 20.0, 30.0]);
253        e.desde = 2.0;
254        e.hasta = Some(5.0);
255        assert!(e.en_t(1.9, &[]).is_none(), "antes de aparecer no está");
256        let medio = e.en_t(2.0 + APARECER * 0.5, &[]).unwrap();
257        let entera = e.en_t(3.0, &[]).unwrap();
258        let alto = |s: &SdfSer| {
259            let (lo, hi) = crate::sdf_kit::extension_y(&s.prims);
260            hi - lo
261        };
262        assert!(alto(&medio) < alto(&entera) * 0.9, "aparece creciendo");
263        assert!((alto(&entera) - Utileria::Estrella.alto()).abs() < 1e-3);
264        assert_eq!(entera.pos, [10.0, 20.0, 30.0]);
265        assert!(e.en_t(5.1, &[]).is_none(), "después de irse no está");
266    }
267
268    #[test]
269    fn la_que_lleva_un_actor_va_en_su_mano_y_lo_sigue() {
270        let mut e = UtileriaSpec::en(Utileria::Flor, [0.0; 3]);
271        e.lleva = Some(1);
272        let mano = Vec3::new(0.3, 0.8, 0.2);
273        let quieto = (Vec3::new(5.0, 3.0, 5.0), 0.0, mano);
274        let a = (Vec3::new(20.0, 4.0, 40.0), 0.0, mano);
275        let b = (Vec3::new(25.0, 4.0, 42.0), PI * 0.5, mano);
276        let en_a = e.en_t(1.0, &[quieto, a]).unwrap();
277        let en_b = e.en_t(1.0, &[quieto, b]).unwrap();
278        let pa = Vec3::from_array(en_a.pos);
279        let pb = Vec3::from_array(en_b.pos);
280        assert!((pa - (a.0 + mano)).length() < 1e-5, "la flor no está en la mano");
281        // Girado un cuarto de vuelta, la mano gira con él (la convención del shader).
282        let girada = b.0 + crate::sdf_kit::rot_y(mano, PI * 0.5);
283        assert!((pb - girada).length() < 1e-5, "la mano no giró con el actor");
284        assert!((pb - pa).length() > 4.0, "no siguió al actor");
285        assert_eq!(en_b.yaw, PI * 0.5, "mira para donde mira el actor");
286        e.lleva = Some(7);
287        assert!(e.en_t(1.0, &[quieto, a]).is_none(), "un actor que no existe no la lleva");
288    }
289
290    #[test]
291    fn la_que_gira_da_las_vueltas_que_dice() {
292        let mut e = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]);
293        e.giro = 0.25;
294        let y0 = e.en_t(1.0, &[]).unwrap().yaw;
295        let y1 = e.en_t(2.0, &[]).unwrap().yaw;
296        assert!(((y1 - y0) - TAU * 0.25).abs() < 1e-4);
297    }
298}