//! **Utilería**: los objetos con nombre que un guion necesita mostrar y que no son //! ni gente, ni fauna, ni mundo — la estrella que la princesa ve aparecer, la flor, //! la perla y la pluma del prendedor. //! //! Se arman con primitivas SDF, igual que los seres, así que entran al MISMO pase //! del ray-marcher con la misma respuesta de luz y **gastan del mismo presupuesto** //! del uniform: por eso el reparto les reserva su lugar antes de repartir el resto //! entre los actores (ver [`reparto_mixto_con`](crate::reparto_mixto_con)). //! //! Una escena los coloca con [`UtileriaSpec`]: en un punto del mundo o **en la mano** //! de uno de sus actores, apareciendo y desapareciendo en instantes del guion. //! //! Espacio LOCAL: base en `y = 0`, centrado en X/Z, la cara mirando `+Z`. Se arman //! en unidades cómodas y se asientan al alto pedido //! ([`asentar`](crate::sdf_kit::asentar)). use std::f32::consts::{PI, TAU}; use llimphi_3d::glam::Vec3; use llimphi_3d::{SdfPrim, SdfSer}; use serde::{Deserialize, Serialize}; use crate::sdf_kit::{asentar, capsula, elipsoide, esfera, tono}; /// Catálogo de utilería. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Utileria { /// Estrella de cinco puntas, de cara a la cámara. Estrella, /// Flor de cinco pétalos sobre su tallo. Flor, /// Perla: una esfera nacarada. Perla, /// Pluma: el cañón y el vexilo curvado. Pluma, /// Prendedor: «con un verso y una perla, y una pluma y una flor» — las tres /// juntas sobre una base de oro. Va al final: postcard numera las variantes. Prendedor, } impl Utileria { /// Todas (para ciclar en un editor). pub const ALL: [Utileria; 5] = [Utileria::Estrella, Utileria::Flor, Utileria::Perla, Utileria::Pluma, Utileria::Prendedor]; /// El color de fábrica de la pieza principal. pub fn color(self) -> [f32; 3] { match self { Utileria::Estrella => [1.0, 0.93, 0.62], Utileria::Flor => [0.96, 0.52, 0.66], Utileria::Perla => [0.95, 0.93, 0.90], Utileria::Pluma => [0.97, 0.97, 0.95], Utileria::Prendedor => [0.95, 0.74, 0.22], } } /// El alto de fábrica, en metros (= voxels). pub fn alto(self) -> f32 { match self { Utileria::Estrella => 0.6, Utileria::Flor => 0.45, Utileria::Perla => 0.08, Utileria::Pluma => 0.35, Utileria::Prendedor => 0.14, } } /// Si brilla con **luz propia**: se ve con su color aunque sea de noche o esté /// en sombra, y en una escena de noche además alumbra lo que tiene cerca /// ([`SceneSpec::luces_en`](crate::SceneSpec::luces_en)). Sólo la estrella. pub fn luz_propia(self) -> bool { matches!(self, Utileria::Estrella) } /// La pieza armada, asentada al `alto` dado y pintada de `color`. pub fn sdf(self, alto: f32, color: [f32; 3]) -> SdfSer { let mut prims = match self { Utileria::Estrella => estrella(color), Utileria::Flor => flor(color), Utileria::Perla => vec![esfera(Vec3::new(0.0, 0.5, 0.0), 0.5, color, 0.0)], Utileria::Pluma => pluma(color), Utileria::Prendedor => prendedor(color), }; asentar(&mut prims, alto.max(0.01)); SdfSer { prims, pos: [0.0; 3], yaw: 0.0 } } } /// Cinco brazos que salen del centro y afinan hacia la punta, en el plano XY: de /// cara a `+Z`, que es donde mira la cámara cuando nadie la gira. fn estrella(color: [f32; 3]) -> Vec { let c = Vec3::new(0.0, 1.0, 0.0); let mut v = vec![esfera(c, 0.30, color, 0.10)]; for i in 0..5 { let a = PI * 0.5 + i as f32 * TAU / 5.0; let punta = c + Vec3::new(a.cos(), a.sin(), 0.0) * 1.0; v.push(capsula(c, 0.24, punta, 0.03, color, 0.10)); } v } /// Tallo verde, cinco pétalos alrededor de un centro amarillo. fn flor(color: [f32; 3]) -> Vec { let verde = [0.30, 0.55, 0.25]; let c = Vec3::new(0.0, 1.0, 0.04); let mut v = vec![ capsula(Vec3::ZERO, 0.03, c - Vec3::new(0.0, 0.05, 0.04), 0.025, verde, 0.0), esfera(c + Vec3::new(0.0, 0.0, 0.03), 0.09, [0.98, 0.82, 0.25], 0.02), ]; for i in 0..5 { let a = PI * 0.5 + i as f32 * TAU / 5.0; let p = c + Vec3::new(a.cos(), a.sin(), 0.0) * 0.17; v.push(elipsoide(p, Vec3::new(0.12, 0.12, 0.04), color, 0.04)); } v } /// El cañón fino y el vexilo como una lágrima alargada, un poco inclinados. fn pluma(color: [f32; 3]) -> Vec { let base = Vec3::ZERO; let punta = Vec3::new(0.18, 1.0, 0.0); vec![ capsula(base, 0.015, punta, 0.006, tono(color, 0.8), 0.0), elipsoide(Vec3::new(0.12, 0.62, 0.0), Vec3::new(0.10, 0.36, 0.02), color, 0.03), ] } /// El prendedor de frente: una base ovalada de `color` (oro), y encima la flor (un /// centro y cuatro pétalos), la perla y la pluma, cada una con su color de fábrica. /// Ocho primitivas: lo mismo que una flor suelta. fn prendedor(color: [f32; 3]) -> Vec { let c = Vec3::new(0.0, 0.5, 0.0); let flor_c = c + Vec3::new(-0.16, 0.08, 0.10); let mut v = vec![elipsoide(c, Vec3::new(0.50, 0.42, 0.08), color, 0.02)]; for i in 0..4 { let a = PI * 0.25 + i as f32 * TAU / 4.0; let p = flor_c + Vec3::new(a.cos(), a.sin(), 0.0) * 0.11; v.push(elipsoide(p, Vec3::new(0.09, 0.09, 0.03), Utileria::Flor.color(), 0.03)); } v.push(esfera(flor_c + Vec3::new(0.0, 0.0, 0.03), 0.06, [0.98, 0.82, 0.25], 0.01)); v.push(esfera(c + Vec3::new(0.20, -0.12, 0.10), 0.09, Utileria::Perla.color(), 0.0)); v.push(elipsoide( c + Vec3::new(0.18, 0.18, 0.09), Vec3::new(0.06, 0.24, 0.02), Utileria::Pluma.color(), 0.02, )); v } /// Lo que tarda una pieza en aparecer (crece de nada a su tamaño), en segundos. pub const APARECER: f32 = 0.4; /// **Una pieza de utilería puesta en una escena**: cuál, dónde, de qué tamaño y /// cuándo está. Si `lleva` nombra a un actor (índice en /// [`SceneSpec::actors`](crate::SceneSpec)), la pieza va en su mano y lo sigue; si /// no, queda en `pos` (coords de grilla, como los sucesos). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct UtileriaSpec { pub cosa: Utileria, /// Dónde está, en coords de grilla `[x, y, z]` (las del mundo y los sucesos). #[serde(default)] pub pos: [f32; 3], /// Alto en metros; `0` = el de fábrica de la pieza. #[serde(default)] pub alto: f32, /// Desde qué instante de la escena se ve (aparece creciendo). #[serde(default)] pub desde: f32, /// Hasta qué instante; `None` = hasta el final. #[serde(default)] pub hasta: Option, /// Giro sobre su eje vertical, en vueltas por segundo (`0` = quieta). #[serde(default)] pub giro: f32, /// El actor que la lleva en la mano (índice en los actores de la escena). #[serde(default)] pub lleva: Option, /// Color propio; `None` = el de fábrica. #[serde(default)] pub color: Option<[f32; 3]>, } impl UtileriaSpec { /// Una pieza de fábrica parada en `pos`, visible toda la escena. pub fn en(cosa: Utileria, pos: [f32; 3]) -> Self { Self { cosa, pos, alto: 0.0, desde: 0.0, hasta: None, giro: 0.0, lleva: None, color: None } } /// El tamaño relativo en `t`: `0` antes de `desde` y después de `hasta`, crece /// en [`APARECER`] segundos al entrar. pub fn escala_en(&self, t: f32) -> f32 { if t < self.desde || self.hasta.is_some_and(|h| t > h) { return 0.0; } let x = ((t - self.desde) / APARECER).clamp(0.0, 1.0); x * x * (3.0 - 2.0 * x) } /// La pieza en `t`, posada. `actores` son los de la escena en ese instante, /// en el orden de [`SceneSpec::actors`](crate::SceneSpec): `(pies en coords de /// grilla, rumbo, mano)`, con la mano en el espacio local del actor /// ([`CharSpec::mano`](crate::CharSpec::mano)). La pieza se apoya en la mano por /// su base. `None` si en `t` no está, o si la lleva un actor que no existe. pub fn en_t(&self, t: f32, actores: &[(Vec3, f32, Vec3)]) -> Option { let k = self.escala_en(t); if k <= 0.0 { return None; } let alto = if self.alto > 0.0 { self.alto } else { self.cosa.alto() } * k; let color = self.color.unwrap_or(self.cosa.color()); let giro = t * self.giro * TAU; let (pos, yaw) = match self.lleva { None => (Vec3::from_array(self.pos), giro), Some(i) => { let &(pies, rumbo, mano) = actores.get(i)?; (pies + crate::sdf_kit::rot_y(mano, rumbo), rumbo + giro) } }; let mut s = self.cosa.sdf(alto, color); if self.cosa.luz_propia() { // Un canal > 1,5 es lo que el shader lee como luz propia; ×2 lo // cruza sin cambiar el tono (se ve normalizado al canal más alto). for p in &mut s.prims { p.color = p.color.map(|c| c * 2.0); } } Some(s.posado([pos.x, pos.y, pos.z], yaw)) } } #[cfg(test)] mod tests { use super::*; #[test] fn cada_pieza_mide_lo_que_se_pide_y_tiene_su_color() { for u in Utileria::ALL { let s = u.sdf(u.alto(), u.color()); assert!(!s.prims.is_empty(), "{u:?} vacía"); let (lo, hi) = crate::sdf_kit::extension_y(&s.prims); assert!(lo.abs() < 1e-3, "{u:?}: la base no está en el piso ({lo})"); assert!((hi - lo - u.alto()).abs() < 1e-3, "{u:?}: mide {} y no {}", hi - lo, u.alto()); assert!(s.prims.iter().any(|p| p.color == u.color()), "{u:?}: sin su color"); // Una pieza es chica: no se come el presupuesto de un actor. assert!(s.prims.len() <= 8, "{u:?}: {} primitivas", s.prims.len()); } } #[test] fn la_estrella_aparece_creciendo_y_se_va_cuando_dice_el_guion() { let mut e = UtileriaSpec::en(Utileria::Estrella, [10.0, 20.0, 30.0]); e.desde = 2.0; e.hasta = Some(5.0); assert!(e.en_t(1.9, &[]).is_none(), "antes de aparecer no está"); let medio = e.en_t(2.0 + APARECER * 0.5, &[]).unwrap(); let entera = e.en_t(3.0, &[]).unwrap(); let alto = |s: &SdfSer| { let (lo, hi) = crate::sdf_kit::extension_y(&s.prims); hi - lo }; assert!(alto(&medio) < alto(&entera) * 0.9, "aparece creciendo"); assert!((alto(&entera) - Utileria::Estrella.alto()).abs() < 1e-3); assert_eq!(entera.pos, [10.0, 20.0, 30.0]); assert!(e.en_t(5.1, &[]).is_none(), "después de irse no está"); } #[test] fn la_que_lleva_un_actor_va_en_su_mano_y_lo_sigue() { let mut e = UtileriaSpec::en(Utileria::Flor, [0.0; 3]); e.lleva = Some(1); let mano = Vec3::new(0.3, 0.8, 0.2); let quieto = (Vec3::new(5.0, 3.0, 5.0), 0.0, mano); let a = (Vec3::new(20.0, 4.0, 40.0), 0.0, mano); let b = (Vec3::new(25.0, 4.0, 42.0), PI * 0.5, mano); let en_a = e.en_t(1.0, &[quieto, a]).unwrap(); let en_b = e.en_t(1.0, &[quieto, b]).unwrap(); let pa = Vec3::from_array(en_a.pos); let pb = Vec3::from_array(en_b.pos); assert!((pa - (a.0 + mano)).length() < 1e-5, "la flor no está en la mano"); // Girado un cuarto de vuelta, la mano gira con él (la convención del shader). let girada = b.0 + crate::sdf_kit::rot_y(mano, PI * 0.5); assert!((pb - girada).length() < 1e-5, "la mano no giró con el actor"); assert!((pb - pa).length() > 4.0, "no siguió al actor"); assert_eq!(en_b.yaw, PI * 0.5, "mira para donde mira el actor"); e.lleva = Some(7); assert!(e.en_t(1.0, &[quieto, a]).is_none(), "un actor que no existe no la lleva"); } #[test] fn la_que_gira_da_las_vueltas_que_dice() { let mut e = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]); e.giro = 0.25; let y0 = e.en_t(1.0, &[]).unwrap().yaw; let y1 = e.en_t(2.0, &[]).unwrap().yaw; assert!(((y1 - y0) - TAU * 0.25).abs() < 1e-4); } }