gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-voxel/examples/ser_sdf_demo.rs Rust · 194 líneas · 8.1 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Certificación visual del **ser SDF** ([`ser_sdf`]): el personaje como ensamble de
2//! primitivas analíticas fundidas por `smin`, sphere-traceado en el MISMO pase que el
3//! mundo voxel (piso damero), con rampa toon + rim y cara por decals (ojos/sonrisa/
4//! rubor). Es la técnica "hero/cerca"; `humanoide_voxel` queda de LOD.
5//!
6//! Compone contra un PISO VOXEL real → certifica de paso la oclusión por `t` y la
7//! sombra del ser proyectada sobre los voxels (el shadow ray del sol marcha los SDF).
8//!
9//! `cargo run -p llimphi-voxel --example ser_sdf_demo --release`
10
11use std::fs::File;
12use std::io::BufWriter;
13
14use llimphi_3d::glam::Vec3;
15use llimphi_3d::{Camera3d, VoxelGrid, VoxelRenderer};
16use llimphi_hal::{wgpu, Hal};
17use llimphi_raster::peniko::Color;
18use llimphi_raster::{vello, Renderer};
19use llimphi_voxel::{ser_sdf, Clip, Colores, EstiloSer, Pose};
20
21const W: u32 = 640;
22const H: u32 = 720;
23const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
24const BG: [u8; 3] = [22, 26, 36];
25
26/// Lado del mundo (piso damero abajo, aire arriba: el ser vive en el aire).
27const DIM: u32 = 112;
28const FLOOR_H: u32 = 2;
29
30fn main() {
31    let hal = pollster::block_on(Hal::new(None)).expect("hal");
32    let mut renderer = Renderer::new(&hal).expect("renderer");
33    let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into());
34
35    let col = Colores::nuevos([0.90, 0.72, 0.58], [0.22, 0.60, 0.72], [0.20, 0.24, 0.36]);
36
37    let idle = Pose::default();
38    let saludo = Clip::Wave.pose(1.2);
39    let festejo = Clip::Cheer.pose(1.0);
40    let casos: [(&str, EstiloSer, Pose); 6] = [
41        ("chibi_amable", EstiloSer::amable(), idle),
42        ("chibi_saludo", EstiloSer::amable(), saludo),
43        ("chibi_festejo", EstiloSer::amable(), festejo),
44        ("robusto", EstiloSer::robusto(), idle),
45        ("agil", EstiloSer::agil(), saludo),
46        ("bebe", EstiloSer::por_edad(llimphi_voxel::Age::Baby), idle),
47    ];
48
49    // Piso damero (el mundo voxel con el que el ser compone y sobre el que sombrea).
50    let mut grid = VoxelGrid::new([DIM, DIM, DIM]);
51    for x in 0..DIM {
52        for z in 0..DIM {
53            for y in 0..FLOOR_H {
54                let c = if (x / 8 + z / 8) % 2 == 0 { [148, 142, 132] } else { [118, 114, 108] };
55                grid.set(x, y, z, c);
56            }
57        }
58    }
59
60    let altura = 72.0; // misma referencia que el preview del studio (nivel Seres)
61    println!("ser_sdf_demo — ser SDF analítico (smin + toon + decals) ALT={altura}");
62    for (nombre, estilo, pose) in &casos {
63        let mut ser = ser_sdf(estilo, pose, altura, col);
64        // Bisección de artefactos: QUITA="3,18,19" remueve primitivas por índice.
65        if let Ok(q) = std::env::var("QUITA") {
66            let fuera: Vec<usize> = q.split(',').filter_map(|s| s.trim().parse().ok()).collect();
67            ser.prims = ser
68                .prims
69                .into_iter()
70                .enumerate()
71                .filter(|(i, _)| !fuera.contains(i))
72                .map(|(_, p)| p)
73                .collect();
74        }
75        // Posarlo parado sobre el piso, centrado en la grilla (coords de voxel [0,dim]).
76        // SER_YAW=<rad> lo gira (ejercita la rotación local del shader).
77        let syaw: f32 = std::env::var("SER_YAW").ok().and_then(|s| s.parse().ok()).unwrap_or(0.0);
78        let posado = ser.posado([DIM as f32 * 0.5, FLOOR_H as f32, DIM as f32 * 0.5], syaw);
79        let n_prims = posado.prims.len();
80        let px = render_ser(&hal, &mut renderer, &grid, posado);
81        let cov = coverage(&px);
82        let out = format!("{dir}/ser_sdf_{nombre}.png");
83        write_png(&px, &out);
84        println!(
85            "  {nombre:14} cabezas={:.1} prims={n_prims} → {cov} px cubiertos → {out}",
86            estilo.cabezas
87        );
88        assert!(cov > 4000, "{nombre}: se renderizó ({cov} px)");
89    }
90    println!("✓ certificado: seres SDF trazados con el mundo voxel (oclusión + sombra + cara).");
91}
92
93/// Renderiza el ser SDF sobre el piso voxel, cámara 3/4 al torso/cara.
94fn render_ser(hal: &Hal, renderer: &mut Renderer, grid: &VoxelGrid, ser: llimphi_3d::SdfSer) -> Vec<u8> {
95    let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, grid);
96    vr.sun_dir = normalize([0.4, 0.6, 0.7]);
97    vr.sers.push(ser);
98
99    // Cámara en coords CENTRADAS (el shader hace `ro = cam_eye + dim/2`): mirar al
100    // centro del cuerpo (~55% de la altura del ser sobre el piso).
101    let alto = 72.0f32;
102    let look = Vec3::new(
103        0.0,
104        FLOOR_H as f32 + alto * 0.55 - DIM as f32 * 0.5,
105        0.0,
106    );
107    let yaw: f32 = std::env::var("YAW").ok().and_then(|s| s.parse().ok()).unwrap_or(28.0);
108    let camera = Camera3d::orbit(look, yaw.to_radians(), 10_f32.to_radians(), alto * 1.9);
109
110    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
111        label: Some("inter"),
112        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
113        mip_level_count: 1,
114        sample_count: 1,
115        dimension: wgpu::TextureDimension::D2,
116        format: FMT,
117        usage: wgpu::TextureUsages::STORAGE_BINDING
118            | wgpu::TextureUsages::TEXTURE_BINDING
119            | wgpu::TextureUsages::RENDER_ATTACHMENT
120            | wgpu::TextureUsages::COPY_SRC,
121        view_formats: &[],
122    });
123    let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
124    let base = vello::Scene::new();
125    renderer
126        .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(BG[0], BG[1], BG[2], 255))
127        .expect("base");
128    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
129    vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), &camera);
130    hal.queue.submit(std::iter::once(enc.finish()));
131    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
132    readback(hal, &inter)
133}
134
135fn normalize(v: [f32; 3]) -> [f32; 3] {
136    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
137    [v[0] / l, v[1] / l, v[2] / l]
138}
139
140fn coverage(px: &[u8]) -> usize {
141    (0..(W * H) as usize)
142        .filter(|&i| {
143            let o = i * 4;
144            (px[o] as i32 - BG[0] as i32).abs()
145                + (px[o + 1] as i32 - BG[1] as i32).abs()
146                + (px[o + 2] as i32 - BG[2] as i32).abs()
147                > 12
148        })
149        .count()
150}
151
152fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
153    let unpadded = (W * 4) as usize;
154    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
155    let padded = unpadded.div_ceil(align) * align;
156    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
157        label: Some("readback"),
158        size: (padded * H as usize) as u64,
159        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
160        mapped_at_creation: false,
161    });
162    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
163    enc.copy_texture_to_buffer(
164        wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
165        wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } },
166        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
167    );
168    hal.queue.submit(std::iter::once(enc.finish()));
169    let slice = buf.slice(..);
170    let (tx, rx) = std::sync::mpsc::channel();
171    slice.map_async(wgpu::MapMode::Read, move |r| {
172        let _ = tx.send(r);
173    });
174    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
175    rx.recv().unwrap().unwrap();
176    let data = slice.get_mapped_range();
177    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
178    for row in 0..H as usize {
179        let s = row * padded;
180        pixels.extend_from_slice(&data[s..s + unpadded]);
181    }
182    drop(data);
183    buf.unmap();
184    pixels
185}
186
187fn write_png(pixels: &[u8], path: &str) {
188    let file = File::create(path).expect("png");
189    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
190    enc.set_color(png::ColorType::Rgba);
191    enc.set_depth(png::BitDepth::Eight);
192    let mut w = enc.write_header().unwrap();
193    w.write_image_data(pixels).unwrap();
194}