gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Sutichaykuna

⎇ main llimphi-3d/examples/reflection_demo.rs Rust · 250 siq'ikuna · 9.0 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Demo headless de [`PlanarReflection`].
2//!
3//! Simula un mundo reflejado pintando la textura de reflexión de un color cálido
4//! brillante, y dibuja una superficie de agua horizontal (`y=0`) vista en
5//! ángulo rasante. Certifica **por texto**:
6//!  - la reflexión es **visible** en el agua (hay píxeles cálidos = el color
7//!    reflejado aparece en la superficie);
8//!  - el **Fresnel** funciona: la parte lejana/rasante del agua refleja más
9//!    (más cálida) que la cercana (más tinte azul).
10//!
11//! `cargo run -p llimphi-3d --example reflection_demo --release`
12
13use std::fs::File;
14use std::io::BufWriter;
15
16use llimphi_3d::glam::Vec3;
17use llimphi_3d::{Camera3d, PlanarReflection, ReflectionPlane, SurfaceParams};
18use llimphi_hal::{wgpu, Hal};
19use llimphi_raster::peniko::Color;
20use llimphi_raster::{vello, Renderer};
21
22const W: u32 = 640;
23const H: u32 = 400;
24const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
26
27fn main() {
28    let hal = pollster::block_on(Hal::new(None)).expect("hal");
29    let mut renderer = Renderer::new(&hal).expect("renderer");
30    let mut refl = PlanarReflection::new(&hal.device, FMT);
31
32    // Agua: quad horizontal en y=0, receding hacia el fondo.
33    refl.set_surface_quad(
34        &hal.device,
35        [
36            [-8.0, 0.0, 2.0],   // cerca-izq
37            [8.0, 0.0, 2.0],    // cerca-der
38            [8.0, 0.0, -16.0],  // lejos-der
39            [-8.0, 0.0, -16.0], // lejos-izq
40        ],
41    );
42
43    // Cámara baja mirando casi a ras del agua (ángulo rasante → Fresnel alto lejos).
44    let camera = Camera3d {
45        eye: Vec3::new(0.0, 0.9, 5.0),
46        target: Vec3::new(0.0, 0.2, -6.0),
47        ..Camera3d::default()
48    };
49    let aspect = W as f32 / H as f32;
50    let plane = ReflectionPlane::horizontal_y(0.0);
51
52    refl.prepare(&hal.device, (W, H));
53    refl.upload_surface(
54        &hal.queue,
55        &camera.view_proj(aspect),
56        &plane,
57        &camera.eye.to_array(),
58        &SurfaceParams::default(),
59        (W, H),
60        0.0,
61    );
62
63    let px = render(&hal, &mut renderer, &mut refl);
64    write_png(&px, "reflection_demo.png");
65
66    // --- Certificación ---
67    // Superficie = píxel no-fondo (el fondo vello es muy oscuro).
68    let mut warm = 0u64; // reflexión cálida visible
69    let mut surf = 0u64;
70    let (mut y_min, mut y_max) = (H as i32, 0i32);
71    for y in 0..H as i32 {
72        for x in 0..W as i32 {
73            let i = ((y * W as i32 + x) * 4) as usize;
74            let (r, g, b) = (px[i] as i32, px[i + 1] as i32, px[i + 2] as i32);
75            if r.max(g).max(b) > 25 {
76                surf += 1;
77                y_min = y_min.min(y);
78                y_max = y_max.max(y);
79                if r > 120 {
80                    warm += 1;
81                }
82            }
83        }
84    }
85    assert!(surf > 0, "no se dibujó superficie");
86    let mid = (y_min + y_max) / 2;
87    // R medio en la mitad lejana (arriba) vs cercana (abajo) de la superficie.
88    let (mut rt, mut nt, mut rb, mut nb) = (0f64, 0u64, 0f64, 0u64);
89    for y in y_min..=y_max {
90        for x in 0..W as i32 {
91            let i = ((y * W as i32 + x) * 4) as usize;
92            let (r, g, b) = (px[i] as i32, px[i + 1] as i32, px[i + 2] as i32);
93            if r.max(g).max(b) <= 25 {
94                continue;
95            }
96            if y < mid {
97                rt += r as f64;
98                nt += 1;
99            } else {
100                rb += r as f64;
101                nb += 1;
102            }
103        }
104    }
105    let r_far = rt / nt.max(1) as f64;
106    let r_near = rb / nb.max(1) as f64;
107
108    eprintln!("reflection_demo: {W}x{H}");
109    eprintln!("  superficie: {surf} px (filas {y_min}..{y_max}); reflexión cálida (r>120): {warm} px");
110    eprintln!("  R medio  lejos/rasante={r_far:.1}  cerca={r_near:.1}  (Fresnel: lejos debe > cerca)");
111    assert!(warm > 500, "la reflexión cálida debería verse en el agua");
112    assert!(r_far > r_near + 8.0, "el Fresnel debería reflejar más en la zona rasante");
113    eprintln!("  OK — reflexión planar visible + Fresnel rasante en la superficie genérica.");
114}
115
116fn render(hal: &Hal, renderer: &mut Renderer, refl: &mut PlanarReflection) -> Vec<u8> {
117    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
118        label: Some("inter"),
119        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
120        mip_level_count: 1,
121        sample_count: 1,
122        dimension: wgpu::TextureDimension::D2,
123        format: FMT,
124        usage: wgpu::TextureUsages::STORAGE_BINDING
125            | wgpu::TextureUsages::TEXTURE_BINDING
126            | wgpu::TextureUsages::RENDER_ATTACHMENT
127            | wgpu::TextureUsages::COPY_SRC,
128        view_formats: &[],
129    });
130    let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
131    let depth = hal.device.create_texture(&wgpu::TextureDescriptor {
132        label: Some("depth"),
133        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
134        mip_level_count: 1,
135        sample_count: 1,
136        dimension: wgpu::TextureDimension::D2,
137        format: DEPTH,
138        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
139        view_formats: &[],
140    });
141    let depth_view = depth.create_view(&Default::default());
142
143    let base = vello::Scene::new();
144    renderer
145        .render_to_view(hal, &base, &inter_view, W, H, Color::from_rgba8(6, 6, 10, 255))
146        .expect("render base");
147
148    let mut enc = hal
149        .device
150        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("refl") });
151
152    // (1) Pase de reflexión: en vez de rendir un mundo reflejado, lo simulamos
153    //     limpiando a un color cálido brillante (el "cielo/mundo" reflejado).
154    {
155        let _rp = refl.reflection_pass(
156            &mut enc,
157            wgpu::Color { r: 1.0, g: 0.62, b: 0.30, a: 1.0 },
158        );
159    }
160
161    // (2) Pase principal: superficie reflectante sobre el fondo vello.
162    {
163        let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
164            label: Some("refl-main"),
165            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
166                view: &inter_view,
167                resolve_target: None,
168                depth_slice: None,
169                ops: wgpu::Operations {
170                    load: wgpu::LoadOp::Load,
171                    store: wgpu::StoreOp::Store,
172                },
173            })],
174            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
175                view: &depth_view,
176                depth_ops: Some(wgpu::Operations {
177                    load: wgpu::LoadOp::Clear(1.0),
178                    store: wgpu::StoreOp::Store,
179                }),
180                stencil_ops: None,
181            }),
182            timestamp_writes: None,
183            occlusion_query_set: None,
184            multiview_mask: None,
185        });
186        refl.draw_surface(&mut pass);
187    }
188
189    hal.queue.submit(std::iter::once(enc.finish()));
190    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
191    readback(hal, &inter)
192}
193
194fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
195    let unpadded = (W * 4) as usize;
196    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
197    let padded = unpadded.div_ceil(align) * align;
198    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
199        label: Some("readback"),
200        size: (padded * H as usize) as u64,
201        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
202        mapped_at_creation: false,
203    });
204    let mut enc = hal
205        .device
206        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
207    enc.copy_texture_to_buffer(
208        wgpu::TexelCopyTextureInfo {
209            texture: target,
210            mip_level: 0,
211            origin: wgpu::Origin3d::ZERO,
212            aspect: wgpu::TextureAspect::All,
213        },
214        wgpu::TexelCopyBufferInfo {
215            buffer: &buf,
216            layout: wgpu::TexelCopyBufferLayout {
217                offset: 0,
218                bytes_per_row: Some(padded as u32),
219                rows_per_image: Some(H),
220            },
221        },
222        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
223    );
224    hal.queue.submit(std::iter::once(enc.finish()));
225    let slice = buf.slice(..);
226    let (tx, rx) = std::sync::mpsc::channel();
227    slice.map_async(wgpu::MapMode::Read, move |r| {
228        let _ = tx.send(r);
229    });
230    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
231    rx.recv().unwrap().unwrap();
232    let data = slice.get_mapped_range();
233    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
234    for row in 0..H as usize {
235        let s = row * padded;
236        pixels.extend_from_slice(&data[s..s + unpadded]);
237    }
238    drop(data);
239    buf.unmap();
240    pixels
241}
242
243fn write_png(pixels: &[u8], path: &str) {
244    let file = File::create(path).expect("png");
245    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
246    enc.set_color(png::ColorType::Rgba);
247    enc.set_depth(png::BitDepth::Eight);
248    let mut w = enc.write_header().unwrap();
249    w.write_image_data(pixels).unwrap();
250}