gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc llimphi-3d/examples/mesh_lights_demo.rs Rust · 199 siq'ikuna · 7.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

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}