tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel/examples/manada_lod_demo.rs Rust · 213 líneas · 8.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! Certificación del **reparto con LOD**: una bandada de 16 humanoides alejándose
2//! del ojo, repartida por [`reparto_sdf`] dentro del presupuesto del uniform
3//! (16 seres / 240 primitivas).
4//!
5//! Sin LOD, 16 caras enteras son ~500 primitivas: el empacador cortaba la lista y los
6//! del fondo desaparecían (o peor, salían mutilados). Acá se ve que entran los 16 y
7//! —lo que importa— que la degradación NO se nota: el que perdió la cara está lo
8//! bastante lejos como para que no hubiera cara que ver.
9//!
10//! `cargo run -p llimphi-voxel --example manada_lod_demo --release`
11
12use std::fs::File;
13use std::io::BufWriter;
14
15use llimphi_3d::glam::Vec3;
16use llimphi_3d::{Camera3d, VoxelGrid, VoxelRenderer, VOXEL_MAX_SER_PRIMS};
17use llimphi_hal::{wgpu, Hal};
18use llimphi_raster::peniko::Color;
19use llimphi_raster::{vello, Renderer};
20use llimphi_voxel::{reparto_sdf, Age, CharSpec, Clip, DetalleSer, EnCuadro};
21
22const W: u32 = 1280;
23const H: u32 = 720;
24const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25const BG: [u8; 3] = [22, 26, 36];
26
27const DIM: u32 = 128;
28const FLOOR_H: u32 = 2;
29/// Cuántos y con qué separación (en unidades de mundo; un adulto mide ~1.8).
30const N: usize = 16;
31const PASO: f32 = 3.4;
32
33fn main() {
34    let hal = pollster::block_on(Hal::new(None)).expect("hal");
35    let mut renderer = Renderer::new(&hal).expect("renderer");
36    let dir = std::env::var("CLAUDE_JOB_DIR").map(|d| format!("{d}/tmp")).unwrap_or("/tmp".into());
37
38    // Bandada en diagonal (para que no se tapen entre sí), alejándose en Z.
39    let specs: Vec<CharSpec> = (0..N)
40        .map(|i| {
41            let mut c = CharSpec::new(format!("ser{i}"), Age::Adult);
42            // Colores variados para distinguirlos (hash simple por índice).
43            let t = i as f32 / N as f32;
44            c.shirt = [0.20 + 0.5 * t, 0.62 - 0.3 * t, 0.55 + 0.3 * (1.0 - t)];
45            c
46        })
47        .collect();
48
49    let x0 = DIM as f32 * 0.5;
50    let z0 = DIM as f32 * 0.18;
51    let cuadro: Vec<EnCuadro> = specs
52        .iter()
53        .enumerate()
54        .map(|(i, spec)| EnCuadro {
55            spec,
56            pos: Vec3::new(
57                x0 + (i as f32 * 0.9).sin() * 4.0 + (i as f32 % 2.0 - 0.5) * 3.0,
58                FLOOR_H as f32,
59                z0 + i as f32 * PASO,
60            ),
61            yaw: 0.0, // todos de frente a la cámara
62            clip: if i % 3 == 0 { Clip::Wave } else { Clip::Walk },
63            fase: i as f32 * 0.7,
64        })
65        .collect();
66
67    // Cámara delante de la fila, a la altura de la cara del primero.
68    let half = DIM as f32 * 0.5;
69    let ojo_grid = Vec3::new(x0, FLOOR_H as f32 + 2.6, z0 - 4.2);
70    let camera = Camera3d::orbit(
71        Vec3::new(x0 - half, FLOOR_H as f32 + 1.4 - half, z0 + 10.0 - half),
72        0.0,
73        6_f32.to_radians(),
74        16.0,
75    );
76
77    let sers = reparto_sdf(&cuadro, ojo_grid, true);
78
79    println!("manada_lod_demo — {N} humanoides, presupuesto {VOXEL_MAX_SER_PRIMS} primitivas");
80    let mut total = 0;
81    for (i, s) in sers.iter().enumerate() {
82        let dist = ojo_grid.distance(Vec3::from(s.pos) + Vec3::new(0.0, 0.9, 0.0));
83        let d = DetalleSer::por_distancia(dist, 1.8);
84        total += s.prims.len();
85        println!(
86            "  #{i:2} dist={dist:5.1}  {:?}\t{} prims\t(acumulado {total})",
87            d,
88            s.prims.len()
89        );
90    }
91    println!("  → {} seres, {total} primitivas de {VOXEL_MAX_SER_PRIMS}", sers.len());
92    assert_eq!(sers.len(), N, "entran los {N}");
93    assert!(total <= VOXEL_MAX_SER_PRIMS, "presupuesto excedido: {total}");
94    // Nadie mutilado: el cuerpo entero son 10 primitivas.
95    for (i, s) in sers.iter().enumerate() {
96        assert!(s.prims.len() >= 10, "#{i} salió incompleto ({} prims)", s.prims.len());
97    }
98
99    // Piso damero.
100    let mut grid = VoxelGrid::new([DIM, DIM, DIM]);
101    for x in 0..DIM {
102        for z in 0..DIM {
103            for y in 0..FLOOR_H {
104                let c = if (x / 6 + z / 6) % 2 == 0 { [150, 146, 136] } else { [120, 116, 110] };
105                grid.set(x, y, z, c);
106            }
107        }
108    }
109
110    let px = render(&hal, &mut renderer, &grid, sers, &camera);
111    let cov = coverage(&px);
112    let out = format!("{dir}/manada_lod.png");
113    write_png(&px, &out);
114    println!("✓ {cov} px cubiertos → {out}");
115    assert!(cov > 20_000, "la bandada se renderizó ({cov} px)");
116}
117
118fn render(
119    hal: &Hal,
120    renderer: &mut Renderer,
121    grid: &VoxelGrid,
122    sers: Vec<llimphi_3d::SdfSer>,
123    camera: &Camera3d,
124) -> Vec<u8> {
125    let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, grid);
126    vr.sun_dir = normalize([0.4, 0.7, 0.55]);
127    vr.sers = sers;
128
129    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
130        label: Some("inter"),
131        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
132        mip_level_count: 1,
133        sample_count: 1,
134        dimension: wgpu::TextureDimension::D2,
135        format: FMT,
136        usage: wgpu::TextureUsages::STORAGE_BINDING
137            | wgpu::TextureUsages::TEXTURE_BINDING
138            | wgpu::TextureUsages::RENDER_ATTACHMENT
139            | wgpu::TextureUsages::COPY_SRC,
140        view_formats: &[],
141    });
142    let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
143    let base = vello::Scene::new();
144    renderer
145        .render_to_view(hal, &base, &view, W, H, Color::from_rgba8(BG[0], BG[1], BG[2], 255))
146        .expect("base");
147    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
148    vr.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), camera);
149    hal.queue.submit(std::iter::once(enc.finish()));
150    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
151    readback(hal, &inter)
152}
153
154fn normalize(v: [f32; 3]) -> [f32; 3] {
155    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
156    [v[0] / l, v[1] / l, v[2] / l]
157}
158
159fn coverage(px: &[u8]) -> usize {
160    (0..(W * H) as usize)
161        .filter(|&i| {
162            let o = i * 4;
163            (px[o] as i32 - BG[0] as i32).abs()
164                + (px[o + 1] as i32 - BG[1] as i32).abs()
165                + (px[o + 2] as i32 - BG[2] as i32).abs()
166                > 12
167        })
168        .count()
169}
170
171fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
172    let unpadded = (W * 4) as usize;
173    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
174    let padded = unpadded.div_ceil(align) * align;
175    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
176        label: Some("readback"),
177        size: (padded * H as usize) as u64,
178        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
179        mapped_at_creation: false,
180    });
181    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
182    enc.copy_texture_to_buffer(
183        wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
184        wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H) } },
185        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
186    );
187    hal.queue.submit(std::iter::once(enc.finish()));
188    let slice = buf.slice(..);
189    let (tx, rx) = std::sync::mpsc::channel();
190    slice.map_async(wgpu::MapMode::Read, move |r| {
191        let _ = tx.send(r);
192    });
193    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
194    rx.recv().unwrap().unwrap();
195    let data = slice.get_mapped_range();
196    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
197    for row in 0..H as usize {
198        let s = row * padded;
199        pixels.extend_from_slice(&data[s..s + unpadded]);
200    }
201    drop(data);
202    buf.unmap();
203    pixels
204}
205
206fn write_png(pixels: &[u8], path: &str) {
207    let file = File::create(path).expect("png");
208    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
209    enc.set_color(png::ColorType::Rgba);
210    enc.set_depth(png::BitDepth::Eight);
211    let mut w = enc.write_header().unwrap();
212    w.write_image_data(pixels).unwrap();
213}