//! **La multitud**: gente diversa por semilla ([`persona`](llimphi_voxel::persona)) //! y una vereda con veinte personas caminando //! ([`multitud_caminando`](llimphi_voxel::multitud_caminando)). //! //! Lo que hay que mirar acá no es una figura sino el CONJUNTO: si al alejarse se ve //! un patrón —cuatro peinados repetidos, todos del mismo alto, todos en el mismo //! cuadro del paso— la multitud no es una multitud. //! //! ```text //! cargo run -p llimphi-voxel --example multitud_demo --release //! ``` //! Salida: `/tmp/multitud_retratos.png` (24 personas de cerca) y //! `/tmp/multitud_calle.png` (la vereda entera, en tres momentos). use llimphi_3d::glam::Vec3; use llimphi_3d::{Atmosphere, Camera3d, Scene3d, VoxelGrid, VoxelRenderer}; use llimphi_voxel::{multitud, multitud_caminando, Clip, DetalleSer}; #[path = "comun/mod.rs"] mod comun; const TW: u32 = 300; const TH: u32 = 340; const W: u32 = 1600; const H: u32 = 760; fn main() { let mut toma = comun::Toma::nueva(); let piso = toma.piso([132, 130, 124], [118, 116, 110]); let dir = comun::dir(); // --- Retratos: 24 personas distintas, encuadre común. let gente = multitud(2026, 24); let mut casillas = Vec::new(); println!("multitud_demo — 24 personas"); for (i, p) in gente.iter().enumerate() { let unidad = p.ser(Clip::Idle, 0.0, DetalleSer::Hero); let (_, r1) = unidad.bound(); let k = comun::RADIO / r1.max(1e-3); let mut alta = *p; alta.altura *= k; let clip = if i % 3 == 0 { Clip::Walk } else { Clip::Idle }; let ser = alta.ser(clip, i as f32 * 0.7, DetalleSer::Hero); let (c, r) = ser.bound(); let posado = ser.posado( [comun::DIM as f32 * 0.5, comun::PISO as f32, comun::DIM as f32 * 0.5], 0.0, ); let px = toma.casilla(&piso, vec![posado], c[1], r, 22.0, TW, TH); println!( " {:>2}. {:?} {:.2} m {:?}{}{}", i + 1, p.edad, p.altura, p.atuendo.peinado, if p.atuendo.barba > 0.0 { " +barba" } else { "" }, if p.atuendo.anteojos { " +anteojos" } else { "" }, ); casillas.push(( format!( "{:?} · {:.2} m · {:?}", p.edad, p.altura, p.atuendo.peinado ), px, )); } let (px, w, h) = toma.hoja("multitud — veinticuatro personas distintas", &casillas, 6, TW, TH); comun::png(&px, w, h, &format!("{dir}/multitud_retratos.png")); println!("→ {dir}/multitud_retratos.png"); // --- La calle: una franja con gente caminando en los dos sentidos. let dim = [160u32, 48, 200]; let mut grid = VoxelGrid::new(dim); for x in 0..dim[0] { for z in 0..dim[2] { // Vereda gris con juntas cada 20, y calle más oscura a un costado. let vereda = x > 30; let junta = (x % 20 == 0) || (z % 20 == 0); let c = match (vereda, junta) { (true, true) => [122, 120, 114], (true, false) => [148, 146, 138], (false, _) => [78, 78, 82], }; for y in 0..3 { grid.set(x, y, z, c); } } } let mut tomas = Vec::new(); // La calle sin nadie: la referencia contra la que se cuenta la gente. let mut vacia: Vec = Vec::new(); for (k, t) in [0.0f32, 4.0, 9.0].into_iter().enumerate() { let caminantes = multitud_caminando(77, 34, Vec3::new(40.0, 3.0, 10.0), 95.0, 150.0, t); // El reparto por presupuesto: los de cerca con cara, los del fondo sin ella. let ojo = Vec3::new(80.0, 12.0, -40.0); let mut orden: Vec<_> = caminantes .iter() .map(|c| ((c.pos - ojo).length(), c)) .collect(); orden.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); let mut sers = Vec::new(); let mut gastado = 0usize; for (d, c) in &orden { let alto_v = c.persona.altura * 4.2; // metros → voxels de esta escena let mut detalle = DetalleSer::por_distancia(*d, alto_v); while gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS { detalle = match detalle { DetalleSer::Hero => DetalleSer::Medio, DetalleSer::Medio => DetalleSer::Lejos, DetalleSer::Lejos => break, }; } if sers.len() >= llimphi_3d::VOXEL_MAX_SERS || gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS { break; } let mut p = c.persona; p.altura = alto_v; let ser = p.posada(c.clip, c.fase, c.pos, c.yaw, None, detalle); gastado += ser.prims.len(); sers.push(ser); } println!( " t={t}: {} personas en el cuadro, {gastado} primitivas de {}", sers.len(), llimphi_3d::VOXEL_MAX_SER_PRIMS ); let px = calle(&mut toma, &grid, dim, sers); if vacia.is_empty() { vacia = calle(&mut toma, &grid, dim, Vec::new()); } println!(" {} px de gente", comun::ocupacion(&px, &vacia)); tomas.push((format!("t = {t:.0} s"), px)); let _ = k; } let (px, w, h) = toma.hoja("multitud — la vereda, tres momentos", &tomas, 1, W, H); comun::png(&px, w, h, &format!("{dir}/multitud_calle.png")); println!("→ {dir}/multitud_calle.png"); } fn calle( toma: &mut comun::Toma, grid: &VoxelGrid, dim: [u32; 3], sers: Vec, ) -> Vec { let mut vr = VoxelRenderer::new(&toma.hal.device, &toma.hal.queue, comun::FMT, grid); vr.sun_dir = [0.36, 0.66, 0.66]; vr.atmosphere = Atmosphere { sky_zenith: [80, 124, 190], sky_horizon: [206, 222, 240], fog_density: 0.55 / dim[2] as f32, god_rays: 0.0, }; vr.sers = sers; let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; // Encuadre: la vereda a la altura de la gente, no un plano cenital de la // baldosa. En la primera toma dos tercios del cuadro eran pavimento vacío. let look = Vec3::new(dim[0] as f32 * 0.58, 7.0, dim[2] as f32 * 0.34) - half; let camera = Camera3d::orbit(look, 6_f32.to_radians(), 7.5_f32.to_radians(), 78.0); let tex = toma.hal.device.create_texture(&llimphi_hal::wgpu::TextureDescriptor { label: Some("calle"), size: llimphi_hal::wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: llimphi_hal::wgpu::TextureDimension::D2, format: comun::FMT, usage: llimphi_hal::wgpu::TextureUsages::STORAGE_BINDING | llimphi_hal::wgpu::TextureUsages::TEXTURE_BINDING | llimphi_hal::wgpu::TextureUsages::RENDER_ATTACHMENT | llimphi_hal::wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = tex.create_view(&llimphi_hal::wgpu::TextureViewDescriptor::default()); toma.renderer .render_to_view( &toma.hal, &llimphi_raster::vello::Scene::new(), &view, W, H, llimphi_raster::peniko::Color::from_rgba8(16, 18, 26, 255), ) .expect("base"); let mut enc = toma .hal .device .create_command_encoder(&llimphi_hal::wgpu::CommandEncoderDescriptor { label: None }); let mut scene = Scene3d::new(); scene.render( &toma.hal.device, &toma.hal.queue, &mut enc, &view, (W, H), &camera, Some(&mut vr), &[], ); toma.hal.queue.submit(std::iter::once(enc.finish())); let _ = toma.hal.device.poll(llimphi_hal::wgpu::PollType::wait_indefinitely()); comun::leer_pub(&toma.hal, &tex, W, H) }