tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! Certificación de la **capa 3**: una foto real → **busto voxel** estilizado
2//! (`photo_to_grid`), renderizado con el kit sub-voxel (bisel) para que se lea como
3//! parte del mundo de bloques y no como una malla fotorreal — la cuantización es lo
4//! que lo saca del valle de inquietud.
5//!
6//! Genera un retrato sintético (con recorte alpha) → busto → render frontal. Certifica
7//! por STATS (recorte, relieve, fidelidad de color). Si existe `pluma.png` en el cwd,
8//! también lo voxeliza y rinde (imagen real, a modo de muestra).
9//!
10//! `cargo run -p llimphi-voxel --example foto_ser --release -- [imagen.png]`
11
12use std::fs::File;
13use std::io::{BufReader, BufWriter};
14
15use llimphi_3d::glam::Vec3;
16use llimphi_3d::{Camera3d, VoxelGrid, VoxelRenderer};
17use llimphi_hal::{wgpu, Hal};
18use llimphi_raster::peniko::Color;
19use llimphi_raster::{vello, Renderer};
20use llimphi_voxel::{photo_to_grid, FotoOpts, Relieve};
21
22const W: u32 = 720;
23const H: u32 = 720;
24const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25const BG: [u8; 3] = [16, 18, 26];
26
27fn main() {
28 let hal = pollster::block_on(Hal::new(None)).expect("hal");
29 let mut renderer = Renderer::new(&hal).expect("renderer");
30 let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into());
31
32 // --- Certificación: retrato sintético con recorte alpha ---
33 let (rw, rh) = (96u32, 112u32);
34 let img = retrato(rw, rh);
35 let opts = FotoOpts { ancho: 64, profundidad: 16, relieve: Relieve::Domo, alpha_min: 128, grano: 0.05 };
36 let (grid, s) = photo_to_grid(&img, rw, rh, &opts);
37
38 println!("foto_ser — retrato sintético {rw}x{rh} → busto voxel");
39 println!(" grilla: {:?}", s.dim);
40 println!(" recorte (foreground): {} px de {}", s.recorte, s.dim[0] * s.dim[1]);
41 println!(" voxels colocados: {}", s.voxeles);
42 println!(" grosor del relieve: {}..{} voxels (domo)", s.grosor_min, s.grosor_max);
43 println!(" color medio: {:?}", s.color_medio);
44
45 // Render frontal con bisel (compone con la capa 2b) + rugosidad leve.
46 let px = render_bust(&hal, &mut renderer, &grid, s.dim, 0.65, 0.25);
47 write_png(&px, &format!("{dir}/foto_ser.png"));
48 let covered = coverage(&px);
49 println!(" render: {covered} px cubiertos → {dir}/foto_ser.png");
50
51 // Asserts de la certificación (los detalles finos ya están en los unit tests).
52 assert!(s.recorte > 0 && (s.recorte as f32) < (s.dim[0] * s.dim[1]) as f32 * 0.9, "recortó el fondo");
53 assert!(s.grosor_max > s.grosor_min && s.grosor_max >= 12, "hay relieve de domo ({}..{})", s.grosor_min, s.grosor_max);
54 assert!(s.color_medio[0] > s.color_medio[2], "conserva el tono cálido de la piel: {:?}", s.color_medio);
55 assert!(covered > 5000, "el busto se renderizó (cubrió {covered} px)");
56
57 // --- Muestra opcional: imagen real (pluma.png) si está en el cwd ---
58 let arg = std::env::args().nth(1);
59 let ruta = arg.as_deref().unwrap_or("pluma.png");
60 match load_png_rgba(ruta) {
61 Some((rgba, w, h)) => {
62 // Sin alpha real: recorte por luminancia (fondo claro fuera). alpha_min=1
63 // deja pasar casi todo; el relieve por luminancia da volumen a la imagen.
64 let o = FotoOpts { ancho: 72, profundidad: 14, relieve: Relieve::Luminancia, alpha_min: 1, grano: 0.05 };
65 let (g2, s2) = photo_to_grid(&rgba, w, h, &o);
66 let p2 = render_bust(&hal, &mut renderer, &g2, s2.dim, 0.6, 0.2);
67 write_png(&p2, &format!("{dir}/foto_ser_real.png"));
68 println!("imagen real '{ruta}' {w}x{h} → {} voxels → {dir}/foto_ser_real.png", s2.voxeles);
69 }
70 None => println!("(sin imagen real en '{ruta}' — omitido)"),
71 }
72
73 println!("✓ certificado: foto → busto voxel con recorte, relieve y color del original.");
74}
75
76/// Retrato sintético RGBA con recorte alpha: pelo + óvalo de piel + ojos + boca.
77fn retrato(w: u32, h: u32) -> Vec<u8> {
78 let cx = w as f32 / 2.0;
79 let cy = h as f32 * 0.52;
80 let rx = w as f32 * 0.34;
81 let ry = h as f32 * 0.42;
82 let mut px = vec![0u8; (w * h * 4) as usize];
83 for y in 0..h {
84 for x in 0..w {
85 let i = ((y * w + x) * 4) as usize;
86 let nx = (x as f32 - cx) / rx;
87 let ny = (y as f32 - cy) / ry;
88 if nx * nx + ny * ny > 1.0 {
89 continue; // fuera del óvalo → transparente
90 }
91 // Piel cálida con leve sombreado vertical.
92 let sh = 1.0 - 0.15 * ny;
93 let mut c = [(228.0 * sh) as u8, (186.0 * sh) as u8, (150.0 * sh) as u8];
94 // Pelo: banda superior.
95 if ny < -0.45 {
96 c = [92, 62, 44];
97 }
98 // Ojos: dos elipses oscuras.
99 for ex in [-0.42f32, 0.42] {
100 let ox = (x as f32 - (cx + ex * rx)) / (rx * 0.16);
101 let oy = (y as f32 - (cy - 0.12 * ry)) / (ry * 0.10);
102 if ox * ox + oy * oy < 1.0 {
103 c = [40, 32, 30];
104 }
105 }
106 // Boca: arco oscuro.
107 let mox = (x as f32 - cx) / (rx * 0.42);
108 let moy = (y as f32 - (cy + 0.5 * ry)) / (ry * 0.08);
109 if mox * mox + moy * moy < 1.0 {
110 c = [150, 70, 66];
111 }
112 px[i] = c[0];
113 px[i + 1] = c[1];
114 px[i + 2] = c[2];
115 px[i + 3] = 255;
116 }
117 }
118 px
119}
120
121/// Renderiza un busto voxel centrado, de frente, con `bisel`/`rugosidad` dados.
122fn render_bust(hal: &Hal, renderer: &mut Renderer, grid: &VoxelGrid, dim: [u32; 3], bisel: f32, rug: f32) -> Vec<u8> {
123 let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, grid);
124 vr.bisel = bisel;
125 vr.rugosidad_amp = rug;
126 vr.sun_dir = normalize([0.35, 0.55, 0.75]); // luz desde el frente-arriba
127 // El relieve sobresale hacia +Z → cámara al frente (mirando -Z), leve picado.
128 let alto = dim[1].max(dim[0]) as f32;
129 let camera = Camera3d::orbit(Vec3::ZERO, 0.0, 8_f32.to_radians(), alto * 1.9);
130
131 let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
132 label: Some("inter"),
133 size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
134 mip_level_count: 1,
135 sample_count: 1,
136 dimension: wgpu::TextureDimension::D2,
137 format: FMT,
138 usage: wgpu::TextureUsages::STORAGE_BINDING
139 | wgpu::TextureUsages::TEXTURE_BINDING
140 | wgpu::TextureUsages::RENDER_ATTACHMENT
141 | wgpu::TextureUsages::COPY_SRC,
142 view_formats: &[],
143 });
144 let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
145 let base = vello::Scene::new();
146 renderer
147 .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(BG[0], BG[1], BG[2], 255))
148 .expect("base");
149 let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
150 vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), &camera);
151 hal.queue.submit(std::iter::once(enc.finish()));
152 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
153 readback(hal, &inter)
154}
155
156fn normalize(v: [f32; 3]) -> [f32; 3] {
157 let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
158 [v[0] / l, v[1] / l, v[2] / l]
159}
160
161fn coverage(px: &[u8]) -> usize {
162 (0..(W * H) as usize)
163 .filter(|&i| {
164 let o = i * 4;
165 (px[o] as i32 - BG[0] as i32).abs()
166 + (px[o + 1] as i32 - BG[1] as i32).abs()
167 + (px[o + 2] as i32 - BG[2] as i32).abs()
168 > 12
169 })
170 .count()
171}
172
173/// Decodifica un PNG a RGBA `w×h`. `None` si no existe o no se pudo leer.
174fn load_png_rgba(path: &str) -> Option<(Vec<u8>, u32, u32)> {
175 let file = File::open(path).ok()?;
176 let dec = png::Decoder::new(BufReader::new(file));
177 let mut reader = dec.read_info().ok()?;
178 let mut buf = vec![0u8; reader.output_buffer_size()?];
179 let info = reader.next_frame(&mut buf).ok()?;
180 let (w, h) = (info.width, info.height);
181 let rgba = match info.color_type {
182 png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
183 png::ColorType::Rgb => {
184 let mut out = Vec::with_capacity((w * h * 4) as usize);
185 for c in buf[..info.buffer_size()].chunks(3) {
186 out.extend_from_slice(&[c[0], c[1], c[2], 255]);
187 }
188 out
189 }
190 png::ColorType::Grayscale => {
191 let mut out = Vec::with_capacity((w * h * 4) as usize);
192 for &g in &buf[..info.buffer_size()] {
193 out.extend_from_slice(&[g, g, g, 255]);
194 }
195 out
196 }
197 _ => return None, // paleta/gris-alfa: fuera de alcance del demo
198 };
199 Some((rgba, w, h))
200}
201
202fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
203 let unpadded = (W * 4) as usize;
204 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
205 let padded = unpadded.div_ceil(align) * align;
206 let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
207 label: Some("readback"),
208 size: (padded * H as usize) as u64,
209 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
210 mapped_at_creation: false,
211 });
212 let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
213 enc.copy_texture_to_buffer(
214 wgpu::TexelCopyTextureInfo {
215 texture: target,
216 mip_level: 0,
217 origin: wgpu::Origin3d::ZERO,
218 aspect: wgpu::TextureAspect::All,
219 },
220 wgpu::TexelCopyBufferInfo {
221 buffer: &buf,
222 layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) },
223 },
224 wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
225 );
226 hal.queue.submit(std::iter::once(enc.finish()));
227 let slice = buf.slice(..);
228 let (tx, rx) = std::sync::mpsc::channel();
229 slice.map_async(wgpu::MapMode::Read, move |r| {
230 let _ = tx.send(r);
231 });
232 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
233 rx.recv().unwrap().unwrap();
234 let data = slice.get_mapped_range();
235 let mut pixels = Vec::with_capacity((W * H * 4) as usize);
236 for row in 0..H as usize {
237 let sst = row * padded;
238 pixels.extend_from_slice(&data[sst..sst + unpadded]);
239 }
240 drop(data);
241 buf.unmap();
242 pixels
243}
244
245fn write_png(pixels: &[u8], path: &str) {
246 let file = File::create(path).expect("png");
247 let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
248 enc.set_color(png::ColorType::Rgba);
249 enc.set_depth(png::BitDepth::Eight);
250 let mut w = enc.write_header().unwrap();
251 w.write_image_data(pixels).unwrap();
252}