//! **El ser, parado en el llano, BAJO las montañas de El Limón** (no flotando en ellas). //! //! Ejercita el pipeline del "ser desde foto" con las mejoras de 2026-07-18: //! 1. Toma una foto (retrato sintético fiel, o `pluma.png`/`arg[1]`), la **absorbe** con un //! sampler de bandas (piel/pelo/ropa FIELES; el pelo de la banda superior, no derivado — //! espeja `llimphi-voxel-studio::rostro::muestrear_rasgos`, que está unit-testeado allá). //! 2. Arma el **humanoide voxel redondeado** (`humanoide_voxel`) con esos colores — cara //! abierta, ojos separados, **sin boca de pico de pato** (la zona de barba es tint plano). //! 3. Sintetiza un terreno estilo **El Limón/Maracay**: llano verde al sur, Cordillera de la //! Costa alzándose al norte. //! 4. **ESTAMPA el ser DENTRO del grid del terreno** (pies sobre el suelo real) → GROUNDED, //! ocluido y sombreado por el paisaje, no un espíritu flotando. //! 5. Rinde: ser en primer plano, montañas de fondo en perspectiva. //! //! `cargo run -p llimphi-voxel --example ser_bajo_elimon --release -- [foto.png]` use std::fs::File; use std::io::{BufReader, BufWriter}; use llimphi_3d::glam::Vec3; use llimphi_3d::{Atmosphere, Camera3d, VoxelGrid, VoxelRenderer}; use llimphi_hal::{wgpu, Hal}; use llimphi_raster::peniko::Color; use llimphi_raster::{vello, Renderer}; use llimphi_voxel::{humanoide_voxel, Colores, EstiloSer, Pose}; const W: u32 = 1280; const H: u32 = 720; const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; fn env_f(k: &str, d: f32) -> f32 { std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d) } fn main() { let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into()); let out = format!("{dir}/ser_bajo_elimon.png"); // --- 1. Foto → rasgos FIELES (piel/pelo/ropa). --- let arg = std::env::args().nth(1); let (rgba, w, h, alpha) = arg .as_deref() .and_then(load_png_rgba) .unwrap_or_else(|| retrato(112, 150)); let (skin, hair, shirt) = muestrear(&rgba, &alpha, w, h); let pants = [shirt[0] * 0.45, shirt[1] * 0.5, (shirt[2] * 0.7).min(1.0)]; eprintln!("rasgos FIELES → piel {:?} pelo {:?} ropa {:?}", u8v(skin), u8v(hair), u8v(shirt)); // --- 2. Humanoide voxel redondeado con esos colores (pelo real, sin boca de pato) --- let estilo = EstiloSer::amable(); let col = Colores::con_pelo(skin, shirt, pants, hair); let alto_ser = env_f("SER_H", 44.0) as u32; let ser = humanoide_voxel(&estilo, &Pose::default(), alto_ser, col); eprintln!("ser voxel: dim {:?}", ser.dim()); // --- 3. Terreno estilo El Limón: llano verde al sur, cordillera al norte --- let mut escena = terreno_elimon(); let dim = escena.dim(); // --- 4. Plantar el ser DENTRO del grid (pies en el suelo del llano, sur) --- let bx = (dim[0] as f32 * 0.5) as u32; let bz = (dim[2] as f32 * 0.20) as u32; // en el llano del sur (frente) let suelo = altura_suelo(&escena, bx, bz); plantar(&mut escena, &ser, bx, suelo + 1, bz, &estilo); eprintln!("ser plantado en ({bx},{},{bz}) sobre el llano; dim escena {dim:?}", suelo + 1); // --- 5. Render cinematográfico: cámara baja en el llano mirando al norte (montañas) --- let hal = pollster::block_on(Hal::new(None)).expect("hal"); let mut renderer = Renderer::new(&hal).expect("renderer"); let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &escena); vr.bisel = 0.85; vr.rugosidad_amp = 0.5; // textura del TERRENO (el ser usa su detalle por-voxel propio) vr.rugosidad_freq = 1.7; vr.silueta_por_voxel = env_f("SIL", 1.0) > 0.5; vr.ao_amount = env_f("AO", 0.3); // baja: el ser redondeado no quiere anillos axiales vr.sun_dir = normalize([0.55, 0.5, -0.35]); // sol cálido bajo, tarde tropical vr.atmosphere = Atmosphere { sky_zenith: [72, 120, 190], sky_horizon: [214, 202, 176], fog_density: env_f("FOG", 0.010), god_rays: 0.4, }; // Cámara: parada en el llano, un poco al sur del ser y a un costado, a la altura de los // ojos, mirando al norte (+Z) → el ser en primer plano, las montañas de fondo. let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; let cam_x = bx as f32 + env_f("CAM_DX", 13.0); let cam_z = bz as f32 - env_f("CAM_BACK", 56.0); let suelo_cam = altura_suelo(&escena, cam_x as u32, cam_z.max(0.0) as u32) as f32; let eye = Vec3::new(cam_x, suelo_cam + env_f("CAM_H", 25.0), cam_z) - half; let cam = Camera3d::fly(eye, env_f("YAW", 0.0).to_radians(), env_f("PITCH", -1.0).to_radians()); let px = render(&hal, &mut renderer, &mut vr, &cam); write_png(&px, &out); eprintln!("✓ toma escrita: {out}"); } fn u8v(c: [f32; 3]) -> [u8; 3] { [(c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8] } /// **Absorbe** la foto en 3 colores fieles (espeja `rostro::muestrear_rasgos`, unit-testeado /// en el studio): **pelo** de la banda superior (top 18%), **piel** de la franja de la cara /// (22–52%, píxeles cálidos), **ropa** de la mitad inferior. Usa el bbox de `alpha>128`. fn muestrear(rgba: &[u8], alpha: &[u8], w: u32, h: u32) -> ([f32; 3], [f32; 3], [f32; 3]) { let (w, h) = (w as usize, h as usize); let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize); let mut hay = false; for y in 0..h { for x in 0..w { if alpha[y * w + x] > 128 { hay = true; x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y); } } } if !hay { x0 = w / 4; x1 = 3 * w / 4; y0 = h / 4; y1 = 3 * h / 4; } let bh = (y1 - y0).max(1); let bw = (x1 - x0).max(1); let cx0 = x0 + bw * 20 / 100; let cx1 = x0 + bw * 80 / 100; let media = |ya: usize, yb: usize, xa: usize, xb: usize, calido: bool| -> [f32; 3] { let (mut r, mut g, mut b, mut n) = (0u64, 0u64, 0u64, 0u64); for y in ya..yb.min(h) { for x in xa..=xb.min(w - 1) { if alpha[y * w + x] <= 128 { continue; } let o = (y * w + x) * 4; let (rr, gg, bb) = (rgba[o] as i32, rgba[o + 1] as i32, rgba[o + 2] as i32); if calido && !(rr >= gg && gg >= bb && (rr - bb) > 6 && (rr + gg + bb) > 140) { continue; } r += rr as u64; g += gg as u64; b += bb as u64; n += 1; } } if n == 0 { return [0.6, 0.5, 0.4]; } [r as f32 / n as f32 / 255.0, g as f32 / n as f32 / 255.0, b as f32 / n as f32 / 255.0] }; let hair = media(y0, y0 + bh * 18 / 100, cx0, cx1, false); let mut skin = media(y0 + bh * 22 / 100, y0 + bh * 52 / 100, cx0, cx1, true); if skin[0] + skin[1] + skin[2] < 0.3 { skin = media(y0 + bh * 22 / 100, y0 + bh * 52 / 100, cx0, cx1, false); } let shirt = media(y0 + bh * 50 / 100, y1 + 1, x0, x1, false); (skin, hair, shirt) } /// Retrato sintético RGBA + alpha con **pelo, piel y ropa bien distintos** (para ver la /// fidelidad): pelo castaño, piel cálida, remera verde. fn retrato(w: u32, h: u32) -> (Vec, u32, u32, Vec) { let mut rgba = vec![0u8; (w * h * 4) as usize]; let mut alpha = vec![0u8; (w * h) as usize]; let cx = w as f32 / 2.0; for y in 0..h { for x in 0..w { let i = (y * w + x) as usize; let o = i * 4; let ny = y as f32 / h as f32; let head = { let dx = (x as f32 - cx) / (w as f32 * 0.28); let dy = (y as f32 - h as f32 * 0.32) / (h as f32 * 0.28); dx * dx + dy * dy < 1.0 }; let torso = ny > 0.58 && ((x as f32 - cx).abs() < w as f32 * 0.34); if !head && !torso { continue; } alpha[i] = 255; let c = if head && ny < 0.16 { [96, 64, 40] // pelo castaño (banda superior) } else if head { [222, 176, 142] // piel cálida } else { [64, 140, 92] // remera verde }; rgba[o] = c[0]; rgba[o + 1] = c[1]; rgba[o + 2] = c[2]; rgba[o + 3] = 255; } } (rgba, w, h, alpha) } /// Terreno estilo El Limón/Maracay: llano verde al sur (z chico), Cordillera de la Costa /// alzándose al norte (z grande). Color por altura (valle→bosque→roca) + grano. fn terreno_elimon() -> VoxelGrid { let dim = [220u32, 96, 210u32]; let mut g = VoxelGrid::new(dim); let base = 3u32; let maxh = 82.0f32; for gz in 0..dim[2] { for gx in 0..dim[0] { let t = gz as f32 / dim[2] as f32; // 0 sur (llano) .. 1 norte (montañas) let rise = smoothstep(0.30, 1.0, t); let fx = gx as f32; let fz = gz as f32; let ridges = 0.55 * (fx * 0.05).sin().abs() + 0.45 * (fx * 0.021 + fz * 0.03).sin().abs(); let peaks = fbm(fx * 0.03, fz * 0.03); let relief = (0.45 * ridges + 0.55 * peaks).clamp(0.0, 1.0); let top = base + (rise * relief * maxh) as u32; for y in 0..=top.min(dim[1] - 1) { let th = y as f32 / (base as f32 + maxh); g.set(gx, y, gz, color_altura(th, gx, y, gz)); } } } g.reset_dirty(); g } fn altura_suelo(g: &VoxelGrid, x: u32, z: u32) -> u32 { let [_, dy, _] = g.dim(); for y in (0..dy).rev() { if g.is_solid(x as i32, y as i32, z as i32) { return y; } } 0 } /// Estampa el ser DENTRO del grid destino con su detalle sub-voxel propio (pies en `(x,y,z)`, /// base centrada en XZ). GROUNDED: comparte el grid del terreno → se ocluye/sombrea con él. fn plantar(dst: &mut VoxelGrid, ser: &VoxelGrid, x: u32, y: u32, z: u32, estilo: &EstiloSer) { let [bw, bh, bd] = ser.dim(); let (cx, cz) = (bw as i32 / 2, bd as i32 / 2); let (rug, bisel) = estilo.detalle(); dst.set_detail_brush(rug, bisel); let dim = dst.dim(); for by in 0..bh { for bz in 0..bd { for bx in 0..bw { if let Some(c) = ser.get(bx, by, bz) { if c[3] <= 127 { continue; } let px = x as i32 + (bx as i32 - cx); let py = y as i32 + by as i32; let pz = z as i32 + (bz as i32 - cz); if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] { dst.set(px as u32, py as u32, pz as u32, [c[0], c[1], c[2]]); } } } } } dst.clear_detail_brush(); } fn color_altura(t: f32, x: u32, y: u32, z: u32) -> [u8; 3] { let base = if t < 0.10 { [92, 138, 74] } else if t < 0.42 { mezcla([92, 138, 74], [62, 106, 58], (t - 0.10) / 0.32) } else if t < 0.72 { mezcla([62, 106, 58], [112, 102, 86], (t - 0.42) / 0.30) } else { mezcla([112, 102, 86], [150, 146, 140], (t - 0.72) / 0.28) }; let j = (hash(x, y, z) - 0.5) * 2.0 * 0.10; let ch = |c: u8| ((c as f32) * (1.0 + j)).clamp(0.0, 255.0) as u8; [ch(base[0]), ch(base[1]), ch(base[2])] } fn smoothstep(a: f32, b: f32, x: f32) -> f32 { let t = ((x - a) / (b - a)).clamp(0.0, 1.0); t * t * (3.0 - 2.0 * t) } fn fbm(x: f32, z: f32) -> f32 { let mut a = 0.5; let (mut fx, mut fz) = (x, z); let mut sum = 0.0; for _ in 0..4 { sum += a * vnoise(fx, fz); fx *= 2.0; fz *= 2.0; a *= 0.5; } sum.clamp(0.0, 1.0) } fn vnoise(x: f32, z: f32) -> f32 { let (x0, z0) = (x.floor(), z.floor()); let (tx, tz) = (x - x0, z - z0); let hh = |i: f32, j: f32| { let mut n = (i as i32).wrapping_mul(374761393).wrapping_add((j as i32).wrapping_mul(668265263)) as u32; n = (n ^ (n >> 13)).wrapping_mul(1274126177); (n & 0xffff) as f32 / 65535.0 }; let sm = |t: f32| t * t * (3.0 - 2.0 * t); let (ux, uz) = (sm(tx), sm(tz)); let a = hh(x0, z0); let b = hh(x0 + 1.0, z0); let c = hh(x0, z0 + 1.0); let d = hh(x0 + 1.0, z0 + 1.0); let ab = a + (b - a) * ux; let cd = c + (d - c) * ux; ab + (cd - ab) * uz } fn mezcla(a: [u8; 3], b: [u8; 3], t: f32) -> [u8; 3] { let m = |i: usize| (a[i] as f32 + (b[i] as f32 - a[i] as f32) * t.clamp(0.0, 1.0)) as u8; [m(0), m(1), m(2)] } fn hash(x: u32, y: u32, z: u32) -> f32 { let mut h = x.wrapping_mul(374761393).wrapping_add(y.wrapping_mul(668265263)).wrapping_add(z.wrapping_mul(2246822519)); h ^= h >> 13; h = h.wrapping_mul(1274126177); (h & 0xffff) as f32 / 65535.0 } fn normalize(v: [f32; 3]) -> [f32; 3] { let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6); [v[0] / l, v[1] / l, v[2] / l] } /// Decodifica un PNG a (rgba, w, h, alpha). `None` si no existe/ilegible. fn load_png_rgba(path: &str) -> Option<(Vec, u32, u32, Vec)> { let file = File::open(path).ok()?; let dec = png::Decoder::new(BufReader::new(file)); let mut reader = dec.read_info().ok()?; let mut buf = vec![0u8; reader.output_buffer_size()?]; let info = reader.next_frame(&mut buf).ok()?; let (w, h) = (info.width, info.height); let rgba: Vec = match info.color_type { png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(), png::ColorType::Rgb => buf[..info.buffer_size()].chunks(3).flat_map(|c| [c[0], c[1], c[2], 255]).collect(), png::ColorType::Grayscale => buf[..info.buffer_size()].iter().flat_map(|&g| [g, g, g, 255]).collect(), _ => return None, }; let alpha: Vec = rgba.chunks_exact(4).map(|p| p[3]).collect(); Some((rgba, w, h, alpha)) } fn render(hal: &Hal, renderer: &mut Renderer, vr: &mut VoxelRenderer, cam: &Camera3d) -> Vec { let bg = [190, 200, 214]; let inter = hal.device.create_texture(&wgpu::TextureDescriptor { label: Some("inter"), size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: FMT, usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = inter.create_view(&wgpu::TextureViewDescriptor::default()); let base = vello::Scene::new(); renderer .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(bg[0], bg[1], bg[2], 255)) .expect("base"); let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), cam); hal.queue.submit(std::iter::once(enc.finish())); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); readback(hal, &inter) } fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec { let unpadded = (W * 4) as usize; let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; let padded = unpadded.div_ceil(align) * align; let buf = hal.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: (padded * H as usize) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); enc.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All }, wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } }, wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, ); hal.queue.submit(std::iter::once(enc.finish())); let slice = buf.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); rx.recv().unwrap().unwrap(); let data = slice.get_mapped_range(); let mut px = Vec::with_capacity((W * H * 4) as usize); for row in 0..H as usize { let s = row * padded; px.extend_from_slice(&data[s..s + unpadded]); } drop(data); buf.unmap(); px } fn write_png(px: &[u8], path: &str) { let f = std::fs::File::create(path).expect("png"); let mut enc = png::Encoder::new(BufWriter::new(f), W, H); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); let mut w = enc.write_header().unwrap(); w.write_image_data(px).unwrap(); }