tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}