gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel/examples/foto_ser.rs Rust · 252 siq'ikuna · 10.5 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//! 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}