//! **Kit de ensamble SDF** — los ladrillos comunes con los que se escriben los seres //! analíticos ([`llimphi_3d::SdfSer`]): esfera, cápsula/cono, elipsoide, decals, y las //! operaciones de conjunto (trasladar, escalar, arquear una cadena). //! //! Existe porque [`ser_sdf`](crate::ser_sdf) nació con estos constructores como //! closures privadas dentro de una función de 400 líneas, y el bestiario //! ([`bicho_sdf`](crate::bicho_sdf)) y la vegetación ([`flora_sdf`](crate::flora_sdf)) //! los necesitaban igual. Son cinco funciones triviales: lo que aportan no es //! abstracción sino **un solo lugar donde están las convenciones** — el orden de la //! lista importa (el `smin` acumula contra lo anterior), el `blend` se mide en las //! mismas unidades que el mundo, y los decals van SIEMPRE después de la geometría. //! //! Convención de espacio, igual que el ser humanoide: **base en `y = 0`**, centrado //! en X/Z, **mirando `+Z`**. El caller lo posa con //! [`SdfSer::posado`](llimphi_3d::SdfSer::posado). use llimphi_3d::glam::Vec3; use llimphi_3d::{SdfKind, SdfPrim}; /// Color desde un hexadecimal `0xRRGGBB`, componente a componente en `[0,1]`. /// /// **Sin corrección de gamma a propósito**: los colores del ser humanoide se /// eligieron a ojo mirando el render (`[0.90, 0.72, 0.58]` de piel), así que la /// paleta del bestiario se escribe en la MISMA escala. Convertir sRGB→lineal acá /// dejaría a los animales oscuros al lado de la gente. #[inline] pub fn rgb(hex: u32) -> [f32; 3] { [ ((hex >> 16) & 0xFF) as f32 / 255.0, ((hex >> 8) & 0xFF) as f32 / 255.0, (hex & 0xFF) as f32 / 255.0, ] } /// Mezcla lineal de dos colores (`t = 0` → `a`). #[inline] pub fn mezcla(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] { let t = t.clamp(0.0, 1.0); [ a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, ] } /// Aclara (`k > 1`) u oscurece (`k < 1`) un color, con tope en 1. #[inline] pub fn tono(c: [f32; 3], k: f32) -> [f32; 3] { [ (c[0] * k).clamp(0.0, 1.0), (c[1] * k).clamp(0.0, 1.0), (c[2] * k).clamp(0.0, 1.0), ] } #[inline] pub(crate) fn v3(v: Vec3) -> [f32; 3] { [v.x, v.y, v.z] } /// Esfera de radio `r` en `c`. #[inline] pub fn esfera(c: Vec3, r: f32, color: [f32; 3], blend: f32) -> SdfPrim { SdfPrim { kind: SdfKind::Esfera, a: v3(c), ra: r, b: v3(c), rb: 0.0, color, blend, } } /// Cápsula/cono redondeado del segmento `a→b`, con radio `ra` en `a` y `rb` en `b`. /// Es el ladrillo de todo lo alargado: patas, cuellos, troncos, frondas, colas. #[inline] pub fn capsula(a: Vec3, ra: f32, b: Vec3, rb: f32, color: [f32; 3], blend: f32) -> SdfPrim { SdfPrim { kind: SdfKind::Capsula, a: v3(a), ra, b: v3(b), rb, color, blend, } } /// Elipsoide centrado en `c` de **semiejes** `r` (no un segundo punto: el campo `b` /// del elipsoide son los semiejes, y trasladarlo es el error clásico — deja discos /// gigantes; ver `LECCIONES` del ser SDF). #[inline] pub fn elipsoide(c: Vec3, r: Vec3, color: [f32; 3], blend: f32) -> SdfPrim { SdfPrim { kind: SdfKind::Elipsoide, a: v3(c), ra: 0.0, b: v3(r), rb: 0.0, color, blend, } } /// Decal esférico: **pinta** color sobre la superficie ya trazada, sin geometría. /// Manchas, rayas, ojos, pupilas, flores en la copa. #[inline] pub fn decal(c: Vec3, r: f32, color: [f32; 3]) -> SdfPrim { SdfPrim { kind: SdfKind::DecalEsfera, a: v3(c), ra: r, b: v3(c), rb: 0.0, color, blend: 0.1, } } /// Decal capsular: pinta una franja a lo largo del segmento `a→b` (rayas de cebra, /// líneas de la boca, bandas de la abeja). #[inline] pub fn decal_linea(a: Vec3, b: Vec3, r: f32, color: [f32; 3]) -> SdfPrim { SdfPrim { kind: SdfKind::DecalCapsula, a: v3(a), ra: r, b: v3(b), rb: r, color, blend: 0.1, } } /// Rota `v` alrededor de X (plano Y-Z; adelante = `+Z`). #[inline] pub fn rot_x(v: Vec3, a: f32) -> Vec3 { let (s, c) = a.sin_cos(); Vec3::new(v.x, v.y * c - v.z * s, v.y * s + v.z * c) } /// Rota `v` alrededor de Y (rumbo). #[inline] pub fn rot_y(v: Vec3, a: f32) -> Vec3 { let (s, c) = a.sin_cos(); Vec3::new(v.x * c + v.z * s, v.y, -v.x * s + v.z * c) } /// Rota `v` alrededor de Z (alabeo lateral). #[inline] pub fn rot_z(v: Vec3, a: f32) -> Vec3 { let (s, c) = a.sin_cos(); Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z) } /// **Escala no uniforme** todo un ensamble ya armado, alrededor del origen local. /// Es el truco del *aplastar y estirar* del ser humanoide, generalizado: en vez de /// propagar un factor por veinte fórmulas de proporción, se deforma la lista. /// /// El elipsoide se deforma EXACTO (sus semiejes viven en `b`); los radios de esfera /// y cápsula son isótropos y se llevan la media de los tres factores — a las /// amplitudes de un rebote o un achatamiento de estilo, eso no se ve. pub fn escalar(prims: &mut [SdfPrim], k: Vec3) { let km = (k.x + k.y + k.z) / 3.0; for p in prims.iter_mut() { p.a = [p.a[0] * k.x, p.a[1] * k.y, p.a[2] * k.z]; p.b = [p.b[0] * k.x, p.b[1] * k.y, p.b[2] * k.z]; p.ra *= km; p.rb *= km; p.blend *= km; } } /// Traslada un ensamble ya armado. Ojo: el elipsoide **no** se traslada por `b` /// (son semiejes), sólo por `a`. pub fn trasladar(prims: &mut [SdfPrim], d: Vec3) { for p in prims.iter_mut() { p.a = [p.a[0] + d.x, p.a[1] + d.y, p.a[2] + d.z]; if p.kind != SdfKind::Elipsoide { p.b = [p.b[0] + d.x, p.b[1] + d.y, p.b[2] + d.z]; } } } /// **Cadena de cápsulas** por una polilínea, con radio interpolado de `r0` a `r1`. /// El ladrillo de todo lo que serpentea: la serpiente, la cola, el tronco curvo de /// una palmera, la liana. `blend` generoso = un solo cuerpo continuo. pub fn cadena( pts: &[Vec3], r0: f32, r1: f32, color: [f32; 3], blend: f32, out: &mut Vec, ) { if pts.len() < 2 { return; } let n = (pts.len() - 1) as f32; for (i, w) in pts.windows(2).enumerate() { let t0 = i as f32 / n; let t1 = (i + 1) as f32 / n; out.push(capsula( w[0], r0 + (r1 - r0) * t0, w[1], r0 + (r1 - r0) * t1, color, blend, )); } } /// **Un ojo** en `c` mirando hacia `dir`: domo blanco (geometría, para que sobreviva /// el escorzo 3/4) + pupila y chispa como decals pegadas al domo. /// /// Es el mismo diseño que la cara del ser humanoide, extraído para que el bestiario /// no invente otro ojo: dos técnicas de ojo en la misma escena se notan. /// `esclera = None` = ojo de animal sin blanco visible (la mayoría): sólo la pupila /// oscura sobre el pelaje, que es lo que se ve de verdad en un ciervo o un pájaro. pub fn ojo( c: Vec3, dir: Vec3, r: f32, esclera: Option<[f32; 3]>, pupila: [f32; 3], chispa: bool, out: &mut Vec, ) { let d = dir.normalize_or_zero(); let d = if d.length_squared() < 0.5 { Vec3::Z } else { d }; match esclera { Some(blanco) => { out.push(esfera(c, r, blanco, r * 0.25)); out.push(decal(c + d * (r * 0.99), r * 0.48, pupila)); if chispa { out.push(decal( c + d * (r * 0.99) + Vec3::new(0.0, r * 0.26, 0.0) + d * (r * 0.10), r * 0.20, [0.98, 0.985, 0.99], )); } } None => { // Globo oscuro como GEOMETRÍA: en un hocico peludo un decal plano se // pierde con la luz rasante; la esferita capta su propio brillo. out.push(esfera(c, r, pupila, r * 0.3)); if chispa { out.push(decal( c + d * (r * 0.92) + Vec3::new(0.0, r * 0.30, 0.0), r * 0.30, [0.97, 0.98, 0.99], )); } } } } /// **Extensión vertical** de un ensamble (mínimo y máximo de `y`), contando sólo la /// GEOMETRÍA: los decals pintan sobre lo ya trazado y no agrandan a nadie — si /// contaran, una mancha del patrón correría la altura del animal. pub fn extension_y(prims: &[SdfPrim]) -> (f32, f32) { let mut lo = f32::INFINITY; let mut hi = f32::NEG_INFINITY; for p in prims { let (a, b) = match p.kind { SdfKind::Elipsoide => (p.a[1] - p.b[1], p.a[1] + p.b[1]), SdfKind::Esfera => (p.a[1] - p.ra, p.a[1] + p.ra), SdfKind::Capsula => ( (p.a[1] - p.ra).min(p.b[1] - p.rb), (p.a[1] + p.ra).max(p.b[1] + p.rb), ), _ => continue, }; lo = lo.min(a); hi = hi.max(b); } if lo > hi { (0.0, 0.0) } else { (lo, hi) } } /// **Asienta y escala**: baja el ensamble hasta apoyar en `y = 0` y lo escala /// uniforme para que mida `alto`. /// /// Es lo que deja escribir cada receta del catálogo en las unidades que le quedan /// cómodas (el cuadrúpedo mide su mundo en alturas de lomo, el ave en alturas /// totales) sin que nadie tenga que hacer la cuenta: la jirafa y la hormiga salen de /// la misma tabla. Devuelve el factor aplicado. pub fn asentar(prims: &mut Vec, alto: f32) -> f32 { let (lo, hi) = extension_y(prims); let h = hi - lo; if h <= 1e-6 { return 1.0; } trasladar(prims, Vec3::new(0.0, -lo, 0.0)); let k = alto / h; escalar(prims, Vec3::splat(k)); k } /// Ruido hash determinista `[0,1)` a partir de dos enteros — la variedad por /// semilla del catálogo (qué tan torcido sale este árbol, de qué color esta remera) /// sin arrastrar un generador aleatorio por toda la API. #[inline] pub fn hash01(a: u32, b: u32) -> f32 { let mut h = a.wrapping_mul(0x9E37_79B9) ^ b.wrapping_mul(0x85EB_CA77); h ^= h >> 15; h = h.wrapping_mul(0x2C1B_3C6D); h ^= h >> 13; (h & 0x00FF_FFFF) as f32 / 16_777_216.0 } /// Igual que [`hash01`] pero centrado en `[-1, 1)`. #[inline] pub fn hash11(a: u32, b: u32) -> f32 { hash01(a, b) * 2.0 - 1.0 } #[cfg(test)] mod tests { use super::*; #[test] fn rgb_mapea_el_hexadecimal() { assert_eq!(rgb(0xFFFFFF), [1.0, 1.0, 1.0]); assert_eq!(rgb(0x000000), [0.0, 0.0, 0.0]); let c = rgb(0x804020); assert!((c[0] - 0.502).abs() < 0.01 && (c[1] - 0.251).abs() < 0.01); } /// El elipsoide se traslada por `a` y NUNCA por `b` (sus semiejes): el error que /// dejaba discos gigantes cuando nació el ser SDF. #[test] fn trasladar_no_toca_los_semiejes_del_elipsoide() { let mut p = vec![elipsoide(Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0), [1.0; 3], 0.1)]; trasladar(&mut p, Vec3::new(10.0, 0.0, 0.0)); assert_eq!(p[0].a, [10.0, 0.0, 0.0]); assert_eq!(p[0].b, [1.0, 2.0, 3.0], "los semiejes no se mueven"); } #[test] fn escalar_deforma_el_elipsoide_por_eje() { let mut p = vec![elipsoide(Vec3::new(0.0, 1.0, 0.0), Vec3::ONE, [1.0; 3], 0.1)]; escalar(&mut p, Vec3::new(2.0, 0.5, 1.0)); assert_eq!(p[0].a, [0.0, 0.5, 0.0]); assert_eq!(p[0].b, [2.0, 0.5, 1.0]); } #[test] fn cadena_hila_los_tramos_con_radio_interpolado() { let pts = [Vec3::ZERO, Vec3::Y, Vec3::new(0.0, 2.0, 0.0)]; let mut out = Vec::new(); cadena(&pts, 1.0, 0.0, [1.0; 3], 0.1, &mut out); assert_eq!(out.len(), 2); assert_eq!(out[0].ra, 1.0); assert!((out[0].rb - 0.5).abs() < 1e-6, "mitad del recorrido = mitad del radio"); assert_eq!(out[1].rb, 0.0); } #[test] fn asentar_apoya_en_cero_y_da_la_altura_pedida() { let mut p = vec![ esfera(Vec3::new(0.0, 5.0, 0.0), 1.0, [1.0; 3], 0.1), capsula(Vec3::new(0.0, 2.0, 0.0), 0.5, Vec3::new(0.0, 4.0, 0.0), 0.5, [1.0; 3], 0.1), ]; let k = asentar(&mut p, 3.0); let (lo, hi) = extension_y(&p); assert!(lo.abs() < 1e-5, "apoya en 0, no en {lo}"); assert!((hi - 3.0).abs() < 1e-5, "mide 3, no {hi}"); assert!((k - 3.0 / 4.5).abs() < 1e-5, "factor {k}"); } /// Un decal no puede agrandar al ser: pinta sobre lo que ya está trazado. #[test] fn los_decals_no_cuentan_para_la_altura() { let base = vec![esfera(Vec3::new(0.0, 1.0, 0.0), 1.0, [1.0; 3], 0.1)]; let mut con = base.clone(); con.push(decal(Vec3::new(0.0, 9.0, 0.0), 0.5, [1.0; 3])); assert_eq!(extension_y(&base), extension_y(&con)); } #[test] fn el_hash_es_determinista_y_reparte() { assert_eq!(hash01(3, 7), hash01(3, 7)); assert_ne!(hash01(3, 7), hash01(3, 8)); let n = 400; let media: f32 = (0..n).map(|i| hash01(i, 1)).sum::() / n as f32; assert!((media - 0.5).abs() < 0.06, "media {media} lejos de 0.5"); } }