gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Sutichaykuna

⎇ main llimphi-voxel/examples/ser_bajo_elimon.rs Rust · 405 siq'ikuna · 16.6 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//! **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}