tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! **El ser, parado en el llano, BAJO las montañas de El Limón** (no flotando en ellas).
2//!
3//! Ejercita el pipeline del "ser desde foto" con las mejoras de 2026-07-18:
4//! 1. Toma una foto (retrato sintético fiel, o `pluma.png`/`arg[1]`), la **absorbe** con un
5//! sampler de bandas (piel/pelo/ropa FIELES; el pelo de la banda superior, no derivado —
6//! espeja `llimphi-voxel-studio::rostro::muestrear_rasgos`, que está unit-testeado allá).
7//! 2. Arma el **humanoide voxel redondeado** (`humanoide_voxel`) con esos colores — cara
8//! abierta, ojos separados, **sin boca de pico de pato** (la zona de barba es tint plano).
9//! 3. Sintetiza un terreno estilo **El Limón/Maracay**: llano verde al sur, Cordillera de la
10//! Costa alzándose al norte.
11//! 4. **ESTAMPA el ser DENTRO del grid del terreno** (pies sobre el suelo real) → GROUNDED,
12//! ocluido y sombreado por el paisaje, no un espíritu flotando.
13//! 5. Rinde: ser en primer plano, montañas de fondo en perspectiva.
14//!
15//! `cargo run -p llimphi-voxel --example ser_bajo_elimon --release -- [foto.png]`
16
17use std::fs::File;
18use std::io::{BufReader, 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 llimphi_voxel::{humanoide_voxel, Colores, EstiloSer, Pose};
26
27const W: u32 = 1280;
28const H: u32 = 720;
29const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
30
31fn env_f(k: &str, d: f32) -> f32 {
32 std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d)
33}
34
35fn main() {
36 let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into());
37 let out = format!("{dir}/ser_bajo_elimon.png");
38
39 // --- 1. Foto → rasgos FIELES (piel/pelo/ropa). ---
40 let arg = std::env::args().nth(1);
41 let (rgba, w, h, alpha) = arg
42 .as_deref()
43 .and_then(load_png_rgba)
44 .unwrap_or_else(|| retrato(112, 150));
45 let (skin, hair, shirt) = muestrear(&rgba, &alpha, w, h);
46 let pants = [shirt[0] * 0.45, shirt[1] * 0.5, (shirt[2] * 0.7).min(1.0)];
47 eprintln!("rasgos FIELES → piel {:?} pelo {:?} ropa {:?}", u8v(skin), u8v(hair), u8v(shirt));
48
49 // --- 2. Humanoide voxel redondeado con esos colores (pelo real, sin boca de pato) ---
50 let estilo = EstiloSer::amable();
51 let col = Colores::con_pelo(skin, shirt, pants, hair);
52 let alto_ser = env_f("SER_H", 44.0) as u32;
53 let ser = humanoide_voxel(&estilo, &Pose::default(), alto_ser, col);
54 eprintln!("ser voxel: dim {:?}", ser.dim());
55
56 // --- 3. Terreno estilo El Limón: llano verde al sur, cordillera al norte ---
57 let mut escena = terreno_elimon();
58 let dim = escena.dim();
59
60 // --- 4. Plantar el ser DENTRO del grid (pies en el suelo del llano, sur) ---
61 let bx = (dim[0] as f32 * 0.5) as u32;
62 let bz = (dim[2] as f32 * 0.20) as u32; // en el llano del sur (frente)
63 let suelo = altura_suelo(&escena, bx, bz);
64 plantar(&mut escena, &ser, bx, suelo + 1, bz, &estilo);
65 eprintln!("ser plantado en ({bx},{},{bz}) sobre el llano; dim escena {dim:?}", suelo + 1);
66
67 // --- 5. Render cinematográfico: cámara baja en el llano mirando al norte (montañas) ---
68 let hal = pollster::block_on(Hal::new(None)).expect("hal");
69 let mut renderer = Renderer::new(&hal).expect("renderer");
70 let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &escena);
71 vr.bisel = 0.85;
72 vr.rugosidad_amp = 0.5; // textura del TERRENO (el ser usa su detalle por-voxel propio)
73 vr.rugosidad_freq = 1.7;
74 vr.silueta_por_voxel = env_f("SIL", 1.0) > 0.5;
75 vr.ao_amount = env_f("AO", 0.3); // baja: el ser redondeado no quiere anillos axiales
76 vr.sun_dir = normalize([0.55, 0.5, -0.35]); // sol cálido bajo, tarde tropical
77 vr.atmosphere = Atmosphere {
78 sky_zenith: [72, 120, 190],
79 sky_horizon: [214, 202, 176],
80 fog_density: env_f("FOG", 0.010),
81 god_rays: 0.4,
82 };
83
84 // Cámara: parada en el llano, un poco al sur del ser y a un costado, a la altura de los
85 // ojos, mirando al norte (+Z) → el ser en primer plano, las montañas de fondo.
86 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
87 let cam_x = bx as f32 + env_f("CAM_DX", 13.0);
88 let cam_z = bz as f32 - env_f("CAM_BACK", 56.0);
89 let suelo_cam = altura_suelo(&escena, cam_x as u32, cam_z.max(0.0) as u32) as f32;
90 let eye = Vec3::new(cam_x, suelo_cam + env_f("CAM_H", 25.0), cam_z) - half;
91 let cam = Camera3d::fly(eye, env_f("YAW", 0.0).to_radians(), env_f("PITCH", -1.0).to_radians());
92
93 let px = render(&hal, &mut renderer, &mut vr, &cam);
94 write_png(&px, &out);
95 eprintln!("✓ toma escrita: {out}");
96}
97
98fn u8v(c: [f32; 3]) -> [u8; 3] {
99 [(c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8]
100}
101
102/// **Absorbe** la foto en 3 colores fieles (espeja `rostro::muestrear_rasgos`, unit-testeado
103/// en el studio): **pelo** de la banda superior (top 18%), **piel** de la franja de la cara
104/// (22–52%, píxeles cálidos), **ropa** de la mitad inferior. Usa el bbox de `alpha>128`.
105fn muestrear(rgba: &[u8], alpha: &[u8], w: u32, h: u32) -> ([f32; 3], [f32; 3], [f32; 3]) {
106 let (w, h) = (w as usize, h as usize);
107 let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);
108 let mut hay = false;
109 for y in 0..h {
110 for x in 0..w {
111 if alpha[y * w + x] > 128 {
112 hay = true;
113 x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y);
114 }
115 }
116 }
117 if !hay {
118 x0 = w / 4; x1 = 3 * w / 4; y0 = h / 4; y1 = 3 * h / 4;
119 }
120 let bh = (y1 - y0).max(1);
121 let bw = (x1 - x0).max(1);
122 let cx0 = x0 + bw * 20 / 100;
123 let cx1 = x0 + bw * 80 / 100;
124 let media = |ya: usize, yb: usize, xa: usize, xb: usize, calido: bool| -> [f32; 3] {
125 let (mut r, mut g, mut b, mut n) = (0u64, 0u64, 0u64, 0u64);
126 for y in ya..yb.min(h) {
127 for x in xa..=xb.min(w - 1) {
128 if alpha[y * w + x] <= 128 { continue; }
129 let o = (y * w + x) * 4;
130 let (rr, gg, bb) = (rgba[o] as i32, rgba[o + 1] as i32, rgba[o + 2] as i32);
131 if calido && !(rr >= gg && gg >= bb && (rr - bb) > 6 && (rr + gg + bb) > 140) { continue; }
132 r += rr as u64; g += gg as u64; b += bb as u64; n += 1;
133 }
134 }
135 if n == 0 { return [0.6, 0.5, 0.4]; }
136 [r as f32 / n as f32 / 255.0, g as f32 / n as f32 / 255.0, b as f32 / n as f32 / 255.0]
137 };
138 let hair = media(y0, y0 + bh * 18 / 100, cx0, cx1, false);
139 let mut skin = media(y0 + bh * 22 / 100, y0 + bh * 52 / 100, cx0, cx1, true);
140 if skin[0] + skin[1] + skin[2] < 0.3 {
141 skin = media(y0 + bh * 22 / 100, y0 + bh * 52 / 100, cx0, cx1, false);
142 }
143 let shirt = media(y0 + bh * 50 / 100, y1 + 1, x0, x1, false);
144 (skin, hair, shirt)
145}
146
147/// Retrato sintético RGBA + alpha con **pelo, piel y ropa bien distintos** (para ver la
148/// fidelidad): pelo castaño, piel cálida, remera verde.
149fn retrato(w: u32, h: u32) -> (Vec<u8>, u32, u32, Vec<u8>) {
150 let mut rgba = vec![0u8; (w * h * 4) as usize];
151 let mut alpha = vec![0u8; (w * h) as usize];
152 let cx = w as f32 / 2.0;
153 for y in 0..h {
154 for x in 0..w {
155 let i = (y * w + x) as usize;
156 let o = i * 4;
157 let ny = y as f32 / h as f32;
158 let head = {
159 let dx = (x as f32 - cx) / (w as f32 * 0.28);
160 let dy = (y as f32 - h as f32 * 0.32) / (h as f32 * 0.28);
161 dx * dx + dy * dy < 1.0
162 };
163 let torso = ny > 0.58 && ((x as f32 - cx).abs() < w as f32 * 0.34);
164 if !head && !torso {
165 continue;
166 }
167 alpha[i] = 255;
168 let c = if head && ny < 0.16 {
169 [96, 64, 40] // pelo castaño (banda superior)
170 } else if head {
171 [222, 176, 142] // piel cálida
172 } else {
173 [64, 140, 92] // remera verde
174 };
175 rgba[o] = c[0];
176 rgba[o + 1] = c[1];
177 rgba[o + 2] = c[2];
178 rgba[o + 3] = 255;
179 }
180 }
181 (rgba, w, h, alpha)
182}
183
184/// Terreno estilo El Limón/Maracay: llano verde al sur (z chico), Cordillera de la Costa
185/// alzándose al norte (z grande). Color por altura (valle→bosque→roca) + grano.
186fn terreno_elimon() -> VoxelGrid {
187 let dim = [220u32, 96, 210u32];
188 let mut g = VoxelGrid::new(dim);
189 let base = 3u32;
190 let maxh = 82.0f32;
191 for gz in 0..dim[2] {
192 for gx in 0..dim[0] {
193 let t = gz as f32 / dim[2] as f32; // 0 sur (llano) .. 1 norte (montañas)
194 let rise = smoothstep(0.30, 1.0, t);
195 let fx = gx as f32;
196 let fz = gz as f32;
197 let ridges = 0.55 * (fx * 0.05).sin().abs() + 0.45 * (fx * 0.021 + fz * 0.03).sin().abs();
198 let peaks = fbm(fx * 0.03, fz * 0.03);
199 let relief = (0.45 * ridges + 0.55 * peaks).clamp(0.0, 1.0);
200 let top = base + (rise * relief * maxh) as u32;
201 for y in 0..=top.min(dim[1] - 1) {
202 let th = y as f32 / (base as f32 + maxh);
203 g.set(gx, y, gz, color_altura(th, gx, y, gz));
204 }
205 }
206 }
207 g.reset_dirty();
208 g
209}
210
211fn altura_suelo(g: &VoxelGrid, x: u32, z: u32) -> u32 {
212 let [_, dy, _] = g.dim();
213 for y in (0..dy).rev() {
214 if g.is_solid(x as i32, y as i32, z as i32) {
215 return y;
216 }
217 }
218 0
219}
220
221/// Estampa el ser DENTRO del grid destino con su detalle sub-voxel propio (pies en `(x,y,z)`,
222/// base centrada en XZ). GROUNDED: comparte el grid del terreno → se ocluye/sombrea con él.
223fn plantar(dst: &mut VoxelGrid, ser: &VoxelGrid, x: u32, y: u32, z: u32, estilo: &EstiloSer) {
224 let [bw, bh, bd] = ser.dim();
225 let (cx, cz) = (bw as i32 / 2, bd as i32 / 2);
226 let (rug, bisel) = estilo.detalle();
227 dst.set_detail_brush(rug, bisel);
228 let dim = dst.dim();
229 for by in 0..bh {
230 for bz in 0..bd {
231 for bx in 0..bw {
232 if let Some(c) = ser.get(bx, by, bz) {
233 if c[3] <= 127 {
234 continue;
235 }
236 let px = x as i32 + (bx as i32 - cx);
237 let py = y as i32 + by as i32;
238 let pz = z as i32 + (bz as i32 - cz);
239 if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
240 dst.set(px as u32, py as u32, pz as u32, [c[0], c[1], c[2]]);
241 }
242 }
243 }
244 }
245 }
246 dst.clear_detail_brush();
247}
248
249fn color_altura(t: f32, x: u32, y: u32, z: u32) -> [u8; 3] {
250 let base = if t < 0.10 {
251 [92, 138, 74]
252 } else if t < 0.42 {
253 mezcla([92, 138, 74], [62, 106, 58], (t - 0.10) / 0.32)
254 } else if t < 0.72 {
255 mezcla([62, 106, 58], [112, 102, 86], (t - 0.42) / 0.30)
256 } else {
257 mezcla([112, 102, 86], [150, 146, 140], (t - 0.72) / 0.28)
258 };
259 let j = (hash(x, y, z) - 0.5) * 2.0 * 0.10;
260 let ch = |c: u8| ((c as f32) * (1.0 + j)).clamp(0.0, 255.0) as u8;
261 [ch(base[0]), ch(base[1]), ch(base[2])]
262}
263
264fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
265 let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
266 t * t * (3.0 - 2.0 * t)
267}
268
269fn fbm(x: f32, z: f32) -> f32 {
270 let mut a = 0.5;
271 let (mut fx, mut fz) = (x, z);
272 let mut sum = 0.0;
273 for _ in 0..4 {
274 sum += a * vnoise(fx, fz);
275 fx *= 2.0;
276 fz *= 2.0;
277 a *= 0.5;
278 }
279 sum.clamp(0.0, 1.0)
280}
281
282fn vnoise(x: f32, z: f32) -> f32 {
283 let (x0, z0) = (x.floor(), z.floor());
284 let (tx, tz) = (x - x0, z - z0);
285 let hh = |i: f32, j: f32| {
286 let mut n = (i as i32).wrapping_mul(374761393).wrapping_add((j as i32).wrapping_mul(668265263)) as u32;
287 n = (n ^ (n >> 13)).wrapping_mul(1274126177);
288 (n & 0xffff) as f32 / 65535.0
289 };
290 let sm = |t: f32| t * t * (3.0 - 2.0 * t);
291 let (ux, uz) = (sm(tx), sm(tz));
292 let a = hh(x0, z0);
293 let b = hh(x0 + 1.0, z0);
294 let c = hh(x0, z0 + 1.0);
295 let d = hh(x0 + 1.0, z0 + 1.0);
296 let ab = a + (b - a) * ux;
297 let cd = c + (d - c) * ux;
298 ab + (cd - ab) * uz
299}
300
301fn mezcla(a: [u8; 3], b: [u8; 3], t: f32) -> [u8; 3] {
302 let m = |i: usize| (a[i] as f32 + (b[i] as f32 - a[i] as f32) * t.clamp(0.0, 1.0)) as u8;
303 [m(0), m(1), m(2)]
304}
305
306fn hash(x: u32, y: u32, z: u32) -> f32 {
307 let mut h = x.wrapping_mul(374761393).wrapping_add(y.wrapping_mul(668265263)).wrapping_add(z.wrapping_mul(2246822519));
308 h ^= h >> 13;
309 h = h.wrapping_mul(1274126177);
310 (h & 0xffff) as f32 / 65535.0
311}
312
313fn normalize(v: [f32; 3]) -> [f32; 3] {
314 let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
315 [v[0] / l, v[1] / l, v[2] / l]
316}
317
318/// Decodifica un PNG a (rgba, w, h, alpha). `None` si no existe/ilegible.
319fn load_png_rgba(path: &str) -> Option<(Vec<u8>, u32, u32, Vec<u8>)> {
320 let file = File::open(path).ok()?;
321 let dec = png::Decoder::new(BufReader::new(file));
322 let mut reader = dec.read_info().ok()?;
323 let mut buf = vec![0u8; reader.output_buffer_size()?];
324 let info = reader.next_frame(&mut buf).ok()?;
325 let (w, h) = (info.width, info.height);
326 let rgba: Vec<u8> = match info.color_type {
327 png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
328 png::ColorType::Rgb => buf[..info.buffer_size()].chunks(3).flat_map(|c| [c[0], c[1], c[2], 255]).collect(),
329 png::ColorType::Grayscale => buf[..info.buffer_size()].iter().flat_map(|&g| [g, g, g, 255]).collect(),
330 _ => return None,
331 };
332 let alpha: Vec<u8> = rgba.chunks_exact(4).map(|p| p[3]).collect();
333 Some((rgba, w, h, alpha))
334}
335
336fn render(hal: &Hal, renderer: &mut Renderer, vr: &mut VoxelRenderer, cam: &Camera3d) -> Vec<u8> {
337 let bg = [190, 200, 214];
338 let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
339 label: Some("inter"),
340 size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
341 mip_level_count: 1,
342 sample_count: 1,
343 dimension: wgpu::TextureDimension::D2,
344 format: FMT,
345 usage: wgpu::TextureUsages::STORAGE_BINDING
346 | wgpu::TextureUsages::TEXTURE_BINDING
347 | wgpu::TextureUsages::RENDER_ATTACHMENT
348 | wgpu::TextureUsages::COPY_SRC,
349 view_formats: &[],
350 });
351 let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
352 let base = vello::Scene::new();
353 renderer
354 .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(bg[0], bg[1], bg[2], 255))
355 .expect("base");
356 let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
357 vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), cam);
358 hal.queue.submit(std::iter::once(enc.finish()));
359 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
360 readback(hal, &inter)
361}
362
363fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
364 let unpadded = (W * 4) as usize;
365 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
366 let padded = unpadded.div_ceil(align) * align;
367 let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
368 label: None,
369 size: (padded * H as usize) as u64,
370 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
371 mapped_at_creation: false,
372 });
373 let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
374 enc.copy_texture_to_buffer(
375 wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
376 wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } },
377 wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
378 );
379 hal.queue.submit(std::iter::once(enc.finish()));
380 let slice = buf.slice(..);
381 let (tx, rx) = std::sync::mpsc::channel();
382 slice.map_async(wgpu::MapMode::Read, move |r| {
383 let _ = tx.send(r);
384 });
385 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
386 rx.recv().unwrap().unwrap();
387 let data = slice.get_mapped_range();
388 let mut px = Vec::with_capacity((W * H * 4) as usize);
389 for row in 0..H as usize {
390 let s = row * padded;
391 px.extend_from_slice(&data[s..s + unpadded]);
392 }
393 drop(data);
394 buf.unmap();
395 px
396}
397
398fn write_png(px: &[u8], path: &str) {
399 let f = std::fs::File::create(path).expect("png");
400 let mut enc = png::Encoder::new(BufWriter::new(f), W, H);
401 enc.set_color(png::ColorType::Rgba);
402 enc.set_depth(png::BitDepth::Eight);
403 let mut w = enc.write_header().unwrap();
404 w.write_image_data(px).unwrap();
405}