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-3d/examples/postfx_demo.rs Rust · 172 lines · 6.4 KB History Who touched what raw

feat(3d): cosechar efectos genéricos de supay + refrescar voxel/director 333dca1d5 · Sergio ·

1//! Demo headless del post-proceso [`PostFx`] (SSAA + bloom) cosechado de supay.
2//!
3//! Rinde el mismo cubo dos veces sobre un fondo vello: una con bloom apagado
4//! (`bloom_strength = 0`) y otra con el bloom por defecto. Vuelca ambos PNG y
5//! certifica **por texto** (sin mirar) que el bloom suma energía luminosa
6//! alrededor de los bordes brillantes: cuenta píxeles cuya luminancia creció y
7//! el delta de luminancia total.
8//!
9//! `cargo run -p llimphi-3d --example postfx_demo --release`
10
11use std::fs::File;
12use std::io::BufWriter;
13
14use llimphi_3d::glam::Vec3;
15use llimphi_3d::{Camera3d, PostFx, PostFxConfig, Renderer3d};
16use llimphi_hal::{wgpu, Hal};
17use llimphi_raster::peniko::Color;
18use llimphi_raster::{vello, Renderer};
19
20const W: u32 = 720;
21const H: u32 = 480;
22const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
23
24fn main() {
25    let hal = pollster::block_on(Hal::new(None)).expect("hal");
26    let mut renderer = Renderer::new(&hal).expect("renderer");
27    let mut r3d = Renderer3d::new(&hal.device, FMT);
28    let mut fx = PostFx::new(&hal.device, FMT);
29
30    let camera = Camera3d::orbit(Vec3::ZERO, 35_f32.to_radians(), 25_f32.to_radians(), 4.0);
31
32    // Sin bloom (sólo SSAA).
33    let off = render(&hal, &mut renderer, &mut r3d, &mut fx, &camera, 0.0);
34    write_png(&off, "postfx_off.png");
35    // Con bloom por defecto.
36    let on = render(&hal, &mut renderer, &mut r3d, &mut fx, &camera, PostFxConfig::default().bloom_strength);
37    write_png(&on, "postfx_on.png");
38
39    // --- Certificación numérica (sin mirar el PNG) ---
40    let (mut grew, mut sum_off, mut sum_on, mut max_gain) = (0u64, 0f64, 0f64, 0f32);
41    for (po, pn) in off.chunks_exact(4).zip(on.chunks_exact(4)) {
42        let lo = lum(po);
43        let ln = lum(pn);
44        sum_off += lo as f64;
45        sum_on += ln as f64;
46        if ln > lo + 0.004 {
47            grew += 1;
48        }
49        max_gain = max_gain.max(ln - lo);
50    }
51    let total = (W * H) as f64;
52    eprintln!("postfx_demo: {W}x{H}, supersample={}", fx.config().supersample);
53    eprintln!("  píxeles que ganaron luz con bloom: {grew} ({:.2}%)", 100.0 * grew as f64 / total);
54    eprintln!("  luminancia media off={:.4} on={:.4} (Δ={:+.4})", sum_off / total, sum_on / total, (sum_on - sum_off) / total);
55    eprintln!("  máxima ganancia en un píxel: {max_gain:.4}");
56    eprintln!("  PNG: postfx_off.png / postfx_on.png");
57    assert!(grew > 0, "el bloom no agregó glow en ningún píxel — algo está mal");
58    assert!(sum_on > sum_off, "el bloom debería sumar luminancia total");
59}
60
61fn lum(p: &[u8]) -> f32 {
62    (0.2126 * p[0] as f32 + 0.7152 * p[1] as f32 + 0.0722 * p[2] as f32) / 255.0
63}
64
65fn render(
66    hal: &Hal,
67    renderer: &mut Renderer,
68    r3d: &mut Renderer3d,
69    fx: &mut PostFx,
70    camera: &Camera3d,
71    bloom_strength: f32,
72) -> Vec<u8> {
73    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
74        label: Some("inter"),
75        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
76        mip_level_count: 1,
77        sample_count: 1,
78        dimension: wgpu::TextureDimension::D2,
79        format: FMT,
80        usage: wgpu::TextureUsages::STORAGE_BINDING
81            | wgpu::TextureUsages::TEXTURE_BINDING
82            | wgpu::TextureUsages::RENDER_ATTACHMENT
83            | wgpu::TextureUsages::COPY_SRC,
84        view_formats: &[],
85    });
86    let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
87
88    // (1) Fondo vello (la base que el blit debe PRESERVAR con LoadOp::Load).
89    let base = vello::Scene::new();
90    renderer
91        .render_to_view(hal, &base, &inter_view, W, H, Color::from_rgba8(18, 22, 32, 255))
92        .expect("render base");
93
94    // (2) Pase 3D envuelto en PostFx (SSAA + bloom) → blit sobre el fondo.
95    let cfg = PostFxConfig { bloom_strength, ..PostFxConfig::default() };
96    fx.set_config(cfg);
97    r3d.upload(&hal.queue, W as f32 / H as f32, camera);
98    fx.prepare(&hal.device, &hal.queue, (W, H));
99    let mut enc = hal
100        .device
101        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("postfx") });
102    {
103        let mut pass = fx.scene_pass(
104            &mut enc,
105            wgpu::Color { r: 0.07, g: 0.086, b: 0.125, a: 1.0 },
106        );
107        r3d.draw(&mut pass);
108    }
109    fx.resolve(&mut enc, &inter_view);
110    hal.queue.submit(std::iter::once(enc.finish()));
111    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
112
113    readback(hal, &inter)
114}
115
116fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
117    let unpadded = (W * 4) as usize;
118    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
119    let padded = unpadded.div_ceil(align) * align;
120    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
121        label: Some("readback"),
122        size: (padded * H as usize) as u64,
123        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
124        mapped_at_creation: false,
125    });
126    let mut enc = hal
127        .device
128        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
129    enc.copy_texture_to_buffer(
130        wgpu::TexelCopyTextureInfo {
131            texture: target,
132            mip_level: 0,
133            origin: wgpu::Origin3d::ZERO,
134            aspect: wgpu::TextureAspect::All,
135        },
136        wgpu::TexelCopyBufferInfo {
137            buffer: &buf,
138            layout: wgpu::TexelCopyBufferLayout {
139                offset: 0,
140                bytes_per_row: Some(padded as u32),
141                rows_per_image: Some(H),
142            },
143        },
144        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
145    );
146    hal.queue.submit(std::iter::once(enc.finish()));
147    let slice = buf.slice(..);
148    let (tx, rx) = std::sync::mpsc::channel();
149    slice.map_async(wgpu::MapMode::Read, move |r| {
150        let _ = tx.send(r);
151    });
152    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
153    rx.recv().unwrap().unwrap();
154    let data = slice.get_mapped_range();
155    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
156    for row in 0..H as usize {
157        let s = row * padded;
158        pixels.extend_from_slice(&data[s..s + unpadded]);
159    }
160    drop(data);
161    buf.unmap();
162    pixels
163}
164
165fn write_png(pixels: &[u8], path: &str) {
166    let file = File::create(path).expect("png");
167    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
168    enc.set_color(png::ColorType::Rgba);
169    enc.set_depth(png::BitDepth::Eight);
170    let mut w = enc.write_header().unwrap();
171    w.write_image_data(pixels).unwrap();
172}