tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel/examples/ser_estilizado.rs Rust · 190 lines · 8.0 KB History Who touched what raw

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 visual del **humanoide voxel redondeado** ([`humanoide_voxel`]): el ser
2//! estilizado que aprovecha el bisel/isosuperficie sub-voxel para un look vinyl-toy/chibi
3//! CON carácter (no cajas Minecraft, no busto fotográfico = no valle). Rinde varios
4//! arquetipos/edades/gestos a PNG y certifica por stats de cobertura.
5//!
6//! Diseño anclado en literatura (Kindchenschema, canon de cabezas, shape language) — ver
7//! el módulo `humano`. Es el caso que amerita render (Regla 8): un visual NUEVO que no se
8//! certifica de otra forma.
9//!
10//! `cargo run -p llimphi-voxel --example ser_estilizado --release`
11
12use std::fs::File;
13use std::io::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::{humanoide_voxel, Clip, Colores, EstiloSer, Pose};
21
22const W: u32 = 640;
23const H: u32 = 720;
24const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25const BG: [u8; 3] = [22, 26, 36];
26
27fn env_f(k: &str, d: f32) -> f32 {
28    std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d)
29}
30
31fn main() {
32    let hal = pollster::block_on(Hal::new(None)).expect("hal");
33    let mut renderer = Renderer::new(&hal).expect("renderer");
34    let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into());
35
36    // Piel/ropa de muestra (lo que la foto regularía).
37    let col = Colores::nuevos([0.90, 0.72, 0.58], [0.22, 0.60, 0.72], [0.20, 0.24, 0.36]);
38
39    // Batería: (etiqueta, estilo, pose). Idle expresivo, gestos, arquetipos, edades.
40    let idle = Pose::default();
41    let saludo = Clip::Wave.pose(1.2);
42    let festejo = Clip::Cheer.pose(1.0);
43    let casos: [(&str, EstiloSer, Pose); 6] = [
44        ("chibi_amable", EstiloSer::amable(), idle),
45        ("chibi_saludo", EstiloSer::amable(), saludo),
46        ("chibi_festejo", EstiloSer::amable(), festejo),
47        ("robusto", EstiloSer::robusto(), idle),
48        ("agil", EstiloSer::agil(), saludo),
49        ("bebe", EstiloSer::por_edad(llimphi_voxel::Age::Baby), idle),
50    ];
51
52    let altura = env_f("ALT", 72.0) as u32; // igual que el preview del studio (nivel Seres)
53    println!("ser_estilizado — humanoide voxel redondeado (bisel + silueta por voxel) ALT={altura}");
54    for (nombre, estilo, pose) in &casos {
55        let g = humanoide_voxel(estilo, pose, altura, col);
56        let dim = g.dim();
57        let solids = count_solids(&g);
58        let px = render_ser(&hal, &mut renderer, &g, dim);
59        let cov = coverage(&px);
60        let out = format!("{dir}/ser_{nombre}.png");
61        write_png(&px, &out);
62        println!(
63            "  {nombre:14} cabezas={:.1} dim={dim:?} voxels={solids} → {cov} px cubiertos → {out}",
64            estilo.cabezas
65        );
66        assert!(solids > 1500, "{nombre}: el ser tiene volumen");
67        assert!(cov > 4000, "{nombre}: se renderizó ({cov} px)");
68    }
69    println!("✓ certificado: seres redondeados con gramática sub-voxel (sin cajas, sin valle).");
70}
71
72fn count_solids(g: &VoxelGrid) -> usize {
73    let [w, h, d] = g.dim();
74    let mut n = 0;
75    for x in 0..w {
76        for y in 0..h {
77            for z in 0..d {
78                if g.is_solid(x as i32, y as i32, z as i32) {
79                    n += 1;
80                }
81            }
82        }
83    }
84    n
85}
86
87/// Renderiza el ser de frente-3/4, con bisel global alto (enciende el trazado de
88/// isosuperficie) + silueta por voxel (el bisel del ser redondea el CONTORNO) + rugosidad
89/// leve (grano de piel). Cámara 3/4 (anti-twinning: el 3/4 evita el aplanado frontal).
90fn render_ser(hal: &Hal, renderer: &mut Renderer, grid: &VoxelGrid, dim: [u32; 3]) -> Vec<u8> {
91    let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, grid);
92    vr.bisel = env_f("BIS", 0.9); // global: enciende trace_smooth (la silueta por voxel lo necesita)
93    // Mismos valores que el studio (nivel Seres): rugosidad GLOBAL 0 — el ser sólo lleva su grano
94    // horneado por voxel (0.06 de `EstiloSer::detalle`). Con RUG global alto se veía como escamas.
95    vr.rugosidad_amp = env_f("RUG", 0.0);
96    vr.rugosidad_freq = 1.7;
97    vr.silueta_por_voxel = env_f("SIL", 1.0) > 0.5;
98    vr.ao_amount = env_f("AO", 0.28); // seres redondeados: poca AO (la axial anilla la esfera)
99    vr.sun_dir = normalize([0.4, 0.6, 0.7]);
100
101    let alto = dim[1] as f32;
102    // Mirar al centro del cuerpo (~55% de la altura: torso/cara, no los pies).
103    let look = Vec3::new(0.0, alto * 0.05, 0.0);
104    let camera = Camera3d::orbit(look, 28_f32.to_radians(), 10_f32.to_radians(), alto * 2.0);
105
106    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
107        label: Some("inter"),
108        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
109        mip_level_count: 1,
110        sample_count: 1,
111        dimension: wgpu::TextureDimension::D2,
112        format: FMT,
113        usage: wgpu::TextureUsages::STORAGE_BINDING
114            | wgpu::TextureUsages::TEXTURE_BINDING
115            | wgpu::TextureUsages::RENDER_ATTACHMENT
116            | wgpu::TextureUsages::COPY_SRC,
117        view_formats: &[],
118    });
119    let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
120    let base = vello::Scene::new();
121    renderer
122        .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(BG[0], BG[1], BG[2], 255))
123        .expect("base");
124    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
125    vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), &camera);
126    hal.queue.submit(std::iter::once(enc.finish()));
127    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
128    readback(hal, &inter)
129}
130
131fn normalize(v: [f32; 3]) -> [f32; 3] {
132    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
133    [v[0] / l, v[1] / l, v[2] / l]
134}
135
136fn coverage(px: &[u8]) -> usize {
137    (0..(W * H) as usize)
138        .filter(|&i| {
139            let o = i * 4;
140            (px[o] as i32 - BG[0] as i32).abs()
141                + (px[o + 1] as i32 - BG[1] as i32).abs()
142                + (px[o + 2] as i32 - BG[2] as i32).abs()
143                > 12
144        })
145        .count()
146}
147
148fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
149    let unpadded = (W * 4) as usize;
150    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
151    let padded = unpadded.div_ceil(align) * align;
152    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
153        label: Some("readback"),
154        size: (padded * H as usize) as u64,
155        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
156        mapped_at_creation: false,
157    });
158    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
159    enc.copy_texture_to_buffer(
160        wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
161        wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } },
162        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
163    );
164    hal.queue.submit(std::iter::once(enc.finish()));
165    let slice = buf.slice(..);
166    let (tx, rx) = std::sync::mpsc::channel();
167    slice.map_async(wgpu::MapMode::Read, move |r| {
168        let _ = tx.send(r);
169    });
170    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
171    rx.recv().unwrap().unwrap();
172    let data = slice.get_mapped_range();
173    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
174    for row in 0..H as usize {
175        let s = row * padded;
176        pixels.extend_from_slice(&data[s..s + unpadded]);
177    }
178    drop(data);
179    buf.unmap();
180    pixels
181}
182
183fn write_png(pixels: &[u8], path: &str) {
184    let file = File::create(path).expect("png");
185    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
186    enc.set_color(png::ColorType::Rgba);
187    enc.set_depth(png::BitDepth::Eight);
188    let mut w = enc.write_header().unwrap();
189    w.write_image_data(pixels).unwrap();
190}