tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! **La multitud**: gente diversa por semilla ([`persona`](llimphi_voxel::persona))
2//! y una vereda con veinte personas caminando
3//! ([`multitud_caminando`](llimphi_voxel::multitud_caminando)).
4//!
5//! Lo que hay que mirar acá no es una figura sino el CONJUNTO: si al alejarse se ve
6//! un patrón —cuatro peinados repetidos, todos del mismo alto, todos en el mismo
7//! cuadro del paso— la multitud no es una multitud.
8//!
9//! ```text
10//! cargo run -p llimphi-voxel --example multitud_demo --release
11//! ```
12//! Salida: `/tmp/multitud_retratos.png` (24 personas de cerca) y
13//! `/tmp/multitud_calle.png` (la vereda entera, en tres momentos).
14
15use llimphi_3d::glam::Vec3;
16use llimphi_3d::{Atmosphere, Camera3d, Scene3d, VoxelGrid, VoxelRenderer};
17use llimphi_voxel::{multitud, multitud_caminando, Clip, DetalleSer};
18
19#[path = "comun/mod.rs"]
20mod comun;
21
22const TW: u32 = 300;
23const TH: u32 = 340;
24const W: u32 = 1600;
25const H: u32 = 760;
26
27fn main() {
28 let mut toma = comun::Toma::nueva();
29 let piso = toma.piso([132, 130, 124], [118, 116, 110]);
30 let dir = comun::dir();
31
32 // --- Retratos: 24 personas distintas, encuadre común.
33 let gente = multitud(2026, 24);
34 let mut casillas = Vec::new();
35 println!("multitud_demo — 24 personas");
36 for (i, p) in gente.iter().enumerate() {
37 let unidad = p.ser(Clip::Idle, 0.0, DetalleSer::Hero);
38 let (_, r1) = unidad.bound();
39 let k = comun::RADIO / r1.max(1e-3);
40 let mut alta = *p;
41 alta.altura *= k;
42 let clip = if i % 3 == 0 { Clip::Walk } else { Clip::Idle };
43 let ser = alta.ser(clip, i as f32 * 0.7, DetalleSer::Hero);
44 let (c, r) = ser.bound();
45 let posado = ser.posado(
46 [comun::DIM as f32 * 0.5, comun::PISO as f32, comun::DIM as f32 * 0.5],
47 0.0,
48 );
49 let px = toma.casilla(&piso, vec![posado], c[1], r, 22.0, TW, TH);
50 println!(
51 " {:>2}. {:?} {:.2} m {:?}{}{}",
52 i + 1,
53 p.edad,
54 p.altura,
55 p.atuendo.peinado,
56 if p.atuendo.barba > 0.0 { " +barba" } else { "" },
57 if p.atuendo.anteojos { " +anteojos" } else { "" },
58 );
59 casillas.push((
60 format!(
61 "{:?} · {:.2} m · {:?}",
62 p.edad, p.altura, p.atuendo.peinado
63 ),
64 px,
65 ));
66 }
67 let (px, w, h) = toma.hoja("multitud — veinticuatro personas distintas", &casillas, 6, TW, TH);
68 comun::png(&px, w, h, &format!("{dir}/multitud_retratos.png"));
69 println!("→ {dir}/multitud_retratos.png");
70
71 // --- La calle: una franja con gente caminando en los dos sentidos.
72 let dim = [160u32, 48, 200];
73 let mut grid = VoxelGrid::new(dim);
74 for x in 0..dim[0] {
75 for z in 0..dim[2] {
76 // Vereda gris con juntas cada 20, y calle más oscura a un costado.
77 let vereda = x > 30;
78 let junta = (x % 20 == 0) || (z % 20 == 0);
79 let c = match (vereda, junta) {
80 (true, true) => [122, 120, 114],
81 (true, false) => [148, 146, 138],
82 (false, _) => [78, 78, 82],
83 };
84 for y in 0..3 {
85 grid.set(x, y, z, c);
86 }
87 }
88 }
89 let mut tomas = Vec::new();
90 // La calle sin nadie: la referencia contra la que se cuenta la gente.
91 let mut vacia: Vec<u8> = Vec::new();
92 for (k, t) in [0.0f32, 4.0, 9.0].into_iter().enumerate() {
93 let caminantes = multitud_caminando(77, 34, Vec3::new(40.0, 3.0, 10.0), 95.0, 150.0, t);
94 // El reparto por presupuesto: los de cerca con cara, los del fondo sin ella.
95 let ojo = Vec3::new(80.0, 12.0, -40.0);
96 let mut orden: Vec<_> = caminantes
97 .iter()
98 .map(|c| ((c.pos - ojo).length(), c))
99 .collect();
100 orden.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
101 let mut sers = Vec::new();
102 let mut gastado = 0usize;
103 for (d, c) in &orden {
104 let alto_v = c.persona.altura * 4.2; // metros → voxels de esta escena
105 let mut detalle = DetalleSer::por_distancia(*d, alto_v);
106 while gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS {
107 detalle = match detalle {
108 DetalleSer::Hero => DetalleSer::Medio,
109 DetalleSer::Medio => DetalleSer::Lejos,
110 DetalleSer::Lejos => break,
111 };
112 }
113 if sers.len() >= llimphi_3d::VOXEL_MAX_SERS
114 || gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS
115 {
116 break;
117 }
118 let mut p = c.persona;
119 p.altura = alto_v;
120 let ser = p.posada(c.clip, c.fase, c.pos, c.yaw, None, detalle);
121 gastado += ser.prims.len();
122 sers.push(ser);
123 }
124 println!(
125 " t={t}: {} personas en el cuadro, {gastado} primitivas de {}",
126 sers.len(),
127 llimphi_3d::VOXEL_MAX_SER_PRIMS
128 );
129 let px = calle(&mut toma, &grid, dim, sers);
130 if vacia.is_empty() {
131 vacia = calle(&mut toma, &grid, dim, Vec::new());
132 }
133 println!(" {} px de gente", comun::ocupacion(&px, &vacia));
134 tomas.push((format!("t = {t:.0} s"), px));
135 let _ = k;
136 }
137 let (px, w, h) = toma.hoja("multitud — la vereda, tres momentos", &tomas, 1, W, H);
138 comun::png(&px, w, h, &format!("{dir}/multitud_calle.png"));
139 println!("→ {dir}/multitud_calle.png");
140}
141
142fn calle(
143 toma: &mut comun::Toma,
144 grid: &VoxelGrid,
145 dim: [u32; 3],
146 sers: Vec<llimphi_3d::SdfSer>,
147) -> Vec<u8> {
148 let mut vr = VoxelRenderer::new(&toma.hal.device, &toma.hal.queue, comun::FMT, grid);
149 vr.sun_dir = [0.36, 0.66, 0.66];
150 vr.atmosphere = Atmosphere {
151 sky_zenith: [80, 124, 190],
152 sky_horizon: [206, 222, 240],
153 fog_density: 0.55 / dim[2] as f32,
154 god_rays: 0.0,
155 };
156 vr.sers = sers;
157 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
158 // Encuadre: la vereda a la altura de la gente, no un plano cenital de la
159 // baldosa. En la primera toma dos tercios del cuadro eran pavimento vacío.
160 let look = Vec3::new(dim[0] as f32 * 0.58, 7.0, dim[2] as f32 * 0.34) - half;
161 let camera = Camera3d::orbit(look, 6_f32.to_radians(), 7.5_f32.to_radians(), 78.0);
162 let tex = toma.hal.device.create_texture(&llimphi_hal::wgpu::TextureDescriptor {
163 label: Some("calle"),
164 size: llimphi_hal::wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
165 mip_level_count: 1,
166 sample_count: 1,
167 dimension: llimphi_hal::wgpu::TextureDimension::D2,
168 format: comun::FMT,
169 usage: llimphi_hal::wgpu::TextureUsages::STORAGE_BINDING
170 | llimphi_hal::wgpu::TextureUsages::TEXTURE_BINDING
171 | llimphi_hal::wgpu::TextureUsages::RENDER_ATTACHMENT
172 | llimphi_hal::wgpu::TextureUsages::COPY_SRC,
173 view_formats: &[],
174 });
175 let view = tex.create_view(&llimphi_hal::wgpu::TextureViewDescriptor::default());
176 toma.renderer
177 .render_to_view(
178 &toma.hal,
179 &llimphi_raster::vello::Scene::new(),
180 &view,
181 W,
182 H,
183 llimphi_raster::peniko::Color::from_rgba8(16, 18, 26, 255),
184 )
185 .expect("base");
186 let mut enc = toma
187 .hal
188 .device
189 .create_command_encoder(&llimphi_hal::wgpu::CommandEncoderDescriptor { label: None });
190 let mut scene = Scene3d::new();
191 scene.render(
192 &toma.hal.device,
193 &toma.hal.queue,
194 &mut enc,
195 &view,
196 (W, H),
197 &camera,
198 Some(&mut vr),
199 &[],
200 );
201 toma.hal.queue.submit(std::iter::once(enc.finish()));
202 let _ = toma.hal.device.poll(llimphi_hal::wgpu::PollType::wait_indefinitely());
203 comun::leer_pub(&toma.hal, &tex, W, H)
204}