//! **Demo end-to-end: «un personaje desde foto, en El Limón mirando las montañas».** //! //! La foto NO se voxeliza literal (eso da el valle de inquietud). Corre así: //! 1. Recorta la persona de la foto (segmentación u2net por el daemon pixel-verbo). //! 2. Muestrea sus RASGOS de color (piel arriba, ropa abajo) — nada de geometría. //! 3. Arma un **personaje ESTILIZADO** del kit (humanoide voxel blocky) con esos //! colores: su estilo es propio, la foto sólo regula piel/ropa. //! 4. Arma el terreno voxel desde un **DEM real** de El Limón/Maracay (SRTM): el llano //! al sur, la Cordillera de la Costa (Henri Pittier) al norte. //! 5. Planta el personaje en el llano (`stamp_busto`) y rinde una toma INMERSIVA: el //! personaje en primer plano, las montañas de fondo, luz cálida y bruma. //! //! Socket por env: `SEG_SOCK` (u2net). Foto/DEM/salida por args. Perillas de cámara y //! escala por env: `CAM_Z`, `CAM_H`, `CAM_PITCH`, `CAM_YAW`, `BUST_Z`, `BUST_X`, `PERS_H`, `EXAG`. //! //! `cargo run -p llimphi-voxel-studio --example demo_elimon --release -- ` use std::io::BufWriter; use llimphi_3d::glam::Vec3; use llimphi_3d::{Atmosphere, Camera3d, VoxelGrid, VoxelRenderer}; use llimphi_hal::{wgpu, Hal}; use llimphi_raster::peniko::Color; use llimphi_raster::{vello, Renderer}; use pixel_verbo_core::{Imagen, OpPixel, Proveedor}; use pixel_verbo_daemon::ClienteBloqueante; const W: u32 = 1280; const H: u32 = 720; const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; fn env_f(k: &str, d: f32) -> f32 { std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d) } fn main() { let args: Vec = std::env::args().collect(); let foto = args.get(1).cloned().unwrap_or_default(); let dem = args.get(2).cloned().unwrap_or_default(); let out = args.get(3).cloned().unwrap_or_else(|| "/tmp/elimon.png".into()); // --- Foto → RGBA (downscale para socket + busto) --- let img = image::open(&foto).expect("abrir foto").to_rgba8(); let ancho_proc = 384u32; let alto_proc = (img.height() * ancho_proc / img.width()).max(1); let chica = image::imageops::resize(&img, ancho_proc, alto_proc, image::imageops::FilterType::Lanczos3); let (pw, ph) = (chica.width(), chica.height()); let rgba = chica.into_raw(); // --- 1. Recorte de la persona (u2net por daemon) → alpha --- let seg_sock = std::env::var("SEG_SOCK").expect("SEG_SOCK"); let seg = ClienteBloqueante::conectar(&seg_sock).expect("conectar u2net"); let recorte = seg .aplicar(&OpPixel::Segmentar { prompt: None }, Some(Imagen::nueva(pw, ph, rgba.clone()).unwrap())) .expect("segmentar"); let alpha: Vec = recorte.bytes.chunks_exact(4).map(|p| p[3]).collect(); eprintln!("recorte: {}x{} (alpha = persona)", recorte.ancho, recorte.alto); // --- 2. RASGOS de color (piel arriba, ropa abajo) — nada de geometría de la foto --- let (skin, shirt, pants) = rasgos(&rgba, &alpha, pw, ph); eprintln!("rasgos: piel {skin:?} ropa {shirt:?}"); // --- 3. Personaje ESTILIZADO del kit (humanoide voxel), coloreado por los rasgos --- let pers_h = env_f("PERS_H", 46.0) as u32; let personaje = personaje_voxel(pers_h, skin, shirt, pants); eprintln!("personaje estilizado: dim {:?}", personaje.dim()); // --- 4. Terreno desde el DEM real de El Limón/Maracay --- let exag = env_f("EXAG", 1.0); let (mut grid, dim) = terreno_desde_dem(&dem, exag); // --- 5. Plantar el personaje en el llano (sur), mirando al sur (a cámara) --- let bx = env_f("BUST_X", (dim[0] as f32) * 0.5) as u32; let bz = env_f("BUST_Z", (dim[2] as f32) * 0.24) as u32; let by = altura_terreno(&grid, bx, bz); // facing 2 = el frente del personaje (+Z) queda hacia −Z = hacia la cámara. plantar(&mut grid, &personaje, bx, by + 1, bz, 2); eprintln!("personaje plantado en ({bx},{},{bz}) del mundo {:?}", by + 1, dim); // --- Render inmersivo --- let hal = pollster::block_on(Hal::new(None)).expect("hal"); let mut renderer = Renderer::new(&hal).expect("renderer"); let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid); // Look sub-voxel cinematográfico: montañas suaves + busto redondeado (bisel), con // grano de terreno; silueta por material para que el busto (bisel autorado) redondee. vr.bisel = 0.85; vr.rugosidad_amp = 0.5; vr.rugosidad_freq = 1.7; vr.silueta_por_voxel = true; // Atardecer tropical: sol cálido bajo desde el oeste, cielo azul, bruma para dar aire. vr.sun_dir = normalize([0.5, 0.55, -0.35]); vr.atmosphere = Atmosphere { sky_zenith: [58, 104, 176], sky_horizon: [206, 196, 168], fog_density: env_f("FOG", 0.010), god_rays: 0.5, }; // Cámara: parada en el llano sur, a la altura de los ojos, mirando al norte (+Z) a // las montañas; el busto queda en primer plano. Todo en coords de mundo (grid // centrado en el origen: voxel (gx,gy,gz) ↦ (gx,gy,gz) − dim/2). let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; let cam_z = env_f("CAM_Z", bz as f32 - 34.0); let cam_x = env_f("CAM_X", bx as f32 + 6.0); let suelo_cam = altura_terreno(&grid, cam_x as u32, cam_z.max(0.0) as u32) as f32; let cam_h = suelo_cam + env_f("CAM_H", 30.0); let eye = Vec3::new(cam_x, cam_h, cam_z) - half; let cam = Camera3d::fly(eye, env_f("CAM_YAW", 0.0).to_radians(), env_f("CAM_PITCH", 6.0).to_radians()); let px = render(&hal, &mut renderer, &mut vr, &cam); write_png(&px, &out); eprintln!("✓ toma inmersiva escrita: {out}"); } /// Muestrea los RASGOS de color de la persona recortada (`alpha>128`): piel cálida del /// tercio superior del bbox, ropa de la mitad inferior; pantalón derivado (más oscuro). /// Devuelve `[u8;3]` para colorear los voxels del personaje. NO toca geometría. fn rasgos(rgba: &[u8], alpha: &[u8], w: u32, h: u32) -> ([u8; 3], [u8; 3], [u8; 3]) { let (w, h) = (w as usize, h as usize); let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize); for y in 0..h { for x in 0..w { if alpha[y * w + x] > 128 { x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y); } } } let bh = (y1.saturating_sub(y0)).max(1); // Piel: cálidos del tercio superior. let (mut sr, mut sg, mut sb, mut sn) = (0u64, 0u64, 0u64, 0u64); for y in y0..(y0 + bh * 45 / 100).min(h) { for x in x0..=x1.min(w - 1) { if alpha[y * w + x] <= 128 { continue; } let o = (y * w + x) * 4; let (r, g, b) = (rgba[o] as i32, rgba[o + 1] as i32, rgba[o + 2] as i32); if r > g && g >= b && (r - b) > 12 && (r - b) < 130 && (r + g + b) > 150 { sr += r as u64; sg += g as u64; sb += b as u64; sn += 1; } } } let skin = if sn > 30 { [(sr / sn) as u8, (sg / sn) as u8, (sb / sn) as u8] } else { [216, 172, 138] }; // Ropa: promedio de la mitad inferior. let (mut cr, mut cg, mut cb, mut cn) = (0u64, 0u64, 0u64, 0u64); for y in (y0 + bh / 2)..=y1.min(h - 1) { for x in x0..=x1.min(w - 1) { if alpha[y * w + x] <= 128 { continue; } let o = (y * w + x) * 4; cr += rgba[o] as u64; cg += rgba[o + 1] as u64; cb += rgba[o + 2] as u64; cn += 1; } } let shirt = if cn > 0 { [(cr / cn) as u8, (cg / cn) as u8, (cb / cn) as u8] } else { [70, 120, 150] }; let pants = [(shirt[0] as f32 * 0.45) as u8, (shirt[1] as f32 * 0.5) as u8, (shirt[2] as f32 * 0.7) as u8]; (skin, shirt, pants) } /// **Personaje ESTILIZADO del kit**: NO cajas tipo Minecraft — formas ORGÁNICAS /// (elipsoides/cápsulas) en proporciones chibi expresivas (cabezón, cuerpo redondo, /// miembros rechonchos), coloreadas por los rasgos de la foto. Se rinde con bisel alto: /// el motor sub-voxel traza la isosuperficie suave y lo convierte en una figura /// redondeada tipo vinyl-toy/Fall-Guys, con carácter. Mira a +Z (ojos grandes al frente). /// Cada voxel lleva su detalle `(rugosidad, bisel)` para que la silueta salga lisa. fn personaje_voxel(h: u32, skin: [u8; 3], shirt: [u8; 3], pants: [u8; 3]) -> VoxelGrid { let h = h.max(28) as f32; // Proporciones chibi (cabezón simpático) pero con cuerpo presente. let head_r = h * 0.205; let body_rx = h * 0.20; let body_ry = h * 0.22; let body_rz = h * 0.17; let limb_r = h * 0.072; let w = (body_rx.max(head_r) * 2.0 + limb_r * 4.0 + 6.0) as u32; let depth = (body_rz.max(head_r) * 2.0 + limb_r * 2.0 + 4.0) as u32; let gy = (h + 4.0) as u32; let mut g = VoxelGrid::new([w, gy, depth]); let (cx, cz) = (w as f32 / 2.0, depth as f32 / 2.0); let leg_cy = h * 0.12; let body_cy = h * 0.42; let head_cy = h * 0.78; let pelo = [46u8, 36, 32]; // Piernas: dos cápsulas cortas y rechonchas. capsula(&mut g, cx - body_rx * 0.42, leg_cy - h * 0.11, leg_cy + h * 0.11, cz, limb_r, pants); capsula(&mut g, cx + body_rx * 0.42, leg_cy - h * 0.11, leg_cy + h * 0.11, cz, limb_r, pants); // Brazos: cápsulas pegadas al cuerpo, caídas (no en cruz). capsula(&mut g, cx - body_rx - limb_r * 0.3, body_cy - h * 0.13, body_cy + h * 0.05, cz, limb_r, shirt); capsula(&mut g, cx + body_rx + limb_r * 0.3, body_cy - h * 0.13, body_cy + h * 0.05, cz, limb_r, shirt); // Cuerpo: elipsoide redondo (pancita). elipsoide(&mut g, cx, body_cy, cz, body_rx, body_ry, body_rz, shirt); // Cabeza: elipsoide grande. elipsoide(&mut g, cx, head_cy, cz, head_r, head_r * 1.04, head_r * 0.98, skin); // Pelo: casquete oscuro sobre la mitad superior de la cabeza (centrado, sin tapar ojos). elipsoide_parcial(&mut g, cx, head_cy + head_r * 0.34, cz, head_r * 1.03, head_r * 0.82, head_r * 1.02, pelo, head_cy + head_r * 0.30); // Ojos: dos esferas oscuras grandes al frente (+Z) con brillo blanco → expresividad. let zf = cz + head_r * 0.80; let ex = head_r * 0.40; let ey = head_cy + head_r * 0.02; let er = head_r * 0.22; for s in [-1.0f32, 1.0] { elipsoide(&mut g, cx + s * ex, ey, zf, er, er * 1.15, er * 0.6, [26, 22, 28]); // Chispa de brillo (arriba del ojo). elipsoide(&mut g, cx + s * ex - er * 0.3, ey + er * 0.4, zf + 0.5, er * 0.28, er * 0.28, er * 0.4, [235, 235, 240]); } // Boca: pequeña curva oscura bajo los ojos. elipsoide(&mut g, cx, head_cy - head_r * 0.42, cz + head_r * 0.78, head_r * 0.28, head_r * 0.10, head_r * 0.5, [120, 70, 66]); g.reset_dirty(); g } /// Planta el personaje en `dst` con la base en `(x,y,z)`, rotado por `facing`, y con un /// detalle sub-voxel de **bisel alto** por voxel (silueta lisa/orgánica) — así el motor lo /// redondea en vez de dejarlo cúbico. Espeja `stamp_busto` pero con NUESTRO detalle. fn plantar(dst: &mut VoxelGrid, src: &VoxelGrid, x: u32, y: u32, z: u32, facing: u8) { let [bw, bh, bd] = src.dim(); let (cx, cz) = (bw as i32 / 2, bd as i32 / 2); dst.set_detail_brush(0.2, 1.0); // rugosidad leve (piel) + bisel máximo (redondeo) for by in 0..bh { for bz in 0..bd { for bx in 0..bw { let c = match src.get(bx, by, bz) { Some(c) if c[3] > 127 => c, _ => continue, }; let (dx, dz) = (bx as i32 - cx, bz as i32 - cz); let (rx, rz) = match facing & 3 { 0 => (dx, dz), 1 => (-dz, dx), 2 => (-dx, -dz), _ => (dz, -dx), }; let (px, py, pz) = (x as i32 + rx, y as i32 + by as i32, z as i32 + rz); if px >= 0 && py >= 0 && pz >= 0 { dst.set_rgba(px as u32, py as u32, pz as u32, [c[0], c[1], c[2]], c[3]); } } } } dst.clear_detail_brush(); } /// Rellena un elipsoide de semiejes `(rx,ry,rz)` centrado en `(cx,cy,cz)` con `color`. fn elipsoide(g: &mut VoxelGrid, cx: f32, cy: f32, cz: f32, rx: f32, ry: f32, rz: f32, c: [u8; 3]) { elipsoide_parcial(g, cx, cy, cz, rx, ry, rz, c, f32::NEG_INFINITY); } /// Elipsoide recortado por abajo en `y >= y_min` (para casquetes de pelo). fn elipsoide_parcial(g: &mut VoxelGrid, cx: f32, cy: f32, cz: f32, rx: f32, ry: f32, rz: f32, c: [u8; 3], y_min: f32) { let [dx, dy, dz] = g.dim(); let (x0, x1) = ((cx - rx).floor().max(0.0) as u32, (cx + rx).ceil().min(dx as f32) as u32); let (y0, y1) = ((cy - ry).floor().max(0.0) as u32, (cy + ry).ceil().min(dy as f32) as u32); let (z0, z1) = ((cz - rz).floor().max(0.0) as u32, (cz + rz).ceil().min(dz as f32) as u32); for x in x0..x1 { for y in y0..y1 { if (y as f32) < y_min { continue; } for z in z0..z1 { let dxx = (x as f32 + 0.5 - cx) / rx; let dyy = (y as f32 + 0.5 - cy) / ry; let dzz = (z as f32 + 0.5 - cz) / rz; if dxx * dxx + dyy * dyy + dzz * dzz <= 1.0 { g.set(x, y, z, c); } } } } } /// Cápsula vertical (cilindro con tapas redondeadas) de radio `r`, de `y0` a `y1` en `x,z`. fn capsula(g: &mut VoxelGrid, cx: f32, y0: f32, y1: f32, cz: f32, r: f32, c: [u8; 3]) { let [dx, dy, dz] = g.dim(); let (xa, xb) = ((cx - r).floor().max(0.0) as u32, (cx + r).ceil().min(dx as f32) as u32); let (za, zb) = ((cz - r).floor().max(0.0) as u32, (cz + r).ceil().min(dz as f32) as u32); let (ya, yb) = ((y0 - r).floor().max(0.0) as u32, (y1 + r).ceil().min(dy as f32) as u32); for x in xa..xb { for z in za..zb { let dxx = x as f32 + 0.5 - cx; let dzz = z as f32 + 0.5 - cz; let rad2 = dxx * dxx + dzz * dzz; for y in ya..yb { let fy = y as f32 + 0.5; // Distancia al segmento [y0,y1] en el eje Y (cilindro + tapas esféricas). let dyy = if fy < y0 { fy - y0 } else if fy > y1 { fy - y1 } else { 0.0 }; if rad2 + dyy * dyy <= r * r { g.set(x, y, z, c); } } } } } /// Lee el DEM (`N N min max` + N filas de N enteros en metros; fila 0 = sur) y arma un /// terreno voxel: sur (llano de El Limón) al frente (z chico), norte (montañas) al fondo. /// Color por altura: verde valle → verde bosque → roca gris; grano determinista. fn terreno_desde_dem(ruta: &str, exag: f32) -> (VoxelGrid, [u32; 3]) { let txt = std::fs::read_to_string(ruta).expect("leer DEM"); let mut lineas = txt.lines(); let cab: Vec = lineas.next().unwrap().split_whitespace().map(|s| s.parse().unwrap()).collect(); let (n, mn, mx) = (cab[0] as usize, cab[2], cab[3]); let mut dem = vec![0f32; n * n]; for (r, l) in lineas.enumerate().take(n) { for (c, v) in l.split_whitespace().enumerate().take(n) { dem[r * n + c] = v.parse().unwrap(); } } let rango = (mx - mn).max(1.0); // Mundo voxel: XZ = 4 celdas por muestra de DEM (upsample bilineal), alto dramático. let escala = 4u32; let dx = (n as u32) * escala; let dz = (n as u32) * escala; let alto_mont = (90.0 * exag) as u32; // voxels de relieve máximo let base = 3u32; let dy = base + alto_mont + 40; // sitio para el busto y cielo let mut g = VoxelGrid::new([dx, dy, dz]); let sample = |fx: f32, fz: f32| -> f32 { // fx en [0,n-1] (col=lon), fz en [0,n-1] (row=lat, 0=sur). Bilineal. let (x0, z0) = (fx.floor(), fz.floor()); let (tx, tz) = (fx - x0, fz - z0); let (x0, z0) = (x0 as usize, z0 as usize); let x1 = (x0 + 1).min(n - 1); let z1 = (z0 + 1).min(n - 1); let a = dem[z0 * n + x0]; let b = dem[z0 * n + x1]; let c = dem[z1 * n + x0]; let d = dem[z1 * n + x1]; let ab = a + (b - a) * tx; let cd = c + (d - c) * tx; ab + (cd - ab) * tz }; for gz in 0..dz { for gx in 0..dx { let fx = gx as f32 / escala as f32; let fz = gz as f32 / escala as f32; let e = sample(fx.min((n - 1) as f32), fz.min((n - 1) as f32)); let t = ((e - mn) / rango).clamp(0.0, 1.0); let top = base + (t * alto_mont as f32) as u32; for y in 0..=top.min(dy - 1) { let th = y as f32 / (base + alto_mont) as f32; let col = color_altura(th, gx, y, gz); g.set(gx, y, gz, col); } } } g.reset_dirty(); (g, [dx, dy, dz]) } /// Altura de la columna sólida `(gx,gz)` (el primer vacío desde arriba). fn altura_terreno(g: &VoxelGrid, gx: u32, gz: u32) -> u32 { let [_, dy, _] = g.dim(); for y in (0..dy).rev() { if g.get(gx, y, gz).map(|c| c[3] > 127).unwrap_or(false) { return y; } } 0 } /// Color del terreno por altura normalizada `[0,1]` (valle verde → bosque → roca) con /// grano determinista para textura. fn color_altura(t: f32, x: u32, y: u32, z: u32) -> [u8; 3] { let base = if t < 0.14 { [86, 132, 72] // valle tropical (El Limón, verde) } else if t < 0.45 { let k = (t - 0.14) / 0.31; mezcla([86, 132, 72], [64, 104, 60], k) // bosque de ladera (cloud forest) } else if t < 0.75 { let k = (t - 0.45) / 0.30; mezcla([64, 104, 60], [110, 100, 86], k) // transición a roca } else { let k = (t - 0.75) / 0.25; mezcla([110, 100, 86], [150, 146, 140], k) // roca alta gris }; let h = hash(x, y, z); let j = (h - 0.5) * 2.0 * 0.10; // ±10% de brillo let ch = |c: u8| ((c as f32) * (1.0 + j)).clamp(0.0, 255.0) as u8; [ch(base[0]), ch(base[1]), ch(base[2])] } fn mezcla(a: [u8; 3], b: [u8; 3], t: f32) -> [u8; 3] { let m = |i: usize| (a[i] as f32 + (b[i] as f32 - a[i] as f32) * t) as u8; [m(0), m(1), m(2)] } fn hash(x: u32, y: u32, z: u32) -> f32 { let mut h = x.wrapping_mul(374761393).wrapping_add(y.wrapping_mul(668265263)).wrapping_add(z.wrapping_mul(2246822519)); h ^= h >> 13; h = h.wrapping_mul(1274126177); (h & 0xffff) as f32 / 65535.0 } fn normalize(v: [f32; 3]) -> [f32; 3] { let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6); [v[0] / l, v[1] / l, v[2] / l] } fn render(hal: &Hal, renderer: &mut Renderer, vr: &mut VoxelRenderer, cam: &Camera3d) -> Vec { let bg = [176, 190, 205]; let inter = hal.device.create_texture(&wgpu::TextureDescriptor { label: Some("inter"), 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 = inter.create_view(&wgpu::TextureViewDescriptor::default()); let base = vello::Scene::new(); renderer .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(bg[0], bg[1], bg[2], 255)) .expect("base"); let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), cam); hal.queue.submit(std::iter::once(enc.finish())); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); readback(hal, &inter) } fn readback(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 { let s = row * padded; px.extend_from_slice(&data[s..s + unpadded]); } drop(data); buf.unmap(); px } fn write_png(px: &[u8], path: &str) { let f = std::fs::File::create(path).expect("png"); let mut enc = png::Encoder::new(BufWriter::new(f), W, H); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); let mut w = enc.write_header().unwrap(); w.write_image_data(px).unwrap(); }