tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc
llimphi-3d/examples/mesh_lights_demo.rs
Historia
Quién tocó qué
crudo
feat(3d): cosechar efectos genéricos de supay + refrescar voxel/director
1//! Demo headless de las **point-lights en el forward-mesh** ([`Renderer3d`]).
2//!
3//! Pinta un muro plano gris (un quad en el plano XY) con una luz puntual cálida
4//! desplazada al `+X`. Certifica **por texto** (sin mirar el PNG):
5//! - sin luces + ambiente 1.0 el render es plano (mitad izq ≈ der) →
6//! compatibilidad hacia atrás;
7//! - con la luz, la mitad derecha (cerca de la luz) es más brillante que la
8//! izquierda y el píxel más iluminado queda **cálido** (r > b) = el tinte de
9//! la luz.
10//!
11//! `cargo run -p llimphi-3d --example mesh_lights_demo --release`
12
13use std::fs::File;
14use std::io::BufWriter;
15
16use llimphi_3d::glam::Vec3;
17use llimphi_3d::{Camera3d, PointLight, Renderer3d, Vertex3d};
18use llimphi_hal::{wgpu, Hal};
19use llimphi_raster::peniko::Color;
20use llimphi_raster::{vello, Renderer};
21
22const W: u32 = 720;
23const H: u32 = 480;
24const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25
26fn main() {
27 let hal = pollster::block_on(Hal::new(None)).expect("hal");
28 let mut renderer = Renderer::new(&hal).expect("renderer");
29
30 // Muro: quad en el plano XY (z=0), gris medio, mirando a +Z (hacia la cámara).
31 let g = [0.5, 0.5, 0.5];
32 let verts = vec![
33 Vertex3d { pos: [-2.4, -1.6, 0.0], color: g },
34 Vertex3d { pos: [2.4, -1.6, 0.0], color: g },
35 Vertex3d { pos: [2.4, 1.6, 0.0], color: g },
36 Vertex3d { pos: [-2.4, 1.6, 0.0], color: g },
37 ];
38 let indices = vec![0u16, 1, 2, 0, 2, 3];
39 let mut r3d = Renderer3d::with_mesh(&hal.device, FMT, &verts, &indices);
40
41 let camera = Camera3d {
42 eye: Vec3::new(0.0, 0.0, 5.0),
43 target: Vec3::ZERO,
44 ..Camera3d::default()
45 };
46
47 // (A) Sin luces, ambiente pleno → debe quedar plano (control).
48 r3d.lights.clear();
49 r3d.ambient = [1.0, 1.0, 1.0];
50 let flat = render(&hal, &mut renderer, &mut r3d, &camera);
51 write_png(&flat, "mesh_lights_flat.png");
52
53 // (B) Una luz cálida al +X, ambiente bajo.
54 r3d.ambient = [0.08, 0.08, 0.10];
55 r3d.lights = vec![PointLight {
56 pos: [1.7, 0.0, 1.3],
57 color: [1.7, 1.05, 0.5], // cálida (naranja), > 1 para brillo intenso
58 range: 5.5,
59 radius: 0.0, // sin sombras en el forward-mesh
60 }];
61 let lit = render(&hal, &mut renderer, &mut r3d, &camera);
62 write_png(&lit, "mesh_lights_lit.png");
63
64 let (fl, fr, _, _) = halves(&flat);
65 let (ll, lr, warm_r, warm_b) = halves(&lit);
66 eprintln!("mesh_lights_demo: {W}x{H}");
67 eprintln!(" CONTROL (sin luces): lum izq={fl:.1} der={fr:.1} (Δ={:+.1}, debe ~0)", fr - fl);
68 eprintln!(" CON LUZ +X: lum izq={ll:.1} der={lr:.1} (Δ={:+.1}, debe >>0)", lr - ll);
69 eprintln!(" píxel más brillante: r={warm_r:.0} b={warm_b:.0} (r>b ⇒ tinte cálido)");
70 assert!((fr - fl).abs() < 2.0, "sin luces el muro debería ser plano");
71 assert!(lr - ll > 8.0, "la luz +X debería iluminar más la mitad derecha");
72 assert!(warm_r > warm_b + 10.0, "la luz cálida debería dar r > b");
73 eprintln!(" OK — point-lights iluminan el forward-mesh con dirección y tinte.");
74}
75
76/// Devuelve (lum_media_izq, lum_media_der, r_del_pixel_mas_brillante, b_idem).
77fn halves(px: &[u8]) -> (f32, f32, f32, f32) {
78 let (mut sl, mut nl, mut sr, mut nr) = (0f64, 0u64, 0f64, 0u64);
79 let (mut best, mut br, mut bb) = (-1f32, 0f32, 0f32);
80 for y in 0..H as usize {
81 for x in 0..W as usize {
82 let i = (y * W as usize + x) * 4;
83 let (r, gg, b) = (px[i] as f32, px[i + 1] as f32, px[i + 2] as f32);
84 let l = 0.2126 * r + 0.7152 * gg + 0.0722 * b;
85 if x < W as usize / 2 {
86 sl += l as f64;
87 nl += 1;
88 } else {
89 sr += l as f64;
90 nr += 1;
91 }
92 if l > best {
93 best = l;
94 br = r;
95 bb = b;
96 }
97 }
98 }
99 (
100 (sl / nl as f64) as f32,
101 (sr / nr as f64) as f32,
102 br,
103 bb,
104 )
105}
106
107fn render(
108 hal: &Hal,
109 renderer: &mut Renderer,
110 r3d: &mut Renderer3d,
111 camera: &Camera3d,
112) -> Vec<u8> {
113 let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
114 label: Some("inter"),
115 size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
116 mip_level_count: 1,
117 sample_count: 1,
118 dimension: wgpu::TextureDimension::D2,
119 format: FMT,
120 usage: wgpu::TextureUsages::STORAGE_BINDING
121 | wgpu::TextureUsages::TEXTURE_BINDING
122 | wgpu::TextureUsages::RENDER_ATTACHMENT
123 | wgpu::TextureUsages::COPY_SRC,
124 view_formats: &[],
125 });
126 let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
127
128 let base = vello::Scene::new();
129 renderer
130 .render_to_view(hal, &base, &inter_view, W, H, Color::from_rgba8(8, 8, 12, 255))
131 .expect("render base");
132
133 let mut enc = hal
134 .device
135 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("mesh-lights") });
136 r3d.render(&hal.device, &hal.queue, &mut enc, &inter_view, (W, H), camera);
137 hal.queue.submit(std::iter::once(enc.finish()));
138 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
139
140 readback(hal, &inter)
141}
142
143fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
144 let unpadded = (W * 4) as usize;
145 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
146 let padded = unpadded.div_ceil(align) * align;
147 let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
148 label: Some("readback"),
149 size: (padded * H as usize) as u64,
150 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
151 mapped_at_creation: false,
152 });
153 let mut enc = hal
154 .device
155 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
156 enc.copy_texture_to_buffer(
157 wgpu::TexelCopyTextureInfo {
158 texture: target,
159 mip_level: 0,
160 origin: wgpu::Origin3d::ZERO,
161 aspect: wgpu::TextureAspect::All,
162 },
163 wgpu::TexelCopyBufferInfo {
164 buffer: &buf,
165 layout: wgpu::TexelCopyBufferLayout {
166 offset: 0,
167 bytes_per_row: Some(padded as u32),
168 rows_per_image: Some(H),
169 },
170 },
171 wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
172 );
173 hal.queue.submit(std::iter::once(enc.finish()));
174 let slice = buf.slice(..);
175 let (tx, rx) = std::sync::mpsc::channel();
176 slice.map_async(wgpu::MapMode::Read, move |r| {
177 let _ = tx.send(r);
178 });
179 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
180 rx.recv().unwrap().unwrap();
181 let data = slice.get_mapped_range();
182 let mut pixels = Vec::with_capacity((W * H * 4) as usize);
183 for row in 0..H as usize {
184 let s = row * padded;
185 pixels.extend_from_slice(&data[s..s + unpadded]);
186 }
187 drop(data);
188 buf.unmap();
189 pixels
190}
191
192fn write_png(pixels: &[u8], path: &str) {
193 let file = File::create(path).expect("png");
194 let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
195 enc.set_color(png::ColorType::Rgba);
196 enc.set_depth(png::BitDepth::Eight);
197 let mut w = enc.write_header().unwrap();
198 w.write_image_data(pixels).unwrap();
199}