tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel/examples/comun/mod.rs Rust · 344 lines · 12.2 KB History Who touched what raw

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! **Arnés de tomas para los catálogos SDF** — lo comparten `bestiario_demo`,
2//! `jardin_demo` y `multitud_demo` (`#[path = "comun/mod.rs"] mod comun;`; cargo no
3//! lo toma por ejemplo porque no es `examples/*.rs` ni `examples/*/main.rs`).
4//!
5//! Hace tres cosas y ninguna es 3D: **encuadra** (cada bicho o planta se escala para
6//! que su esfera envolvente ocupe lo mismo — si no, la ballena llena el cuadro y la
7//! hormiga es un punto), **compone** la hoja de contacto, y **etiqueta** cada casilla
8//! con su nombre. Sin el nombre, una hoja de ochenta y cinco animales obliga a
9//! contar filas con el dedo.
10
11use std::fs::File;
12use std::io::BufWriter;
13
14use llimphi_3d::glam::Vec3;
15use llimphi_3d::{Atmosphere, Camera3d, Scene3d, SdfSer, VoxelGrid, VoxelRenderer};
16use llimphi_hal::{wgpu, Hal};
17use llimphi_raster::peniko::{Blob, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat};
18use llimphi_raster::{kurbo, vello, Renderer};
19
20pub const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
21/// Lado del mundito de cada casilla (voxels).
22pub const DIM: u32 = 128;
23/// Alto del piso.
24pub const PISO: u32 = 2;
25/// Radio al que se lleva la esfera envolvente de todo sujeto: el encuadre común.
26pub const RADIO: f32 = 30.0;
27
28pub struct Toma {
29    pub hal: Hal,
30    pub renderer: Renderer,
31    pub ts: llimphi_text::Typesetter,
32}
33
34impl Toma {
35    pub fn nueva() -> Self {
36        let hal = pollster::block_on(Hal::new(None)).expect("hal");
37        let renderer = Renderer::new(&hal).expect("renderer");
38        Self { hal, renderer, ts: llimphi_text::Typesetter::default() }
39    }
40
41    /// Piso a cuadros del tamaño del mundito de una casilla.
42    pub fn piso(&self, claro: [u8; 3], oscuro: [u8; 3]) -> VoxelGrid {
43        let mut g = VoxelGrid::new([DIM, DIM, DIM]);
44        for x in 0..DIM {
45            for z in 0..DIM {
46                let c = if (x / 10 + z / 10) % 2 == 0 { claro } else { oscuro };
47                for y in 0..PISO {
48                    g.set(x, y, z, c);
49                }
50            }
51        }
52        g
53    }
54
55    /// **Una casilla**: el sujeto (ya posado) sobre el piso, encuadrado. `yaw_deg`
56    /// gira la cámara alrededor.
57    pub fn casilla(
58        &mut self,
59        grid: &VoxelGrid,
60        sers: Vec<SdfSer>,
61        centro_y: f32,
62        radio: f32,
63        yaw_deg: f32,
64        w: u32,
65        h: u32,
66    ) -> Vec<u8> {
67        let mut vr = VoxelRenderer::new(&self.hal.device, &self.hal.queue, FMT, grid);
68        vr.sun_dir = normaliza([0.40, 0.62, 0.68]);
69        vr.atmosphere = Atmosphere {
70            sky_zenith: [78, 122, 186],
71            sky_horizon: [206, 222, 240],
72            fog_density: 0.10 / DIM as f32,
73            god_rays: 0.0,
74        };
75        vr.sers = sers;
76        let half = DIM as f32 * 0.5;
77        let look = Vec3::new(0.0, PISO as f32 + centro_y - half, 0.0);
78        let camera = Camera3d::orbit(
79            look,
80            yaw_deg.to_radians(),
81            16_f32.to_radians(),
82            // 2.15, no 3.05: con el encuadre viejo el sujeto ocupaba un cuarto
83            // de la casilla y la hoja de contacto no servía para mirar nada.
84            radio * 2.15,
85        );
86        let mut scene = Scene3d::new();
87        self.render(&mut scene, &mut vr, &camera, w, h)
88    }
89
90    fn render(
91        &mut self,
92        scene: &mut Scene3d,
93        vr: &mut VoxelRenderer,
94        camera: &Camera3d,
95        w: u32,
96        h: u32,
97    ) -> Vec<u8> {
98        let tex = self.textura(w, h);
99        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
100        self.renderer
101            .render_to_view(&self.hal, &vello::Scene::new(), &view, w, h, Color::from_rgba8(16, 18, 26, 255))
102            .expect("base");
103        let mut enc = self
104            .hal
105            .device
106            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("casilla") });
107        scene.render(
108            &self.hal.device,
109            &self.hal.queue,
110            &mut enc,
111            &view,
112            (w, h),
113            camera,
114            Some(vr),
115            &[],
116        );
117        self.hal.queue.submit(std::iter::once(enc.finish()));
118        let _ = self.hal.device.poll(wgpu::PollType::wait_indefinitely());
119        leer(&self.hal, &tex, w, h)
120    }
121
122    fn textura(&self, w: u32, h: u32) -> wgpu::Texture {
123        self.hal.device.create_texture(&wgpu::TextureDescriptor {
124            label: Some("toma"),
125            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
126            mip_level_count: 1,
127            sample_count: 1,
128            dimension: wgpu::TextureDimension::D2,
129            format: FMT,
130            usage: wgpu::TextureUsages::STORAGE_BINDING
131                | wgpu::TextureUsages::TEXTURE_BINDING
132                | wgpu::TextureUsages::RENDER_ATTACHMENT
133                | wgpu::TextureUsages::COPY_SRC,
134            view_formats: &[],
135        })
136    }
137
138    /// **La hoja de contacto**: pega las casillas en una grilla y le escribe el
139    /// nombre a cada una (banda inferior), más un título arriba.
140    pub fn hoja(
141        &mut self,
142        titulo: &str,
143        casillas: &[(String, Vec<u8>)],
144        cols: usize,
145        tw: u32,
146        th: u32,
147    ) -> (Vec<u8>, u32, u32) {
148        let filas = casillas.len().div_ceil(cols) as u32;
149        let cab = 46u32;
150        let w = cols as u32 * tw;
151        let h = filas * th + cab;
152        // 1) pegado en CPU.
153        let mut px = vec![0u8; (w * h * 4) as usize];
154        for (i, y) in (0..h).enumerate() {
155            let _ = i;
156            for x in 0..w {
157                let o = ((y * w + x) * 4) as usize;
158                px[o] = 20;
159                px[o + 1] = 22;
160                px[o + 2] = 30;
161                px[o + 3] = 255;
162            }
163        }
164        for (i, (_, tile)) in casillas.iter().enumerate() {
165            let cx = (i % cols) as u32 * tw;
166            let cy = cab + (i / cols) as u32 * th;
167            for y in 0..th.min(th) {
168                let dst = (((cy + y) * w + cx) * 4) as usize;
169                let src = (y * tw * 4) as usize;
170                let n = (tw * 4) as usize;
171                if src + n <= tile.len() && dst + n <= px.len() {
172                    px[dst..dst + n].copy_from_slice(&tile[src..src + n]);
173                }
174            }
175        }
176        // 2) segunda pasada: la imagen como fondo y el texto encima (vello).
177        let img = ImageBrush::new(ImageData {
178            data: Blob::new(std::sync::Arc::new(px)),
179            format: ImageFormat::Rgba8,
180            alpha_type: ImageAlphaType::Alpha,
181            width: w,
182            height: h,
183        });
184        let mut scene = vello::Scene::new();
185        scene.draw_image(img.as_ref(), kurbo::Affine::IDENTITY);
186        let blanco = Color::from_rgba8(240, 240, 236, 255);
187        let sombra = Color::from_rgba8(12, 12, 16, 210);
188        // Título.
189        let lay = texto(&mut self.ts, titulo, 30.0, w as f32 - 24.0);
190        llimphi_text::draw_layout(&mut scene, &lay, blanco, (16.0, 8.0));
191        for (i, (nombre, _)) in casillas.iter().enumerate() {
192            let cx = (i % cols) as f32 * tw as f32;
193            let cy = cab as f32 + (i / cols) as f32 * th as f32;
194            // Banda oscura para que el nombre se lea sobre cualquier fondo.
195            scene.fill(
196                vello::peniko::Fill::NonZero,
197                kurbo::Affine::IDENTITY,
198                sombra,
199                None,
200                &kurbo::Rect::new(
201                    cx as f64,
202                    (cy + th as f32 - 24.0) as f64,
203                    (cx + tw as f32) as f64,
204                    (cy + th as f32) as f64,
205                ),
206            );
207            let lay = texto(&mut self.ts, nombre, 17.0, tw as f32 - 12.0);
208            llimphi_text::draw_layout(
209                &mut scene,
210                &lay,
211                blanco,
212                (cx as f64 + 8.0, (cy + th as f32 - 22.0) as f64),
213            );
214        }
215        let tex = self.textura(w, h);
216        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
217        self.renderer
218            .render_to_view(&self.hal, &scene, &view, w, h, Color::from_rgba8(20, 22, 30, 255))
219            .expect("hoja");
220        let _ = self.hal.device.poll(wgpu::PollType::wait_indefinitely());
221        (leer(&self.hal, &tex, w, h), w, h)
222    }
223}
224
225/// El readback, público para los demos que arman su propia toma (la calle de
226/// `multitud_demo` no es una casilla del catálogo).
227pub fn leer_pub(hal: &Hal, tex: &wgpu::Texture, w: u32, h: u32) -> Vec<u8> {
228    leer(hal, tex, w, h)
229}
230
231fn leer(hal: &Hal, tex: &wgpu::Texture, w: u32, h: u32) -> Vec<u8> {
232    let unpadded = (w * 4) as usize;
233    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
234    let padded = unpadded.div_ceil(align) * align;
235    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
236        label: Some("readback"),
237        size: (padded * h as usize) as u64,
238        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
239        mapped_at_creation: false,
240    });
241    let mut enc = hal
242        .device
243        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
244    enc.copy_texture_to_buffer(
245        wgpu::TexelCopyTextureInfo {
246            texture: tex,
247            mip_level: 0,
248            origin: wgpu::Origin3d::ZERO,
249            aspect: wgpu::TextureAspect::All,
250        },
251        wgpu::TexelCopyBufferInfo {
252            buffer: &buf,
253            layout: wgpu::TexelCopyBufferLayout {
254                offset: 0,
255                bytes_per_row: Some(padded as u32),
256                rows_per_image: Some(h),
257            },
258        },
259        wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
260    );
261    hal.queue.submit(std::iter::once(enc.finish()));
262    let slice = buf.slice(..);
263    let (tx, rx) = std::sync::mpsc::channel();
264    slice.map_async(wgpu::MapMode::Read, move |r| {
265        let _ = tx.send(r);
266    });
267    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
268    rx.recv().unwrap().unwrap();
269    let data = slice.get_mapped_range();
270    let mut px = Vec::with_capacity((w * h * 4) as usize);
271    for row in 0..h as usize {
272        let s = row * padded;
273        px.extend_from_slice(&data[s..s + unpadded]);
274    }
275    drop(data);
276    buf.unmap();
277    px
278}
279
280pub fn png(px: &[u8], w: u32, h: u32, path: &str) {
281    let file = File::create(path).expect("png");
282    let mut enc = png::Encoder::new(BufWriter::new(file), w, h);
283    enc.set_color(png::ColorType::Rgba);
284    enc.set_depth(png::BitDepth::Eight);
285    let mut wtr = enc.write_header().unwrap();
286    wtr.write_image_data(px).unwrap();
287}
288
289/// **Cuántos píxeles ocupa el sujeto**: los que difieren de la MISMA casilla
290/// renderizada vacía.
291///
292/// La primera versión comparaba contra un color de cielo escrito a mano y daba
293/// `96000 px` —la casilla entera— para las treinta plantas y los ochenta y cinco
294/// animales, porque el cielo es un degradé y el piso es un damero: casi ningún
295/// píxel coincidía con ese color. Un contador que da siempre el máximo no es un
296/// contador chico, es uno que **miente** (Regla 8.ter), y encima decía «✓ todo
297/// dibujó de sobra» mientras la primera hoja tenía cinco defectos a la vista.
298pub fn ocupacion(px: &[u8], vacia: &[u8]) -> usize {
299    px.chunks_exact(4)
300        .zip(vacia.chunks_exact(4))
301        .filter(|(a, b)| {
302            let d = (a[0] as i32 - b[0] as i32).abs()
303                + (a[1] as i32 - b[1] as i32).abs()
304                + (a[2] as i32 - b[2] as i32).abs();
305            d > 24
306        })
307        .count()
308}
309
310/// Directorio de salida (respeta `CLAUDE_JOB_DIR` como el resto de los demos).
311pub fn dir() -> String {
312    std::env::var("CLAUDE_JOB_DIR")
313        .map(|d| format!("{d}/tmp"))
314        .unwrap_or_else(|_| "/tmp".into())
315}
316
317/// Un layout de una línea: la firma pública del typesetter pide doce argumentos y
318/// once son siempre los mismos acá.
319fn texto(
320    ts: &mut llimphi_text::Typesetter,
321    s: &str,
322    px: f32,
323    ancho: f32,
324) -> llimphi_text::parley::Layout<()> {
325    ts.layout(
326        s,
327        px,
328        Some(ancho),
329        llimphi_text::Alignment::Start,
330        1.15,
331        false,
332        None,
333        400.0,
334        false,
335        false,
336        0.0,
337        0.0,
338    )
339}
340
341fn normaliza(v: [f32; 3]) -> [f32; 3] {
342    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
343    [v[0] / l, v[1] / l, v[2] / l]
344}