tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-voxel-studio/examples/demo_elimon.rs Rust · 467 siq'ikuna · 21.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! **Demo end-to-end: «un personaje desde foto, en El Limón mirando las montañas».**
2//!
3//! La foto NO se voxeliza literal (eso da el valle de inquietud). Corre así:
4//!   1. Recorta la persona de la foto (segmentación u2net por el daemon pixel-verbo).
5//!   2. Muestrea sus RASGOS de color (piel arriba, ropa abajo) — nada de geometría.
6//!   3. Arma un **personaje ESTILIZADO** del kit (humanoide voxel blocky) con esos
7//!      colores: su estilo es propio, la foto sólo regula piel/ropa.
8//!   4. Arma el terreno voxel desde un **DEM real** de El Limón/Maracay (SRTM): el llano
9//!      al sur, la Cordillera de la Costa (Henri Pittier) al norte.
10//!   5. Planta el personaje en el llano (`stamp_busto`) y rinde una toma INMERSIVA: el
11//!      personaje en primer plano, las montañas de fondo, luz cálida y bruma.
12//!
13//! Socket por env: `SEG_SOCK` (u2net). Foto/DEM/salida por args. Perillas de cámara y
14//! escala por env: `CAM_Z`, `CAM_H`, `CAM_PITCH`, `CAM_YAW`, `BUST_Z`, `BUST_X`, `PERS_H`, `EXAG`.
15//!
16//! `cargo run -p llimphi-voxel-studio --example demo_elimon --release -- <foto> <dem> <out.png>`
17
18use std::io::BufWriter;
19
20use llimphi_3d::glam::Vec3;
21use llimphi_3d::{Atmosphere, Camera3d, VoxelGrid, VoxelRenderer};
22use llimphi_hal::{wgpu, Hal};
23use llimphi_raster::peniko::Color;
24use llimphi_raster::{vello, Renderer};
25use pixel_verbo_core::{Imagen, OpPixel, Proveedor};
26use pixel_verbo_daemon::ClienteBloqueante;
27
28const W: u32 = 1280;
29const H: u32 = 720;
30const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
31
32fn env_f(k: &str, d: f32) -> f32 {
33    std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d)
34}
35
36fn main() {
37    let args: Vec<String> = std::env::args().collect();
38    let foto = args.get(1).cloned().unwrap_or_default();
39    let dem = args.get(2).cloned().unwrap_or_default();
40    let out = args.get(3).cloned().unwrap_or_else(|| "/tmp/elimon.png".into());
41
42    // --- Foto → RGBA (downscale para socket + busto) ---
43    let img = image::open(&foto).expect("abrir foto").to_rgba8();
44    let ancho_proc = 384u32;
45    let alto_proc = (img.height() * ancho_proc / img.width()).max(1);
46    let chica = image::imageops::resize(&img, ancho_proc, alto_proc, image::imageops::FilterType::Lanczos3);
47    let (pw, ph) = (chica.width(), chica.height());
48    let rgba = chica.into_raw();
49
50    // --- 1. Recorte de la persona (u2net por daemon) → alpha ---
51    let seg_sock = std::env::var("SEG_SOCK").expect("SEG_SOCK");
52    let seg = ClienteBloqueante::conectar(&seg_sock).expect("conectar u2net");
53    let recorte = seg
54        .aplicar(&OpPixel::Segmentar { prompt: None }, Some(Imagen::nueva(pw, ph, rgba.clone()).unwrap()))
55        .expect("segmentar");
56    let alpha: Vec<u8> = recorte.bytes.chunks_exact(4).map(|p| p[3]).collect();
57    eprintln!("recorte: {}x{} (alpha = persona)", recorte.ancho, recorte.alto);
58
59    // --- 2. RASGOS de color (piel arriba, ropa abajo) — nada de geometría de la foto ---
60    let (skin, shirt, pants) = rasgos(&rgba, &alpha, pw, ph);
61    eprintln!("rasgos: piel {skin:?}  ropa {shirt:?}");
62
63    // --- 3. Personaje ESTILIZADO del kit (humanoide voxel), coloreado por los rasgos ---
64    let pers_h = env_f("PERS_H", 46.0) as u32;
65    let personaje = personaje_voxel(pers_h, skin, shirt, pants);
66    eprintln!("personaje estilizado: dim {:?}", personaje.dim());
67
68    // --- 4. Terreno desde el DEM real de El Limón/Maracay ---
69    let exag = env_f("EXAG", 1.0);
70    let (mut grid, dim) = terreno_desde_dem(&dem, exag);
71
72    // --- 5. Plantar el personaje en el llano (sur), mirando al sur (a cámara) ---
73    let bx = env_f("BUST_X", (dim[0] as f32) * 0.5) as u32;
74    let bz = env_f("BUST_Z", (dim[2] as f32) * 0.24) as u32;
75    let by = altura_terreno(&grid, bx, bz);
76    // facing 2 = el frente del personaje (+Z) queda hacia −Z = hacia la cámara.
77    plantar(&mut grid, &personaje, bx, by + 1, bz, 2);
78    eprintln!("personaje plantado en ({bx},{},{bz}) del mundo {:?}", by + 1, dim);
79
80    // --- Render inmersivo ---
81    let hal = pollster::block_on(Hal::new(None)).expect("hal");
82    let mut renderer = Renderer::new(&hal).expect("renderer");
83    let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
84    // Look sub-voxel cinematográfico: montañas suaves + busto redondeado (bisel), con
85    // grano de terreno; silueta por material para que el busto (bisel autorado) redondee.
86    vr.bisel = 0.85;
87    vr.rugosidad_amp = 0.5;
88    vr.rugosidad_freq = 1.7;
89    vr.silueta_por_voxel = true;
90    // Atardecer tropical: sol cálido bajo desde el oeste, cielo azul, bruma para dar aire.
91    vr.sun_dir = normalize([0.5, 0.55, -0.35]);
92    vr.atmosphere = Atmosphere {
93        sky_zenith: [58, 104, 176],
94        sky_horizon: [206, 196, 168],
95        fog_density: env_f("FOG", 0.010),
96        god_rays: 0.5,
97    };
98
99    // Cámara: parada en el llano sur, a la altura de los ojos, mirando al norte (+Z) a
100    // las montañas; el busto queda en primer plano. Todo en coords de mundo (grid
101    // centrado en el origen: voxel (gx,gy,gz) ↦ (gx,gy,gz) − dim/2).
102    let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
103    let cam_z = env_f("CAM_Z", bz as f32 - 34.0);
104    let cam_x = env_f("CAM_X", bx as f32 + 6.0);
105    let suelo_cam = altura_terreno(&grid, cam_x as u32, cam_z.max(0.0) as u32) as f32;
106    let cam_h = suelo_cam + env_f("CAM_H", 30.0);
107    let eye = Vec3::new(cam_x, cam_h, cam_z) - half;
108    let cam = Camera3d::fly(eye, env_f("CAM_YAW", 0.0).to_radians(), env_f("CAM_PITCH", 6.0).to_radians());
109
110    let px = render(&hal, &mut renderer, &mut vr, &cam);
111    write_png(&px, &out);
112    eprintln!("✓ toma inmersiva escrita: {out}");
113}
114
115/// Muestrea los RASGOS de color de la persona recortada (`alpha>128`): piel cálida del
116/// tercio superior del bbox, ropa de la mitad inferior; pantalón derivado (más oscuro).
117/// Devuelve `[u8;3]` para colorear los voxels del personaje. NO toca geometría.
118fn rasgos(rgba: &[u8], alpha: &[u8], w: u32, h: u32) -> ([u8; 3], [u8; 3], [u8; 3]) {
119    let (w, h) = (w as usize, h as usize);
120    let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);
121    for y in 0..h {
122        for x in 0..w {
123            if alpha[y * w + x] > 128 {
124                x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y);
125            }
126        }
127    }
128    let bh = (y1.saturating_sub(y0)).max(1);
129    // Piel: cálidos del tercio superior.
130    let (mut sr, mut sg, mut sb, mut sn) = (0u64, 0u64, 0u64, 0u64);
131    for y in y0..(y0 + bh * 45 / 100).min(h) {
132        for x in x0..=x1.min(w - 1) {
133            if alpha[y * w + x] <= 128 { continue; }
134            let o = (y * w + x) * 4;
135            let (r, g, b) = (rgba[o] as i32, rgba[o + 1] as i32, rgba[o + 2] as i32);
136            if r > g && g >= b && (r - b) > 12 && (r - b) < 130 && (r + g + b) > 150 {
137                sr += r as u64; sg += g as u64; sb += b as u64; sn += 1;
138            }
139        }
140    }
141    let skin = if sn > 30 { [(sr / sn) as u8, (sg / sn) as u8, (sb / sn) as u8] } else { [216, 172, 138] };
142    // Ropa: promedio de la mitad inferior.
143    let (mut cr, mut cg, mut cb, mut cn) = (0u64, 0u64, 0u64, 0u64);
144    for y in (y0 + bh / 2)..=y1.min(h - 1) {
145        for x in x0..=x1.min(w - 1) {
146            if alpha[y * w + x] <= 128 { continue; }
147            let o = (y * w + x) * 4;
148            cr += rgba[o] as u64; cg += rgba[o + 1] as u64; cb += rgba[o + 2] as u64; cn += 1;
149        }
150    }
151    let shirt = if cn > 0 { [(cr / cn) as u8, (cg / cn) as u8, (cb / cn) as u8] } else { [70, 120, 150] };
152    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];
153    (skin, shirt, pants)
154}
155
156/// **Personaje ESTILIZADO del kit**: NO cajas tipo Minecraft — formas ORGÁNICAS
157/// (elipsoides/cápsulas) en proporciones chibi expresivas (cabezón, cuerpo redondo,
158/// miembros rechonchos), coloreadas por los rasgos de la foto. Se rinde con bisel alto:
159/// el motor sub-voxel traza la isosuperficie suave y lo convierte en una figura
160/// redondeada tipo vinyl-toy/Fall-Guys, con carácter. Mira a +Z (ojos grandes al frente).
161/// Cada voxel lleva su detalle `(rugosidad, bisel)` para que la silueta salga lisa.
162fn personaje_voxel(h: u32, skin: [u8; 3], shirt: [u8; 3], pants: [u8; 3]) -> VoxelGrid {
163    let h = h.max(28) as f32;
164    // Proporciones chibi (cabezón simpático) pero con cuerpo presente.
165    let head_r = h * 0.205;
166    let body_rx = h * 0.20;
167    let body_ry = h * 0.22;
168    let body_rz = h * 0.17;
169    let limb_r = h * 0.072;
170    let w = (body_rx.max(head_r) * 2.0 + limb_r * 4.0 + 6.0) as u32;
171    let depth = (body_rz.max(head_r) * 2.0 + limb_r * 2.0 + 4.0) as u32;
172    let gy = (h + 4.0) as u32;
173    let mut g = VoxelGrid::new([w, gy, depth]);
174    let (cx, cz) = (w as f32 / 2.0, depth as f32 / 2.0);
175
176    let leg_cy = h * 0.12;
177    let body_cy = h * 0.42;
178    let head_cy = h * 0.78;
179    let pelo = [46u8, 36, 32];
180
181    // Piernas: dos cápsulas cortas y rechonchas.
182    capsula(&mut g, cx - body_rx * 0.42, leg_cy - h * 0.11, leg_cy + h * 0.11, cz, limb_r, pants);
183    capsula(&mut g, cx + body_rx * 0.42, leg_cy - h * 0.11, leg_cy + h * 0.11, cz, limb_r, pants);
184    // Brazos: cápsulas pegadas al cuerpo, caídas (no en cruz).
185    capsula(&mut g, cx - body_rx - limb_r * 0.3, body_cy - h * 0.13, body_cy + h * 0.05, cz, limb_r, shirt);
186    capsula(&mut g, cx + body_rx + limb_r * 0.3, body_cy - h * 0.13, body_cy + h * 0.05, cz, limb_r, shirt);
187    // Cuerpo: elipsoide redondo (pancita).
188    elipsoide(&mut g, cx, body_cy, cz, body_rx, body_ry, body_rz, shirt);
189    // Cabeza: elipsoide grande.
190    elipsoide(&mut g, cx, head_cy, cz, head_r, head_r * 1.04, head_r * 0.98, skin);
191    // Pelo: casquete oscuro sobre la mitad superior de la cabeza (centrado, sin tapar ojos).
192    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);
193    // Ojos: dos esferas oscuras grandes al frente (+Z) con brillo blanco → expresividad.
194    let zf = cz + head_r * 0.80;
195    let ex = head_r * 0.40;
196    let ey = head_cy + head_r * 0.02;
197    let er = head_r * 0.22;
198    for s in [-1.0f32, 1.0] {
199        elipsoide(&mut g, cx + s * ex, ey, zf, er, er * 1.15, er * 0.6, [26, 22, 28]);
200        // Chispa de brillo (arriba del ojo).
201        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]);
202    }
203    // Boca: pequeña curva oscura bajo los ojos.
204    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]);
205
206    g.reset_dirty();
207    g
208}
209
210/// Planta el personaje en `dst` con la base en `(x,y,z)`, rotado por `facing`, y con un
211/// detalle sub-voxel de **bisel alto** por voxel (silueta lisa/orgánica) — así el motor lo
212/// redondea en vez de dejarlo cúbico. Espeja `stamp_busto` pero con NUESTRO detalle.
213fn plantar(dst: &mut VoxelGrid, src: &VoxelGrid, x: u32, y: u32, z: u32, facing: u8) {
214    let [bw, bh, bd] = src.dim();
215    let (cx, cz) = (bw as i32 / 2, bd as i32 / 2);
216    dst.set_detail_brush(0.2, 1.0); // rugosidad leve (piel) + bisel máximo (redondeo)
217    for by in 0..bh {
218        for bz in 0..bd {
219            for bx in 0..bw {
220                let c = match src.get(bx, by, bz) {
221                    Some(c) if c[3] > 127 => c,
222                    _ => continue,
223                };
224                let (dx, dz) = (bx as i32 - cx, bz as i32 - cz);
225                let (rx, rz) = match facing & 3 {
226                    0 => (dx, dz),
227                    1 => (-dz, dx),
228                    2 => (-dx, -dz),
229                    _ => (dz, -dx),
230                };
231                let (px, py, pz) = (x as i32 + rx, y as i32 + by as i32, z as i32 + rz);
232                if px >= 0 && py >= 0 && pz >= 0 {
233                    dst.set_rgba(px as u32, py as u32, pz as u32, [c[0], c[1], c[2]], c[3]);
234                }
235            }
236        }
237    }
238    dst.clear_detail_brush();
239}
240
241/// Rellena un elipsoide de semiejes `(rx,ry,rz)` centrado en `(cx,cy,cz)` con `color`.
242fn elipsoide(g: &mut VoxelGrid, cx: f32, cy: f32, cz: f32, rx: f32, ry: f32, rz: f32, c: [u8; 3]) {
243    elipsoide_parcial(g, cx, cy, cz, rx, ry, rz, c, f32::NEG_INFINITY);
244}
245
246/// Elipsoide recortado por abajo en `y >= y_min` (para casquetes de pelo).
247fn elipsoide_parcial(g: &mut VoxelGrid, cx: f32, cy: f32, cz: f32, rx: f32, ry: f32, rz: f32, c: [u8; 3], y_min: f32) {
248    let [dx, dy, dz] = g.dim();
249    let (x0, x1) = ((cx - rx).floor().max(0.0) as u32, (cx + rx).ceil().min(dx as f32) as u32);
250    let (y0, y1) = ((cy - ry).floor().max(0.0) as u32, (cy + ry).ceil().min(dy as f32) as u32);
251    let (z0, z1) = ((cz - rz).floor().max(0.0) as u32, (cz + rz).ceil().min(dz as f32) as u32);
252    for x in x0..x1 {
253        for y in y0..y1 {
254            if (y as f32) < y_min {
255                continue;
256            }
257            for z in z0..z1 {
258                let dxx = (x as f32 + 0.5 - cx) / rx;
259                let dyy = (y as f32 + 0.5 - cy) / ry;
260                let dzz = (z as f32 + 0.5 - cz) / rz;
261                if dxx * dxx + dyy * dyy + dzz * dzz <= 1.0 {
262                    g.set(x, y, z, c);
263                }
264            }
265        }
266    }
267}
268
269/// Cápsula vertical (cilindro con tapas redondeadas) de radio `r`, de `y0` a `y1` en `x,z`.
270fn capsula(g: &mut VoxelGrid, cx: f32, y0: f32, y1: f32, cz: f32, r: f32, c: [u8; 3]) {
271    let [dx, dy, dz] = g.dim();
272    let (xa, xb) = ((cx - r).floor().max(0.0) as u32, (cx + r).ceil().min(dx as f32) as u32);
273    let (za, zb) = ((cz - r).floor().max(0.0) as u32, (cz + r).ceil().min(dz as f32) as u32);
274    let (ya, yb) = ((y0 - r).floor().max(0.0) as u32, (y1 + r).ceil().min(dy as f32) as u32);
275    for x in xa..xb {
276        for z in za..zb {
277            let dxx = x as f32 + 0.5 - cx;
278            let dzz = z as f32 + 0.5 - cz;
279            let rad2 = dxx * dxx + dzz * dzz;
280            for y in ya..yb {
281                let fy = y as f32 + 0.5;
282                // Distancia al segmento [y0,y1] en el eje Y (cilindro + tapas esféricas).
283                let dyy = if fy < y0 { fy - y0 } else if fy > y1 { fy - y1 } else { 0.0 };
284                if rad2 + dyy * dyy <= r * r {
285                    g.set(x, y, z, c);
286                }
287            }
288        }
289    }
290}
291
292/// Lee el DEM (`N N min max` + N filas de N enteros en metros; fila 0 = sur) y arma un
293/// terreno voxel: sur (llano de El Limón) al frente (z chico), norte (montañas) al fondo.
294/// Color por altura: verde valle → verde bosque → roca gris; grano determinista.
295fn terreno_desde_dem(ruta: &str, exag: f32) -> (VoxelGrid, [u32; 3]) {
296    let txt = std::fs::read_to_string(ruta).expect("leer DEM");
297    let mut lineas = txt.lines();
298    let cab: Vec<f32> = lineas.next().unwrap().split_whitespace().map(|s| s.parse().unwrap()).collect();
299    let (n, mn, mx) = (cab[0] as usize, cab[2], cab[3]);
300    let mut dem = vec![0f32; n * n];
301    for (r, l) in lineas.enumerate().take(n) {
302        for (c, v) in l.split_whitespace().enumerate().take(n) {
303            dem[r * n + c] = v.parse().unwrap();
304        }
305    }
306    let rango = (mx - mn).max(1.0);
307
308    // Mundo voxel: XZ = 4 celdas por muestra de DEM (upsample bilineal), alto dramático.
309    let escala = 4u32;
310    let dx = (n as u32) * escala;
311    let dz = (n as u32) * escala;
312    let alto_mont = (90.0 * exag) as u32; // voxels de relieve máximo
313    let base = 3u32;
314    let dy = base + alto_mont + 40; // sitio para el busto y cielo
315    let mut g = VoxelGrid::new([dx, dy, dz]);
316
317    let sample = |fx: f32, fz: f32| -> f32 {
318        // fx en [0,n-1] (col=lon), fz en [0,n-1] (row=lat, 0=sur). Bilineal.
319        let (x0, z0) = (fx.floor(), fz.floor());
320        let (tx, tz) = (fx - x0, fz - z0);
321        let (x0, z0) = (x0 as usize, z0 as usize);
322        let x1 = (x0 + 1).min(n - 1);
323        let z1 = (z0 + 1).min(n - 1);
324        let a = dem[z0 * n + x0];
325        let b = dem[z0 * n + x1];
326        let c = dem[z1 * n + x0];
327        let d = dem[z1 * n + x1];
328        let ab = a + (b - a) * tx;
329        let cd = c + (d - c) * tx;
330        ab + (cd - ab) * tz
331    };
332
333    for gz in 0..dz {
334        for gx in 0..dx {
335            let fx = gx as f32 / escala as f32;
336            let fz = gz as f32 / escala as f32;
337            let e = sample(fx.min((n - 1) as f32), fz.min((n - 1) as f32));
338            let t = ((e - mn) / rango).clamp(0.0, 1.0);
339            let top = base + (t * alto_mont as f32) as u32;
340            for y in 0..=top.min(dy - 1) {
341                let th = y as f32 / (base + alto_mont) as f32;
342                let col = color_altura(th, gx, y, gz);
343                g.set(gx, y, gz, col);
344            }
345        }
346    }
347    g.reset_dirty();
348    (g, [dx, dy, dz])
349}
350
351/// Altura de la columna sólida `(gx,gz)` (el primer vacío desde arriba).
352fn altura_terreno(g: &VoxelGrid, gx: u32, gz: u32) -> u32 {
353    let [_, dy, _] = g.dim();
354    for y in (0..dy).rev() {
355        if g.get(gx, y, gz).map(|c| c[3] > 127).unwrap_or(false) {
356            return y;
357        }
358    }
359    0
360}
361
362/// Color del terreno por altura normalizada `[0,1]` (valle verde → bosque → roca) con
363/// grano determinista para textura.
364fn color_altura(t: f32, x: u32, y: u32, z: u32) -> [u8; 3] {
365    let base = if t < 0.14 {
366        [86, 132, 72] // valle tropical (El Limón, verde)
367    } else if t < 0.45 {
368        let k = (t - 0.14) / 0.31;
369        mezcla([86, 132, 72], [64, 104, 60], k) // bosque de ladera (cloud forest)
370    } else if t < 0.75 {
371        let k = (t - 0.45) / 0.30;
372        mezcla([64, 104, 60], [110, 100, 86], k) // transición a roca
373    } else {
374        let k = (t - 0.75) / 0.25;
375        mezcla([110, 100, 86], [150, 146, 140], k) // roca alta gris
376    };
377    let h = hash(x, y, z);
378    let j = (h - 0.5) * 2.0 * 0.10; // ±10% de brillo
379    let ch = |c: u8| ((c as f32) * (1.0 + j)).clamp(0.0, 255.0) as u8;
380    [ch(base[0]), ch(base[1]), ch(base[2])]
381}
382
383fn mezcla(a: [u8; 3], b: [u8; 3], t: f32) -> [u8; 3] {
384    let m = |i: usize| (a[i] as f32 + (b[i] as f32 - a[i] as f32) * t) as u8;
385    [m(0), m(1), m(2)]
386}
387
388fn hash(x: u32, y: u32, z: u32) -> f32 {
389    let mut h = x.wrapping_mul(374761393).wrapping_add(y.wrapping_mul(668265263)).wrapping_add(z.wrapping_mul(2246822519));
390    h ^= h >> 13;
391    h = h.wrapping_mul(1274126177);
392    (h & 0xffff) as f32 / 65535.0
393}
394
395fn normalize(v: [f32; 3]) -> [f32; 3] {
396    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
397    [v[0] / l, v[1] / l, v[2] / l]
398}
399
400fn render(hal: &Hal, renderer: &mut Renderer, vr: &mut VoxelRenderer, cam: &Camera3d) -> Vec<u8> {
401    let bg = [176, 190, 205];
402    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
403        label: Some("inter"),
404        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
405        mip_level_count: 1,
406        sample_count: 1,
407        dimension: wgpu::TextureDimension::D2,
408        format: FMT,
409        usage: wgpu::TextureUsages::STORAGE_BINDING
410            | wgpu::TextureUsages::TEXTURE_BINDING
411            | wgpu::TextureUsages::RENDER_ATTACHMENT
412            | wgpu::TextureUsages::COPY_SRC,
413        view_formats: &[],
414    });
415    let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
416    let base = vello::Scene::new();
417    renderer
418        .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(bg[0], bg[1], bg[2], 255))
419        .expect("base");
420    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
421    vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), cam);
422    hal.queue.submit(std::iter::once(enc.finish()));
423    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
424    readback(hal, &inter)
425}
426
427fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
428    let unpadded = (W * 4) as usize;
429    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
430    let padded = unpadded.div_ceil(align) * align;
431    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
432        label: None,
433        size: (padded * H as usize) as u64,
434        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
435        mapped_at_creation: false,
436    });
437    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
438    enc.copy_texture_to_buffer(
439        wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
440        wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } },
441        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
442    );
443    hal.queue.submit(std::iter::once(enc.finish()));
444    let slice = buf.slice(..);
445    let (tx, rx) = std::sync::mpsc::channel();
446    slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
447    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
448    rx.recv().unwrap().unwrap();
449    let data = slice.get_mapped_range();
450    let mut px = Vec::with_capacity((W * H * 4) as usize);
451    for row in 0..H as usize {
452        let s = row * padded;
453        px.extend_from_slice(&data[s..s + unpadded]);
454    }
455    drop(data);
456    buf.unmap();
457    px
458}
459
460fn write_png(px: &[u8], path: &str) {
461    let f = std::fs::File::create(path).expect("png");
462    let mut enc = png::Encoder::new(BufWriter::new(f), W, H);
463    enc.set_color(png::ColorType::Rgba);
464    enc.set_depth(png::BitDepth::Eight);
465    let mut w = enc.write_header().unwrap();
466    w.write_image_data(px).unwrap();
467}