tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc
llimphi-3d/examples/postfx_demo.rs
History
Who touched what
raw
feat(3d): cosechar efectos genéricos de supay + refrescar voxel/director
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}