//! **La noche, certificada en el píxel.** Filma la MISMA escena del arranque //! (una princesa que camina y saluda con una estrella en la mano) de tres //! maneras —de día, de noche, y de noche sin la estrella— y cuenta lo que //! cambió, sin mirar: //! //! - **el cielo se oscurece**: luminancia media de la franja de arriba; //! - **hay estrellas**: puntos aislados más claros que el cielo que los rodea, //! en el plano de contraluz (el que encuadra la luna). De día tiene que dar //! cero: es el brazo de control del contador; //! - **hay luna**: el píxel más claro del cielo en ese plano; //! - **la estrella alumbra**: cuántos píxeles cambian al sacarla, de noche //! contra de día. De día sólo cambia la estrella misma; de noche, además, //! lo que su luz alcanza. Y de día tiene que ser POCO: la estrella mide 0,9 //! y cabe en unos cientos de píxeles. Este número fue el que delató que en //! la Iris Xe todos los seres se dibujaban como su esfera envolvente (daba //! ~12.000: la estrella era una bola, y la actriz otra). //! //! ```sh //! cargo run -p llimphi-voxel-studio --release --example noche_en_el_pixel //! ``` //! //! Y el **decorado** y el **mar**: el mismo plano general en el mundo «mar», con //! y sin el palacio y el kiosko plantados al fondo — cuántos píxeles cambia el //! decorado, y cuánta agua hay en cuadro contra el desierto. //! //! Deja los cuadros en `/tmp/noche_*.png` por si alguien quiere mirarlos. use std::io::BufWriter; use llimphi_hal::{wgpu, Hal}; use llimphi_raster::peniko::Color; use llimphi_raster::{vello, Renderer}; use llimphi_voxel::{ world_dim, CasaTipo, Cielo, Clip, Decorado, Project, SceneSpec, Utileria, UtileriaSpec, PREVIEW_DIM_XZ, }; use llimphi_voxel_render::{Escenario, FMT}; const W: u32 = 960; const H: u32 = 540; /// Plano de contraluz (mira hacia el sol, que de noche es la luna). const T_CONTRALUZ: f32 = 1.8; /// Plano general, todavía caminando: la estrella y lo que tiene alrededor. const T_SALUDO: f32 = 0.9; fn escena(p: &Project, noche: bool, estrella: bool) -> SceneSpec { escena_en(p, "desierto", noche, estrella) } fn escena_en(p: &Project, mundo: &str, noche: bool, estrella: bool) -> SceneSpec { let mundo = p.mundos.iter().find(|m| m.name == mundo).expect("mundo").id; let mut s = SceneSpec::walk_and_emote("noche", mundo, 1, Clip::Wave, world_dim(PREVIEW_DIM_XZ)); s.handheld = 0.0; // sin temblor: los tres cuadros tienen que encuadrar igual if estrella { let mut e = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]); e.lleva = Some(0); e.alto = 0.9; s.utileria.push(e); } if noche { s.cielo = Some(Cielo::noche()); } s } fn filmar(hal: &Hal, renderer: &mut Renderer, p: &Project, s: &SceneSpec, t: f32, nombre: &str) -> Vec { let mut escenario = Escenario::preparar(&hal.device, &hal.queue, p, s).expect("escenario"); let target = hal.device.create_texture(&wgpu::TextureDescriptor { label: Some("noche"), size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: FMT, usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = target.create_view(&wgpu::TextureViewDescriptor::default()); let [r, g, b, a] = escenario.cielo(); renderer .render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(r, g, b, a)) .expect("clear"); let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); escenario.cuadro(&hal.device, &hal.queue, &mut enc, &view, (W, H), (0.0, 0.0, W as f32, H as f32), t); hal.queue.submit(std::iter::once(enc.finish())); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); let px = leer(hal, &target); guardar(&px, &format!("/tmp/noche_{nombre}.png")); px } fn leer(hal: &Hal, target: &wgpu::Texture) -> Vec { let unpadded = (W * 4) as usize; let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; let padded = unpadded.div_ceil(align) * align; let buf = hal.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: (padded * H as usize) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); enc.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H), }, }, wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, ); hal.queue.submit(std::iter::once(enc.finish())); let slice = buf.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); rx.recv().unwrap().unwrap(); let data = slice.get_mapped_range(); let mut px = Vec::with_capacity((W * H * 4) as usize); for row in 0..H as usize { px.extend_from_slice(&data[row * padded..row * padded + unpadded]); } px } fn guardar(px: &[u8], ruta: &str) { let f = std::fs::File::create(ruta).expect("png"); let mut e = png::Encoder::new(BufWriter::new(f), W, H); e.set_color(png::ColorType::Rgba); e.set_depth(png::BitDepth::Eight); e.write_header().unwrap().write_image_data(px).unwrap(); } fn lum(px: &[u8], x: u32, y: u32) -> f32 { let i = ((y * W + x) * 4) as usize; 0.2126 * px[i] as f32 + 0.7152 * px[i + 1] as f32 + 0.0722 * px[i + 2] as f32 } /// Luminancia media de la franja de arriba (el 15 % del alto). fn cielo_medio(px: &[u8]) -> f32 { let h = H * 15 / 100; let mut s = 0.0; for y in 0..h { for x in 0..W { s += lum(px, x, y); } } s / (h * W) as f32 } /// Estrellas en la mitad de arriba: un píxel que es máximo local y le saca /// más de 40 de luminancia a la mediana de su anillo de radio 3. fn estrellas(px: &[u8]) -> usize { let mut n = 0; for y in 3..H / 2 { for x in 3..W - 3 { let c = lum(px, x, y); let mut anillo = Vec::with_capacity(24); let mut maximo = true; for dy in -3i32..=3 { for dx in -3i32..=3 { if dx == 0 && dy == 0 { continue; } let v = lum(px, (x as i32 + dx) as u32, (y as i32 + dy) as u32); if dx.abs().max(dy.abs()) <= 1 && v >= c { maximo = false; } if dx.abs().max(dy.abs()) == 3 { anillo.push(v); } } } anillo.sort_by(f32::total_cmp); if maximo && c - anillo[anillo.len() / 2] > 40.0 { n += 1; } } } n } /// El píxel más claro de la mitad de arriba. fn mas_claro(px: &[u8]) -> f32 { (0..H / 2).flat_map(|y| (0..W).map(move |x| (x, y))).map(|(x, y)| lum(px, x, y)).fold(0.0, f32::max) } /// Píxeles «de agua»: azul que domina claramente al rojo y al verde. fn agua(px: &[u8]) -> usize { px.chunks_exact(4).filter(|c| c[2] as i32 > c[0] as i32 + 40 && c[2] as i32 > c[1] as i32 + 10).count() } /// Cuántos píxeles difieren en más de `umbral` de luminancia. fn cambiados(a: &[u8], b: &[u8], umbral: f32) -> usize { (0..H).flat_map(|y| (0..W).map(move |x| (x, y))).filter(|&(x, y)| (lum(a, x, y) - lum(b, x, y)).abs() > umbral).count() } fn main() { let hal = pollster::block_on(Hal::new(None)).expect("gpu"); let mut renderer = Renderer::new(&hal).expect("renderer"); let p = Project::starter(); let dia_c = filmar(&hal, &mut renderer, &p, &escena(&p, false, true), T_CONTRALUZ, "dia_contraluz"); let noche_c = filmar(&hal, &mut renderer, &p, &escena(&p, true, true), T_CONTRALUZ, "noche_contraluz"); let noche_c_sin = filmar(&hal, &mut renderer, &p, &escena(&p, true, false), T_CONTRALUZ, "noche_contraluz_sin_estrella"); let dia = filmar(&hal, &mut renderer, &p, &escena(&p, false, true), T_SALUDO, "dia_saludo"); let dia_sin = filmar(&hal, &mut renderer, &p, &escena(&p, false, false), T_SALUDO, "dia_saludo_sin_estrella"); let noche = filmar(&hal, &mut renderer, &p, &escena(&p, true, true), T_SALUDO, "noche_saludo"); let noche_sin = filmar(&hal, &mut renderer, &p, &escena(&p, true, false), T_SALUDO, "noche_saludo_sin_estrella"); println!("cielo (franja de arriba, luminancia media 0..255)"); println!(" día contraluz {:6.1} saludo {:6.1}", cielo_medio(&dia_c), cielo_medio(&dia)); println!(" noche contraluz {:6.1} saludo {:6.1}", cielo_medio(&noche_c), cielo_medio(&noche)); println!("estrellas en la mitad de arriba (contraluz)"); println!(" día {} noche {}", estrellas(&dia_c), estrellas(&noche_c)); println!("píxel más claro del cielo (contraluz): día {:.0} noche {:.0}", mas_claro(&dia_c), mas_claro(&noche_c)); println!("contraluz de noche: píxeles que cambian al sacar la estrella {}", cambiados(&noche_c, &noche_c_sin, 8.0)); println!("píxeles que cambian al sacar la estrella (plano general, umbral 8)"); let (cd, cn) = (cambiados(&dia, &dia_sin, 8.0), cambiados(&noche, &noche_sin, 8.0)); println!(" día {cd} noche {cn} ({:.1}×)", cn as f32 / cd.max(1) as f32); // El decorado, plantado DELANTE de la cámara: detrás de la actriz en la // dirección en que mira el plano (el palacio) y a un costado (el kiosko). // Puesto respecto del recorrido caía fuera de cuadro y no medía nada. let mut con = escena_en(&p, "mar", false, false); let (frente, lado, ahi) = { let e = Escenario::preparar(&hal.device, &hal.queue, &p, &con).expect("escenario"); let cam = e.camara_en(T_SALUDO); let f = (cam.target - cam.eye) * llimphi_3d::glam::Vec3::new(1.0, 0.0, 1.0); let f = f.normalize_or_zero(); let s = con.scripts()[0].sample(T_SALUDO); (f, llimphi_3d::glam::Vec3::new(-f.z, 0.0, f.x), (s.gx, s.gz)) }; let punto = |adelante: f32, costado: f32| { (ahi.0 + frente.x * adelante + lado.x * costado, ahi.1 + frente.z * adelante + lado.z * costado) }; let (px, pz) = punto(26.0, 0.0); let (kx, kz) = punto(10.0, -12.0); con.decorado.push(Decorado::en(CasaTipo::Palacio, px, pz)); con.decorado.push(Decorado::en(CasaTipo::Kiosko, kx, kz)); let mar_con = filmar(&hal, &mut renderer, &p, &con, T_SALUDO, "mar_decorado"); let mar_sin = filmar(&hal, &mut renderer, &p, &escena_en(&p, "mar", false, false), T_SALUDO, "mar_sin_decorado"); println!("decorado (mar, plano general): píxeles que cambian al plantarlo {}", cambiados(&mar_con, &mar_sin, 8.0)); println!("agua en cuadro: mar {} desierto {}", agua(&mar_sin), agua(&dia_sin)); // La luna, de noche: sólo para mirar (no hay número que diga «parece la luna»). let _ = filmar(&hal, &mut renderer, &p, &escena_en(&p, "luna", true, true), T_CONTRALUZ, "luna_contraluz"); }