tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-voxel/src/multitud.rs Rust · 385 lines · 15.0 KB History Who touched what raw

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

1//! **La multitud** — gente diversa por semilla: la vereda, la plaza, la tribuna.
2//!
3//! El ser SDF ya sabía dibujar UNA persona muy bien. El problema de una multitud es
4//! otro: **veinte veces la misma persona con la remera de otro color no es una
5//! multitud**, es un patrón, y el ojo lo caza al instante. Acá vive lo que hace que
6//! veinte figuras se lean como veinte personas:
7//!
8//! 1. **Constitución**: no alcanza con la edad. Dos adultos difieren en canon de
9//!    cabezas, redondez, angulosidad, panza y grosor de miembros — el mismo juego de
10//!    perillas de [`EstiloSer`], sacudido por semilla.
11//! 2. **Piel, pelo y ropa de tablas reales**, no de un `rand` sobre el cubo RGB: diez
12//!    tonos de piel que existen, nueve colores de pelo (con canas y teñidos), y una
13//!    paleta de ropa que combina.
14//! 3. **Peinado y atuendo** ([`Atuendo`]): la silueta de la cabeza es lo primero que
15//!    distingue a alguien a cincuenta metros, mucho antes que la cara.
16//! 4. **Fase propia**: si todos caminan en el mismo cuadro del ciclo, la multitud
17//!    marcha. Cada uno arranca donde le toca y camina a su velocidad.
18//!
19//! Nada de esto guarda estado: [`persona`] es una función pura de la semilla, así
20//! que el mismo número da la misma persona en el preview, en el export y en la otra
21//! máquina.
22
23use llimphi_3d::glam::Vec3;
24use llimphi_3d::SdfSer;
25
26use crate::actor::{Age, Build, Clip};
27use crate::humano::{Colores, EstiloSer};
28use crate::sdf_kit::{hash01, hash11, rgb};
29use crate::ser_sdf::{ser_sdf_atuendo, Atuendo, DetalleSer, Peinado, Sombrero};
30
31/// Tonos de piel: el espectro humano, de más claro a más oscuro. Escritos a mano
32/// (no interpolados) porque la piel no viaja por una recta del cubo RGB — el medio
33/// tiene otro matiz que las puntas.
34const PIELES: [u32; 10] = [
35    0xF2D9C2, 0xEBCBAA, 0xE0B892, 0xD2A277, 0xC08A5E, 0xA9744B, 0x8E5C39, 0x734830, 0x573526,
36    0x3E241B,
37];
38
39/// Colores de pelo: negros, castaños, rubios, pelirrojos, canas y dos teñidos.
40const PELOS: [u32; 11] = [
41    0x1A1618, 0x2E2119, 0x4A3222, 0x6A4526, 0x8A6034, 0xB08040, 0xD8B870, 0x9A3A20, 0x8A8A8E,
42    0xE0DED8, 0x6A3AC0,
43];
44
45/// Paleta de ropa: colores que se ven en una calle, ni pasteles de catálogo ni
46/// saturados de juguete.
47const REMERAS: [u32; 18] = [
48    0xC8483C, 0xE0783A, 0xE8B830, 0x6A9A3A, 0x2E8A5E, 0x2A7A8A, 0x2E5EA0, 0x4A4AA0, 0x8A4A9A,
49    0xD06A90, 0xF2EFE8, 0xC8C2B4, 0x8A8A90, 0x3A3A42, 0x7A5A3A, 0x2A6A6A, 0xE8DCC0, 0x9A2A3A,
50];
51
52/// Pantalones/faldas: menos variedad y más oscuros, como en la calle de verdad.
53const PANTALONES: [u32; 10] = [
54    0x2E3A55, 0x1E2430, 0x3A3A40, 0x5A5A62, 0x6A5A46, 0x8A7A62, 0x2E4A3A, 0x4A2A2A, 0xC8C0B0,
55    0x223244,
56];
57
58/// Una persona: la constitución, la paleta y lo que lleva puesto. Es todo lo que
59/// hace falta para dibujarla en cualquier pose.
60#[derive(Debug, Clone, Copy)]
61pub struct Persona {
62    pub edad: Age,
63    pub estilo: EstiloSer,
64    pub colores: Colores,
65    pub atuendo: Atuendo,
66    /// Alto en unidades de mundo (metros, si el mundo está en metros).
67    pub altura: f32,
68}
69
70impl Persona {
71    /// La persona armada como ser SDF, en la pose del `clip`/`fase`, sin posar
72    /// (local, pies en el origen).
73    pub fn ser(&self, clip: Clip, fase: f32, detalle: DetalleSer) -> SdfSer {
74        let gesto = crate::Gesto::vivo(fase, clip);
75        ser_sdf_atuendo(
76            &self.estilo,
77            &clip.pose(fase),
78            self.altura,
79            self.colores,
80            &gesto,
81            detalle,
82            &self.atuendo,
83        )
84    }
85
86    /// La persona posada en el mundo, mirando a `look` si se le pasa un punto.
87    pub fn posada(
88        &self,
89        clip: Clip,
90        fase: f32,
91        pos: Vec3,
92        yaw: f32,
93        look: Option<Vec3>,
94        detalle: DetalleSer,
95    ) -> SdfSer {
96        let mut gesto = crate::Gesto::vivo(fase, clip);
97        if let Some(t) = look {
98            let ojos = crate::altura_ojos(&self.estilo, self.altura);
99            gesto = gesto.mirar(t, pos, yaw, ojos);
100        }
101        ser_sdf_atuendo(
102            &self.estilo,
103            &clip.pose(fase),
104            self.altura,
105            self.colores,
106            &gesto,
107            detalle,
108            &self.atuendo,
109        )
110        .posado([pos.x, pos.y, pos.z], yaw)
111    }
112}
113
114/// **Una persona a partir de una semilla.** Función pura: la misma semilla da
115/// siempre la misma persona, en el preview y en el export.
116pub fn persona(sem: u32) -> Persona {
117    // Edad: la pirámide de una calle cualquiera — mayoría adulta, algunos chicos y
118    // jóvenes, unos pocos mayores. Sin bebés: un bebé no camina solo por la vereda.
119    let e = hash01(sem, 1);
120    let edad = if e < 0.14 {
121        Age::Child
122    } else if e < 0.30 {
123        Age::Teen
124    } else if e < 0.86 {
125        Age::Adult
126    } else {
127        Age::Elder
128    };
129
130    // Constitución: la edad pone la base y la semilla la sacude. Los rangos son
131    // angostos a propósito — un canon de cabezas ±0.45 ya cambia la silueta entera.
132    let mut estilo = EstiloSer::por_edad(edad);
133    estilo.cabezas = (estilo.cabezas + hash11(sem, 2) * 0.45).clamp(2.6, 5.2);
134    estilo.redondez = (0.55 + hash01(sem, 3) * 0.45).clamp(0.0, 1.0);
135    estilo.angulosidad = (hash01(sem, 4) * 0.55).clamp(0.0, 1.0);
136    estilo.panza = (estilo.panza + hash11(sem, 5) * 0.16).clamp(0.85, 1.35);
137    estilo.extremidad = (estilo.extremidad * (1.0 + hash11(sem, 6) * 0.22)).clamp(0.045, 0.11);
138    estilo.mejilla = (estilo.mejilla + hash11(sem, 7) * 0.08).clamp(0.92, 1.24);
139    estilo.ojo_tam = (estilo.ojo_tam + hash11(sem, 8) * 0.03).clamp(0.15, 0.34);
140
141    // Altura: la del canon de la edad, ±9 %.
142    let altura = Build::for_age(edad).height * (1.0 + hash11(sem, 9) * 0.09);
143
144    // Piel, pelo y ropa.
145    let piel = rgb(PIELES[(hash01(sem, 10) * PIELES.len() as f32) as usize % PIELES.len()]);
146    let mut i_pelo = (hash01(sem, 11) * PELOS.len() as f32) as usize % PELOS.len();
147    if edad == Age::Elder && hash01(sem, 12) < 0.72 {
148        // Las canas no son un color más: casi todo mayor las tiene.
149        i_pelo = if hash01(sem, 13) < 0.5 { 8 } else { 9 };
150    } else if i_pelo == 10 && hash01(sem, 14) > 0.12 {
151        // El teñido existe, pero uno de cada ocho, no uno de cada once.
152        i_pelo = 1;
153    }
154    let pelo = rgb(PELOS[i_pelo]);
155    let remera = rgb(REMERAS[(hash01(sem, 15) * REMERAS.len() as f32) as usize % REMERAS.len()]);
156    let pantalon =
157        rgb(PANTALONES[(hash01(sem, 16) * PANTALONES.len() as f32) as usize % PANTALONES.len()]);
158
159    // Peinado: correlacionado con la edad (la calvicie y el rodete no se reparten
160    // parejo por la pirámide).
161    let h = hash01(sem, 17);
162    let peinado = match edad {
163        Age::Child | Age::Baby => match h {
164            x if x < 0.30 => Peinado::Casquete,
165            x if x < 0.55 => Peinado::Corto,
166            x if x < 0.75 => Peinado::DosColas,
167            x if x < 0.90 => Peinado::Melena,
168            _ => Peinado::Afro,
169        },
170        Age::Elder => match h {
171            x if x < 0.30 => Peinado::Calvo,
172            x if x < 0.60 => Peinado::Corto,
173            x if x < 0.78 => Peinado::Rodete,
174            x if x < 0.92 => Peinado::Melena,
175            _ => Peinado::Larga,
176        },
177        _ => match h {
178            x if x < 0.18 => Peinado::Casquete,
179            x if x < 0.36 => Peinado::Corto,
180            x if x < 0.50 => Peinado::Melena,
181            x if x < 0.64 => Peinado::Larga,
182            x if x < 0.74 => Peinado::Coleta,
183            x if x < 0.83 => Peinado::Rodete,
184            x if x < 0.89 => Peinado::Afro,
185            x if x < 0.94 => Peinado::DosColas,
186            x if x < 0.97 => Peinado::Cresta,
187            _ => Peinado::Calvo,
188        },
189    };
190
191    let s = hash01(sem, 18);
192    let sombrero = match s {
193        x if x < 0.08 => Sombrero::Gorra,
194        x if x < 0.13 => Sombrero::Ala,
195        x if x < 0.17 => Sombrero::Gorro,
196        x if x < 0.21 => Sombrero::Panuelo,
197        x if x < 0.23 => Sombrero::Casco,
198        _ => Sombrero::Ninguno,
199    };
200    let barba = if matches!(edad, Age::Adult | Age::Elder) && hash01(sem, 19) < 0.34 {
201        0.35 + hash01(sem, 20) * 0.65
202    } else {
203        0.0
204    };
205    let falda = if hash01(sem, 21) < 0.30 {
206        0.35 + hash01(sem, 22) * 0.65
207    } else {
208        0.0
209    };
210    let color_falda = if falda > 0.0 && hash01(sem, 23) < 0.55 {
211        Some(pantalon)
212    } else {
213        None
214    };
215    let color_sombrero = if hash01(sem, 24) < 0.5 {
216        Some(rgb(PANTALONES[(hash01(sem, 25) * PANTALONES.len() as f32) as usize % PANTALONES.len()]))
217    } else {
218        None
219    };
220
221    Persona {
222        edad,
223        estilo,
224        colores: Colores::con_pelo(piel, remera, pantalon, pelo),
225        atuendo: Atuendo {
226            peinado,
227            barba,
228            sombrero,
229            color_sombrero,
230            anteojos: hash01(sem, 26) < 0.22,
231            falda,
232            color_falda,
233            manto: 0.0,
234            color_manto: None,
235        },
236        altura,
237    }
238}
239
240/// **Un grupo de `n` personas distintas** a partir de una semilla.
241pub fn multitud(sem: u32, n: usize) -> Vec<Persona> {
242    (0..n as u32).map(|i| persona(sem.wrapping_mul(0x9E37_79B9).wrapping_add(i * 7919))).collect()
243}
244
245/// Una persona **en la calle**: quién es, dónde está, hacia dónde va y en qué punto
246/// de su paso.
247#[derive(Debug, Clone, Copy)]
248pub struct Caminante {
249    pub persona: Persona,
250    /// Posición en coordenadas de mundo (las que consume el renderer).
251    pub pos: Vec3,
252    /// Rumbo en radianes (`0` = mirando `+Z`).
253    pub yaw: f32,
254    pub clip: Clip,
255    /// Fase del ciclo de caminata.
256    pub fase: f32,
257}
258
259/// **Una vereda con gente caminando**, determinista por semilla.
260///
261/// `ancho` × `largo` es la franja (en X y Z) por la que se reparten; `t` es el
262/// tiempo, y cada uno avanza a SU velocidad y vuelve a entrar por el otro extremo
263/// (la calle no se vacía). La mitad va en cada sentido — una multitud en la que
264/// todos caminan para el mismo lado es un desfile.
265///
266/// `origen` es la esquina de la franja: el llamador la pone donde tenga el piso.
267pub fn multitud_caminando(
268    sem: u32,
269    n: usize,
270    origen: Vec3,
271    ancho: f32,
272    largo: f32,
273    t: f32,
274) -> Vec<Caminante> {
275    let mut out = Vec::with_capacity(n);
276    for i in 0..n as u32 {
277        let s = sem.wrapping_mul(0x9E37_79B9).wrapping_add(i * 7919);
278        let persona = persona(s);
279        let sentido = if i % 2 == 0 { 1.0 } else { -1.0f32 };
280        // Velocidad: proporcional a la altura (las piernas largas caminan más
281        // rápido) y con su propio desvío. Un chico trota para seguir el ritmo.
282        let vel = persona.altura * (0.55 + 0.25 * hash01(s, 40));
283        let carril = (i as f32 + 0.5) / n as f32;
284        let x = origen.x + (carril + hash11(s, 41) * 0.03).clamp(0.0, 1.0) * ancho;
285        let avance = (hash01(s, 42) * largo + t * vel * sentido).rem_euclid(largo);
286        let z = origen.z + avance;
287        let clip = if hash01(s, 43) < 0.08 {
288            Clip::Idle
289        } else if hash01(s, 44) < 0.06 {
290            Clip::Run
291        } else {
292            Clip::Walk
293        };
294        // Fase propia: sin esto la multitud marcha en formación.
295        let fase = hash01(s, 45) * 6.283 + t * vel * 2.2;
296        out.push(Caminante {
297            persona,
298            pos: Vec3::new(x, origen.y, z),
299            yaw: if sentido > 0.0 { 0.0 } else { std::f32::consts::PI },
300            clip,
301            fase,
302        });
303    }
304    out
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use std::collections::HashSet;
311
312    #[test]
313    fn la_misma_semilla_da_la_misma_persona() {
314        let a = persona(1234);
315        let b = persona(1234);
316        assert_eq!(a.altura, b.altura);
317        assert_eq!(a.colores.skin, b.colores.skin);
318        assert_eq!(a.atuendo.peinado, b.atuendo.peinado);
319    }
320
321    /// Lo que esta caja tiene que garantizar: que una multitud NO sea la misma
322    /// persona repetida. Se mide sobre lo que el ojo agarra primero — silueta de la
323    /// cabeza, altura y color.
324    #[test]
325    fn una_multitud_de_treinta_no_se_repite() {
326        let g = multitud(7, 30);
327        let peinados: HashSet<_> = g.iter().map(|p| p.atuendo.peinado).collect();
328        assert!(peinados.len() >= 6, "sólo {} peinados en 30 personas", peinados.len());
329        let pieles: HashSet<_> = g.iter().map(|p| format!("{:?}", p.colores.skin)).collect();
330        assert!(pieles.len() >= 6, "sólo {} tonos de piel en 30", pieles.len());
331        let remeras: HashSet<_> = g.iter().map(|p| format!("{:?}", p.colores.shirt)).collect();
332        assert!(remeras.len() >= 10, "sólo {} remeras en 30", remeras.len());
333        // Alturas al centímetro: se repiten algunas (la edad manda el grueso y el
334        // desvío es ±9 %), pero dos tercios distintos ya es una multitud pareja.
335        let alturas: HashSet<_> = g.iter().map(|p| (p.altura * 100.0) as i32).collect();
336        assert!(alturas.len() >= 20, "sólo {} alturas distintas en 30", alturas.len());
337        // Y la constitución, que es lo que hace que no sean el mismo muñeco.
338        let cabezas: HashSet<_> = g.iter().map(|p| (p.estilo.cabezas * 20.0) as i32).collect();
339        assert!(cabezas.len() >= 15, "sólo {} cánones en 30", cabezas.len());
340    }
341
342    /// Las edades tienen que aparecer todas (una calle de puros adultos es un
343    /// error de reparto, no una calle).
344    #[test]
345    fn la_calle_tiene_chicos_y_mayores() {
346        let g = multitud(3, 60);
347        let edades: HashSet<_> = g.iter().map(|p| format!("{:?}", p.edad)).collect();
348        assert!(edades.len() >= 3, "sólo {edades:?}");
349        assert!(g.iter().any(|p| p.edad == Age::Child), "no hay chicos");
350        assert!(g.iter().any(|p| p.edad == Age::Elder), "no hay mayores");
351    }
352
353    /// Si todos comparten fase, la multitud MARCHA. Es el defecto clásico y no se
354    /// ve en un cuadro fijo: hay que medirlo.
355    #[test]
356    fn nadie_camina_en_la_misma_fase() {
357        let c = multitud_caminando(11, 20, Vec3::ZERO, 8.0, 30.0, 0.0);
358        let fases: HashSet<_> = c.iter().map(|k| (k.fase * 50.0) as i32).collect();
359        assert!(fases.len() >= 18, "sólo {} fases distintas en 20", fases.len());
360        // Y la mitad va para cada lado.
361        let ida = c.iter().filter(|k| k.yaw.abs() < 0.1).count();
362        assert!((ida as i32 - 10).abs() <= 2, "{ida} de 20 en un sentido");
363    }
364
365    /// La calle no se vacía: al avanzar el tiempo, todos siguen dentro de la franja.
366    #[test]
367    fn la_calle_no_se_vacia_al_avanzar_el_tiempo() {
368        for t in [0.0f32, 3.0, 17.0, 120.0] {
369            let c = multitud_caminando(5, 24, Vec3::new(-4.0, 0.0, 0.0), 8.0, 30.0, t);
370            for k in &c {
371                assert!(k.pos.z >= -0.01 && k.pos.z <= 30.01, "t={t} se fue a z={}", k.pos.z);
372                assert!(k.pos.x >= -4.01 && k.pos.x <= 4.01, "t={t} se fue a x={}", k.pos.x);
373            }
374        }
375    }
376
377    #[test]
378    fn una_persona_se_arma_como_ser_sdf() {
379        let p = persona(99);
380        let s = p.ser(Clip::Walk, 1.0, DetalleSer::Hero);
381        assert!(s.prims.len() > 20, "sólo {} primitivas", s.prims.len());
382        let (_, r) = s.bound();
383        assert!(r > 0.0 && r < p.altura * 3.0, "esfera envolvente rara: {r}");
384    }
385}