tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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}