gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc llimphi-3d/examples/sky_billboard_demo.rs Rust · 255 líneas · 9.1 KB Historia Quién tocó qué crudo

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

1//! Demo headless de [`SkyBackdrop`] + [`Billboards`].
2//!
3//! Compone un cielo cilíndrico (panorama con una barra verde brillante) y un
4//! billboard blanco en el origen, en un pase con depth. Certifica **por texto**:
5//!  - al girar el `yaw`, la barra del cielo se **desplaza horizontalmente**
6//!    (muestreo cilíndrico correcto);
7//!  - el billboard aparece **centrado** y **de cara a la cámara** (su cuadro
8//!    blanco cubre el centro de la imagen en ambos yaws).
9//!
10//! `cargo run -p llimphi-3d --example sky_billboard_demo --release`
11
12use std::fs::File;
13use std::io::BufWriter;
14
15use llimphi_3d::glam::Vec3;
16use llimphi_3d::{Billboard, Billboards, Camera3d, SkyBackdrop, SkyParams};
17use llimphi_hal::{wgpu, Hal};
18use llimphi_raster::peniko::Color;
19use llimphi_raster::{vello, Renderer};
20
21const W: u32 = 640;
22const H: u32 = 400;
23const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
24const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
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    // Cielo: 512×64, fondo azul oscuro + una barra verde brillante en u≈0.
31    let (sw, sh) = (512u32, 64u32);
32    let mut sky_px = vec![0u8; (sw * sh * 4) as usize];
33    for y in 0..sh {
34        for x in 0..sw {
35            let i = ((y * sw + x) * 4) as usize;
36            let bar = x < 6; // barra angosta en el borde izquierdo (u=0)
37            let (r, g, b) = if bar { (40, 255, 40) } else { (12, 14, 30) };
38            sky_px[i] = r;
39            sky_px[i + 1] = g;
40            sky_px[i + 2] = b;
41            sky_px[i + 3] = 255;
42        }
43    }
44    let mut sky = SkyBackdrop::new(&hal.device, FMT);
45    sky.set_texture(&hal.device, &hal.queue, sw, sh, &sky_px);
46
47    // Billboard: atlas 16×16 blanco opaco; un quad en el origen.
48    let atlas = vec![255u8; 16 * 16 * 4];
49    let mut bb = Billboards::new(&hal.device, FMT);
50    bb.set_atlas(&hal.device, &hal.queue, 16, 16, &atlas);
51    bb.set_billboards(
52        &hal.device,
53        &[Billboard {
54            center: [0.0, 0.0, 0.0],
55            size: [1.4, 1.4],
56            uv_min: [0.0, 0.0],
57            uv_max: [1.0, 1.0],
58            tint: [1.0, 1.0, 1.0, 1.0],
59        }],
60    );
61
62    let bar0 = {
63        let px = render(&hal, &mut renderer, &sky, &bb, 0.0);
64        write_png(&px, "sky_billboard_yaw0.png");
65        certify_frame(&px, "yaw=0.0")
66    };
67    let bar1 = {
68        let px = render(&hal, &mut renderer, &sky, &bb, 0.5);
69        write_png(&px, "sky_billboard_yaw05.png");
70        certify_frame(&px, "yaw=0.5")
71    };
72
73    eprintln!("sky_billboard_demo: {W}x{H}");
74    eprintln!("  barra de cielo: x@yaw0={bar0}  x@yaw0.5={bar1}  (Δ={})", (bar1 - bar0).abs());
75    assert!(
76        (bar1 - bar0).abs() > 20,
77        "la barra del cielo debería desplazarse al girar el yaw"
78    );
79    eprintln!("  OK — cielo cilíndrico gira con el yaw + billboard centrado de cara a la cámara.");
80}
81
82/// Mide: (a) que el billboard blanco cubra el centro; (b) la columna x con más
83/// verde (la barra de cielo). Devuelve esa columna x.
84fn certify_frame(px: &[u8], tag: &str) -> i32 {
85    // (a) Centro blanco (el billboard).
86    let ci = ((H / 2 * W + W / 2) * 4) as usize;
87    let (r, g, b) = (px[ci], px[ci + 1], px[ci + 2]);
88    assert!(
89        r > 200 && g > 200 && b > 200,
90        "{tag}: el billboard debería pintar blanco en el centro (vi {r},{g},{b})"
91    );
92    // Esquina (fondo de cielo, no billboard): no debe ser blanca.
93    let cor = 0usize;
94    assert!(
95        !(px[cor] > 200 && px[cor + 1] > 200 && px[cor + 2] > 200),
96        "{tag}: la esquina no debería ser el billboard"
97    );
98
99    // (b) Columna con más verde puro (verde alto, rojo/azul bajos) = la barra.
100    // Muestreamos una banda superior para no contar el billboard.
101    let mut best_x = 0i32;
102    let mut best = -1.0f32;
103    for x in 0..W as usize {
104        let mut acc = 0.0f32;
105        for y in 0..(H as usize / 4) {
106            let i = (y * W as usize + x) * 4;
107            let (r, g, b) = (px[i] as f32, px[i + 1] as f32, px[i + 2] as f32);
108            acc += (g - r).max(0.0) + (g - b).max(0.0);
109        }
110        if acc > best {
111            best = acc;
112            best_x = x as i32;
113        }
114    }
115    best_x
116}
117
118fn render(
119    hal: &Hal,
120    renderer: &mut Renderer,
121    sky: &SkyBackdrop,
122    bb: &Billboards,
123    yaw: f32,
124) -> Vec<u8> {
125    let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
126        label: Some("inter"),
127        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
128        mip_level_count: 1,
129        sample_count: 1,
130        dimension: wgpu::TextureDimension::D2,
131        format: FMT,
132        usage: wgpu::TextureUsages::STORAGE_BINDING
133            | wgpu::TextureUsages::TEXTURE_BINDING
134            | wgpu::TextureUsages::RENDER_ATTACHMENT
135            | wgpu::TextureUsages::COPY_SRC,
136        view_formats: &[],
137    });
138    let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
139    let depth = hal.device.create_texture(&wgpu::TextureDescriptor {
140        label: Some("depth"),
141        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
142        mip_level_count: 1,
143        sample_count: 1,
144        dimension: wgpu::TextureDimension::D2,
145        format: DEPTH,
146        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
147        view_formats: &[],
148    });
149    let depth_view = depth.create_view(&Default::default());
150
151    let base = vello::Scene::new();
152    renderer
153        .render_to_view(hal, &base, &inter_view, W, H, Color::from_rgba8(0, 0, 0, 255))
154        .expect("render base");
155
156    let aspect = W as f32 / H as f32;
157    let camera = Camera3d::orbit(Vec3::ZERO, yaw, 0.0, 4.0);
158    sky.upload(
159        &hal.queue,
160        &SkyParams { yaw, pitch: 0.0, fov_x: std::f32::consts::FRAC_PI_2, aspect, ..SkyParams::default() },
161    );
162    bb.upload(&hal.queue, aspect, &camera);
163
164    let mut enc = hal
165        .device
166        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("sky-bb") });
167    {
168        let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
169            label: Some("sky-bb-pass"),
170            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
171                view: &inter_view,
172                resolve_target: None,
173                depth_slice: None,
174                ops: wgpu::Operations {
175                    load: wgpu::LoadOp::Load,
176                    store: wgpu::StoreOp::Store,
177                },
178            })],
179            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
180                view: &depth_view,
181                depth_ops: Some(wgpu::Operations {
182                    load: wgpu::LoadOp::Clear(1.0),
183                    store: wgpu::StoreOp::Store,
184                }),
185                stencil_ops: None,
186            }),
187            timestamp_writes: None,
188            occlusion_query_set: None,
189        });
190        sky.draw(&mut pass); // fondo primero
191        bb.draw(&mut pass); // billboards encima
192    }
193    hal.queue.submit(std::iter::once(enc.finish()));
194    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
195
196    readback(hal, &inter)
197}
198
199fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
200    let unpadded = (W * 4) as usize;
201    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
202    let padded = unpadded.div_ceil(align) * align;
203    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
204        label: Some("readback"),
205        size: (padded * H as usize) as u64,
206        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
207        mapped_at_creation: false,
208    });
209    let mut enc = hal
210        .device
211        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
212    enc.copy_texture_to_buffer(
213        wgpu::TexelCopyTextureInfo {
214            texture: target,
215            mip_level: 0,
216            origin: wgpu::Origin3d::ZERO,
217            aspect: wgpu::TextureAspect::All,
218        },
219        wgpu::TexelCopyBufferInfo {
220            buffer: &buf,
221            layout: wgpu::TexelCopyBufferLayout {
222                offset: 0,
223                bytes_per_row: Some(padded as u32),
224                rows_per_image: Some(H),
225            },
226        },
227        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
228    );
229    hal.queue.submit(std::iter::once(enc.finish()));
230    let slice = buf.slice(..);
231    let (tx, rx) = std::sync::mpsc::channel();
232    slice.map_async(wgpu::MapMode::Read, move |r| {
233        let _ = tx.send(r);
234    });
235    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
236    rx.recv().unwrap().unwrap();
237    let data = slice.get_mapped_range();
238    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
239    for row in 0..H as usize {
240        let s = row * padded;
241        pixels.extend_from_slice(&data[s..s + unpadded]);
242    }
243    drop(data);
244    buf.unmap();
245    pixels
246}
247
248fn write_png(pixels: &[u8], path: &str) {
249    let file = File::create(path).expect("png");
250    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
251    enc.set_color(png::ColorType::Rgba);
252    enc.set_depth(png::BitDepth::Eight);
253    let mut w = enc.write_header().unwrap();
254    w.write_image_data(pixels).unwrap();
255}