//! **La multitud** — gente diversa por semilla: la vereda, la plaza, la tribuna. //! //! El ser SDF ya sabía dibujar UNA persona muy bien. El problema de una multitud es //! otro: **veinte veces la misma persona con la remera de otro color no es una //! multitud**, es un patrón, y el ojo lo caza al instante. Acá vive lo que hace que //! veinte figuras se lean como veinte personas: //! //! 1. **Constitución**: no alcanza con la edad. Dos adultos difieren en canon de //! cabezas, redondez, angulosidad, panza y grosor de miembros — el mismo juego de //! perillas de [`EstiloSer`], sacudido por semilla. //! 2. **Piel, pelo y ropa de tablas reales**, no de un `rand` sobre el cubo RGB: diez //! tonos de piel que existen, nueve colores de pelo (con canas y teñidos), y una //! paleta de ropa que combina. //! 3. **Peinado y atuendo** ([`Atuendo`]): la silueta de la cabeza es lo primero que //! distingue a alguien a cincuenta metros, mucho antes que la cara. //! 4. **Fase propia**: si todos caminan en el mismo cuadro del ciclo, la multitud //! marcha. Cada uno arranca donde le toca y camina a su velocidad. //! //! Nada de esto guarda estado: [`persona`] es una función pura de la semilla, así //! que el mismo número da la misma persona en el preview, en el export y en la otra //! máquina. use llimphi_3d::glam::Vec3; use llimphi_3d::SdfSer; use crate::actor::{Age, Build, Clip}; use crate::humano::{Colores, EstiloSer}; use crate::sdf_kit::{hash01, hash11, rgb}; use crate::ser_sdf::{ser_sdf_atuendo, Atuendo, DetalleSer, Peinado, Sombrero}; /// Tonos de piel: el espectro humano, de más claro a más oscuro. Escritos a mano /// (no interpolados) porque la piel no viaja por una recta del cubo RGB — el medio /// tiene otro matiz que las puntas. const PIELES: [u32; 10] = [ 0xF2D9C2, 0xEBCBAA, 0xE0B892, 0xD2A277, 0xC08A5E, 0xA9744B, 0x8E5C39, 0x734830, 0x573526, 0x3E241B, ]; /// Colores de pelo: negros, castaños, rubios, pelirrojos, canas y dos teñidos. const PELOS: [u32; 11] = [ 0x1A1618, 0x2E2119, 0x4A3222, 0x6A4526, 0x8A6034, 0xB08040, 0xD8B870, 0x9A3A20, 0x8A8A8E, 0xE0DED8, 0x6A3AC0, ]; /// Paleta de ropa: colores que se ven en una calle, ni pasteles de catálogo ni /// saturados de juguete. const REMERAS: [u32; 18] = [ 0xC8483C, 0xE0783A, 0xE8B830, 0x6A9A3A, 0x2E8A5E, 0x2A7A8A, 0x2E5EA0, 0x4A4AA0, 0x8A4A9A, 0xD06A90, 0xF2EFE8, 0xC8C2B4, 0x8A8A90, 0x3A3A42, 0x7A5A3A, 0x2A6A6A, 0xE8DCC0, 0x9A2A3A, ]; /// Pantalones/faldas: menos variedad y más oscuros, como en la calle de verdad. const PANTALONES: [u32; 10] = [ 0x2E3A55, 0x1E2430, 0x3A3A40, 0x5A5A62, 0x6A5A46, 0x8A7A62, 0x2E4A3A, 0x4A2A2A, 0xC8C0B0, 0x223244, ]; /// Una persona: la constitución, la paleta y lo que lleva puesto. Es todo lo que /// hace falta para dibujarla en cualquier pose. #[derive(Debug, Clone, Copy)] pub struct Persona { pub edad: Age, pub estilo: EstiloSer, pub colores: Colores, pub atuendo: Atuendo, /// Alto en unidades de mundo (metros, si el mundo está en metros). pub altura: f32, } impl Persona { /// La persona armada como ser SDF, en la pose del `clip`/`fase`, sin posar /// (local, pies en el origen). pub fn ser(&self, clip: Clip, fase: f32, detalle: DetalleSer) -> SdfSer { let gesto = crate::Gesto::vivo(fase, clip); ser_sdf_atuendo( &self.estilo, &clip.pose(fase), self.altura, self.colores, &gesto, detalle, &self.atuendo, ) } /// La persona posada en el mundo, mirando a `look` si se le pasa un punto. pub fn posada( &self, clip: Clip, fase: f32, pos: Vec3, yaw: f32, look: Option, detalle: DetalleSer, ) -> SdfSer { let mut gesto = crate::Gesto::vivo(fase, clip); if let Some(t) = look { let ojos = crate::altura_ojos(&self.estilo, self.altura); gesto = gesto.mirar(t, pos, yaw, ojos); } ser_sdf_atuendo( &self.estilo, &clip.pose(fase), self.altura, self.colores, &gesto, detalle, &self.atuendo, ) .posado([pos.x, pos.y, pos.z], yaw) } } /// **Una persona a partir de una semilla.** Función pura: la misma semilla da /// siempre la misma persona, en el preview y en el export. pub fn persona(sem: u32) -> Persona { // Edad: la pirámide de una calle cualquiera — mayoría adulta, algunos chicos y // jóvenes, unos pocos mayores. Sin bebés: un bebé no camina solo por la vereda. let e = hash01(sem, 1); let edad = if e < 0.14 { Age::Child } else if e < 0.30 { Age::Teen } else if e < 0.86 { Age::Adult } else { Age::Elder }; // Constitución: la edad pone la base y la semilla la sacude. Los rangos son // angostos a propósito — un canon de cabezas ±0.45 ya cambia la silueta entera. let mut estilo = EstiloSer::por_edad(edad); estilo.cabezas = (estilo.cabezas + hash11(sem, 2) * 0.45).clamp(2.6, 5.2); estilo.redondez = (0.55 + hash01(sem, 3) * 0.45).clamp(0.0, 1.0); estilo.angulosidad = (hash01(sem, 4) * 0.55).clamp(0.0, 1.0); estilo.panza = (estilo.panza + hash11(sem, 5) * 0.16).clamp(0.85, 1.35); estilo.extremidad = (estilo.extremidad * (1.0 + hash11(sem, 6) * 0.22)).clamp(0.045, 0.11); estilo.mejilla = (estilo.mejilla + hash11(sem, 7) * 0.08).clamp(0.92, 1.24); estilo.ojo_tam = (estilo.ojo_tam + hash11(sem, 8) * 0.03).clamp(0.15, 0.34); // Altura: la del canon de la edad, ±9 %. let altura = Build::for_age(edad).height * (1.0 + hash11(sem, 9) * 0.09); // Piel, pelo y ropa. let piel = rgb(PIELES[(hash01(sem, 10) * PIELES.len() as f32) as usize % PIELES.len()]); let mut i_pelo = (hash01(sem, 11) * PELOS.len() as f32) as usize % PELOS.len(); if edad == Age::Elder && hash01(sem, 12) < 0.72 { // Las canas no son un color más: casi todo mayor las tiene. i_pelo = if hash01(sem, 13) < 0.5 { 8 } else { 9 }; } else if i_pelo == 10 && hash01(sem, 14) > 0.12 { // El teñido existe, pero uno de cada ocho, no uno de cada once. i_pelo = 1; } let pelo = rgb(PELOS[i_pelo]); let remera = rgb(REMERAS[(hash01(sem, 15) * REMERAS.len() as f32) as usize % REMERAS.len()]); let pantalon = rgb(PANTALONES[(hash01(sem, 16) * PANTALONES.len() as f32) as usize % PANTALONES.len()]); // Peinado: correlacionado con la edad (la calvicie y el rodete no se reparten // parejo por la pirámide). let h = hash01(sem, 17); let peinado = match edad { Age::Child | Age::Baby => match h { x if x < 0.30 => Peinado::Casquete, x if x < 0.55 => Peinado::Corto, x if x < 0.75 => Peinado::DosColas, x if x < 0.90 => Peinado::Melena, _ => Peinado::Afro, }, Age::Elder => match h { x if x < 0.30 => Peinado::Calvo, x if x < 0.60 => Peinado::Corto, x if x < 0.78 => Peinado::Rodete, x if x < 0.92 => Peinado::Melena, _ => Peinado::Larga, }, _ => match h { x if x < 0.18 => Peinado::Casquete, x if x < 0.36 => Peinado::Corto, x if x < 0.50 => Peinado::Melena, x if x < 0.64 => Peinado::Larga, x if x < 0.74 => Peinado::Coleta, x if x < 0.83 => Peinado::Rodete, x if x < 0.89 => Peinado::Afro, x if x < 0.94 => Peinado::DosColas, x if x < 0.97 => Peinado::Cresta, _ => Peinado::Calvo, }, }; let s = hash01(sem, 18); let sombrero = match s { x if x < 0.08 => Sombrero::Gorra, x if x < 0.13 => Sombrero::Ala, x if x < 0.17 => Sombrero::Gorro, x if x < 0.21 => Sombrero::Panuelo, x if x < 0.23 => Sombrero::Casco, _ => Sombrero::Ninguno, }; let barba = if matches!(edad, Age::Adult | Age::Elder) && hash01(sem, 19) < 0.34 { 0.35 + hash01(sem, 20) * 0.65 } else { 0.0 }; let falda = if hash01(sem, 21) < 0.30 { 0.35 + hash01(sem, 22) * 0.65 } else { 0.0 }; let color_falda = if falda > 0.0 && hash01(sem, 23) < 0.55 { Some(pantalon) } else { None }; let color_sombrero = if hash01(sem, 24) < 0.5 { Some(rgb(PANTALONES[(hash01(sem, 25) * PANTALONES.len() as f32) as usize % PANTALONES.len()])) } else { None }; Persona { edad, estilo, colores: Colores::con_pelo(piel, remera, pantalon, pelo), atuendo: Atuendo { peinado, barba, sombrero, color_sombrero, anteojos: hash01(sem, 26) < 0.22, falda, color_falda, manto: 0.0, color_manto: None, }, altura, } } /// **Un grupo de `n` personas distintas** a partir de una semilla. pub fn multitud(sem: u32, n: usize) -> Vec { (0..n as u32).map(|i| persona(sem.wrapping_mul(0x9E37_79B9).wrapping_add(i * 7919))).collect() } /// Una persona **en la calle**: quién es, dónde está, hacia dónde va y en qué punto /// de su paso. #[derive(Debug, Clone, Copy)] pub struct Caminante { pub persona: Persona, /// Posición en coordenadas de mundo (las que consume el renderer). pub pos: Vec3, /// Rumbo en radianes (`0` = mirando `+Z`). pub yaw: f32, pub clip: Clip, /// Fase del ciclo de caminata. pub fase: f32, } /// **Una vereda con gente caminando**, determinista por semilla. /// /// `ancho` × `largo` es la franja (en X y Z) por la que se reparten; `t` es el /// tiempo, y cada uno avanza a SU velocidad y vuelve a entrar por el otro extremo /// (la calle no se vacía). La mitad va en cada sentido — una multitud en la que /// todos caminan para el mismo lado es un desfile. /// /// `origen` es la esquina de la franja: el llamador la pone donde tenga el piso. pub fn multitud_caminando( sem: u32, n: usize, origen: Vec3, ancho: f32, largo: f32, t: f32, ) -> Vec { let mut out = Vec::with_capacity(n); for i in 0..n as u32 { let s = sem.wrapping_mul(0x9E37_79B9).wrapping_add(i * 7919); let persona = persona(s); let sentido = if i % 2 == 0 { 1.0 } else { -1.0f32 }; // Velocidad: proporcional a la altura (las piernas largas caminan más // rápido) y con su propio desvío. Un chico trota para seguir el ritmo. let vel = persona.altura * (0.55 + 0.25 * hash01(s, 40)); let carril = (i as f32 + 0.5) / n as f32; let x = origen.x + (carril + hash11(s, 41) * 0.03).clamp(0.0, 1.0) * ancho; let avance = (hash01(s, 42) * largo + t * vel * sentido).rem_euclid(largo); let z = origen.z + avance; let clip = if hash01(s, 43) < 0.08 { Clip::Idle } else if hash01(s, 44) < 0.06 { Clip::Run } else { Clip::Walk }; // Fase propia: sin esto la multitud marcha en formación. let fase = hash01(s, 45) * 6.283 + t * vel * 2.2; out.push(Caminante { persona, pos: Vec3::new(x, origen.y, z), yaw: if sentido > 0.0 { 0.0 } else { std::f32::consts::PI }, clip, fase, }); } out } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; #[test] fn la_misma_semilla_da_la_misma_persona() { let a = persona(1234); let b = persona(1234); assert_eq!(a.altura, b.altura); assert_eq!(a.colores.skin, b.colores.skin); assert_eq!(a.atuendo.peinado, b.atuendo.peinado); } /// Lo que esta caja tiene que garantizar: que una multitud NO sea la misma /// persona repetida. Se mide sobre lo que el ojo agarra primero — silueta de la /// cabeza, altura y color. #[test] fn una_multitud_de_treinta_no_se_repite() { let g = multitud(7, 30); let peinados: HashSet<_> = g.iter().map(|p| p.atuendo.peinado).collect(); assert!(peinados.len() >= 6, "sólo {} peinados en 30 personas", peinados.len()); let pieles: HashSet<_> = g.iter().map(|p| format!("{:?}", p.colores.skin)).collect(); assert!(pieles.len() >= 6, "sólo {} tonos de piel en 30", pieles.len()); let remeras: HashSet<_> = g.iter().map(|p| format!("{:?}", p.colores.shirt)).collect(); assert!(remeras.len() >= 10, "sólo {} remeras en 30", remeras.len()); // Alturas al centímetro: se repiten algunas (la edad manda el grueso y el // desvío es ±9 %), pero dos tercios distintos ya es una multitud pareja. let alturas: HashSet<_> = g.iter().map(|p| (p.altura * 100.0) as i32).collect(); assert!(alturas.len() >= 20, "sólo {} alturas distintas en 30", alturas.len()); // Y la constitución, que es lo que hace que no sean el mismo muñeco. let cabezas: HashSet<_> = g.iter().map(|p| (p.estilo.cabezas * 20.0) as i32).collect(); assert!(cabezas.len() >= 15, "sólo {} cánones en 30", cabezas.len()); } /// Las edades tienen que aparecer todas (una calle de puros adultos es un /// error de reparto, no una calle). #[test] fn la_calle_tiene_chicos_y_mayores() { let g = multitud(3, 60); let edades: HashSet<_> = g.iter().map(|p| format!("{:?}", p.edad)).collect(); assert!(edades.len() >= 3, "sólo {edades:?}"); assert!(g.iter().any(|p| p.edad == Age::Child), "no hay chicos"); assert!(g.iter().any(|p| p.edad == Age::Elder), "no hay mayores"); } /// Si todos comparten fase, la multitud MARCHA. Es el defecto clásico y no se /// ve en un cuadro fijo: hay que medirlo. #[test] fn nadie_camina_en_la_misma_fase() { let c = multitud_caminando(11, 20, Vec3::ZERO, 8.0, 30.0, 0.0); let fases: HashSet<_> = c.iter().map(|k| (k.fase * 50.0) as i32).collect(); assert!(fases.len() >= 18, "sólo {} fases distintas en 20", fases.len()); // Y la mitad va para cada lado. let ida = c.iter().filter(|k| k.yaw.abs() < 0.1).count(); assert!((ida as i32 - 10).abs() <= 2, "{ida} de 20 en un sentido"); } /// La calle no se vacía: al avanzar el tiempo, todos siguen dentro de la franja. #[test] fn la_calle_no_se_vacia_al_avanzar_el_tiempo() { for t in [0.0f32, 3.0, 17.0, 120.0] { let c = multitud_caminando(5, 24, Vec3::new(-4.0, 0.0, 0.0), 8.0, 30.0, t); for k in &c { assert!(k.pos.z >= -0.01 && k.pos.z <= 30.01, "t={t} se fue a z={}", k.pos.z); assert!(k.pos.x >= -4.01 && k.pos.x <= 4.01, "t={t} se fue a x={}", k.pos.x); } } } #[test] fn una_persona_se_arma_como_ser_sdf() { let p = persona(99); let s = p.ser(Clip::Walk, 1.0, DetalleSer::Hero); assert!(s.prims.len() > 20, "sólo {} primitivas", s.prims.len()); let (_, r) = s.bound(); assert!(r > 0.0 && r < p.altura * 3.0, "esfera envolvente rara: {r}"); } }