tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}