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