gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel-studio/examples/noche_en_el_pixel.rs Rust · 259 líneas · 11.5 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! **La noche, certificada en el píxel.** Filma la MISMA escena del arranque
2//! (una princesa que camina y saluda con una estrella en la mano) de tres
3//! maneras —de día, de noche, y de noche sin la estrella— y cuenta lo que
4//! cambió, sin mirar:
5//!
6//! - **el cielo se oscurece**: luminancia media de la franja de arriba;
7//! - **hay estrellas**: puntos aislados más claros que el cielo que los rodea,
8//!   en el plano de contraluz (el que encuadra la luna). De día tiene que dar
9//!   cero: es el brazo de control del contador;
10//! - **hay luna**: el píxel más claro del cielo en ese plano;
11//! - **la estrella alumbra**: cuántos píxeles cambian al sacarla, de noche
12//!   contra de día. De día sólo cambia la estrella misma; de noche, además,
13//!   lo que su luz alcanza. Y de día tiene que ser POCO: la estrella mide 0,9
14//!   y cabe en unos cientos de píxeles. Este número fue el que delató que en
15//!   la Iris Xe todos los seres se dibujaban como su esfera envolvente (daba
16//!   ~12.000: la estrella era una bola, y la actriz otra).
17//!
18//! ```sh
19//! cargo run -p llimphi-voxel-studio --release --example noche_en_el_pixel
20//! ```
21//!
22//! Y el **decorado** y el **mar**: el mismo plano general en el mundo «mar», con
23//! y sin el palacio y el kiosko plantados al fondo — cuántos píxeles cambia el
24//! decorado, y cuánta agua hay en cuadro contra el desierto.
25//!
26//! Deja los cuadros en `/tmp/noche_*.png` por si alguien quiere mirarlos.
27
28use std::io::BufWriter;
29
30use llimphi_hal::{wgpu, Hal};
31use llimphi_raster::peniko::Color;
32use llimphi_raster::{vello, Renderer};
33use llimphi_voxel::{
34    world_dim, CasaTipo, Cielo, Clip, Decorado, Project, SceneSpec, Utileria, UtileriaSpec, PREVIEW_DIM_XZ,
35};
36use llimphi_voxel_render::{Escenario, FMT};
37
38const W: u32 = 960;
39const H: u32 = 540;
40/// Plano de contraluz (mira hacia el sol, que de noche es la luna).
41const T_CONTRALUZ: f32 = 1.8;
42/// Plano general, todavía caminando: la estrella y lo que tiene alrededor.
43const T_SALUDO: f32 = 0.9;
44
45fn escena(p: &Project, noche: bool, estrella: bool) -> SceneSpec {
46    escena_en(p, "desierto", noche, estrella)
47}
48
49fn escena_en(p: &Project, mundo: &str, noche: bool, estrella: bool) -> SceneSpec {
50    let mundo = p.mundos.iter().find(|m| m.name == mundo).expect("mundo").id;
51    let mut s = SceneSpec::walk_and_emote("noche", mundo, 1, Clip::Wave, world_dim(PREVIEW_DIM_XZ));
52    s.handheld = 0.0; // sin temblor: los tres cuadros tienen que encuadrar igual
53    if estrella {
54        let mut e = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]);
55        e.lleva = Some(0);
56        e.alto = 0.9;
57        s.utileria.push(e);
58    }
59    if noche {
60        s.cielo = Some(Cielo::noche());
61    }
62    s
63}
64
65fn filmar(hal: &Hal, renderer: &mut Renderer, p: &Project, s: &SceneSpec, t: f32, nombre: &str) -> Vec<u8> {
66    let mut escenario = Escenario::preparar(&hal.device, &hal.queue, p, s).expect("escenario");
67    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
68        label: Some("noche"),
69        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
70        mip_level_count: 1,
71        sample_count: 1,
72        dimension: wgpu::TextureDimension::D2,
73        format: FMT,
74        usage: wgpu::TextureUsages::STORAGE_BINDING
75            | wgpu::TextureUsages::TEXTURE_BINDING
76            | wgpu::TextureUsages::RENDER_ATTACHMENT
77            | wgpu::TextureUsages::COPY_SRC,
78        view_formats: &[],
79    });
80    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
81    let [r, g, b, a] = escenario.cielo();
82    renderer
83        .render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(r, g, b, a))
84        .expect("clear");
85    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
86    escenario.cuadro(&hal.device, &hal.queue, &mut enc, &view, (W, H), (0.0, 0.0, W as f32, H as f32), t);
87    hal.queue.submit(std::iter::once(enc.finish()));
88    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
89    let px = leer(hal, &target);
90    guardar(&px, &format!("/tmp/noche_{nombre}.png"));
91    px
92}
93
94fn leer(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
95    let unpadded = (W * 4) as usize;
96    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
97    let padded = unpadded.div_ceil(align) * align;
98    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
99        label: None,
100        size: (padded * H as usize) as u64,
101        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
102        mapped_at_creation: false,
103    });
104    let mut enc = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
105    enc.copy_texture_to_buffer(
106        wgpu::TexelCopyTextureInfo {
107            texture: target,
108            mip_level: 0,
109            origin: wgpu::Origin3d::ZERO,
110            aspect: wgpu::TextureAspect::All,
111        },
112        wgpu::TexelCopyBufferInfo {
113            buffer: &buf,
114            layout: wgpu::TexelCopyBufferLayout {
115                offset: 0,
116                bytes_per_row: Some(padded as u32),
117                rows_per_image: Some(H),
118            },
119        },
120        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
121    );
122    hal.queue.submit(std::iter::once(enc.finish()));
123    let slice = buf.slice(..);
124    let (tx, rx) = std::sync::mpsc::channel();
125    slice.map_async(wgpu::MapMode::Read, move |r| {
126        let _ = tx.send(r);
127    });
128    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
129    rx.recv().unwrap().unwrap();
130    let data = slice.get_mapped_range();
131    let mut px = Vec::with_capacity((W * H * 4) as usize);
132    for row in 0..H as usize {
133        px.extend_from_slice(&data[row * padded..row * padded + unpadded]);
134    }
135    px
136}
137
138fn guardar(px: &[u8], ruta: &str) {
139    let f = std::fs::File::create(ruta).expect("png");
140    let mut e = png::Encoder::new(BufWriter::new(f), W, H);
141    e.set_color(png::ColorType::Rgba);
142    e.set_depth(png::BitDepth::Eight);
143    e.write_header().unwrap().write_image_data(px).unwrap();
144}
145
146fn lum(px: &[u8], x: u32, y: u32) -> f32 {
147    let i = ((y * W + x) * 4) as usize;
148    0.2126 * px[i] as f32 + 0.7152 * px[i + 1] as f32 + 0.0722 * px[i + 2] as f32
149}
150
151/// Luminancia media de la franja de arriba (el 15 % del alto).
152fn cielo_medio(px: &[u8]) -> f32 {
153    let h = H * 15 / 100;
154    let mut s = 0.0;
155    for y in 0..h {
156        for x in 0..W {
157            s += lum(px, x, y);
158        }
159    }
160    s / (h * W) as f32
161}
162
163/// Estrellas en la mitad de arriba: un píxel que es máximo local y le saca
164/// más de 40 de luminancia a la mediana de su anillo de radio 3.
165fn estrellas(px: &[u8]) -> usize {
166    let mut n = 0;
167    for y in 3..H / 2 {
168        for x in 3..W - 3 {
169            let c = lum(px, x, y);
170            let mut anillo = Vec::with_capacity(24);
171            let mut maximo = true;
172            for dy in -3i32..=3 {
173                for dx in -3i32..=3 {
174                    if dx == 0 && dy == 0 {
175                        continue;
176                    }
177                    let v = lum(px, (x as i32 + dx) as u32, (y as i32 + dy) as u32);
178                    if dx.abs().max(dy.abs()) <= 1 && v >= c {
179                        maximo = false;
180                    }
181                    if dx.abs().max(dy.abs()) == 3 {
182                        anillo.push(v);
183                    }
184                }
185            }
186            anillo.sort_by(f32::total_cmp);
187            if maximo && c - anillo[anillo.len() / 2] > 40.0 {
188                n += 1;
189            }
190        }
191    }
192    n
193}
194
195/// El píxel más claro de la mitad de arriba.
196fn mas_claro(px: &[u8]) -> f32 {
197    (0..H / 2).flat_map(|y| (0..W).map(move |x| (x, y))).map(|(x, y)| lum(px, x, y)).fold(0.0, f32::max)
198}
199
200/// Píxeles «de agua»: azul que domina claramente al rojo y al verde.
201fn agua(px: &[u8]) -> usize {
202    px.chunks_exact(4).filter(|c| c[2] as i32 > c[0] as i32 + 40 && c[2] as i32 > c[1] as i32 + 10).count()
203}
204
205/// Cuántos píxeles difieren en más de `umbral` de luminancia.
206fn cambiados(a: &[u8], b: &[u8], umbral: f32) -> usize {
207    (0..H).flat_map(|y| (0..W).map(move |x| (x, y))).filter(|&(x, y)| (lum(a, x, y) - lum(b, x, y)).abs() > umbral).count()
208}
209
210fn main() {
211    let hal = pollster::block_on(Hal::new(None)).expect("gpu");
212    let mut renderer = Renderer::new(&hal).expect("renderer");
213    let p = Project::starter();
214
215    let dia_c = filmar(&hal, &mut renderer, &p, &escena(&p, false, true), T_CONTRALUZ, "dia_contraluz");
216    let noche_c = filmar(&hal, &mut renderer, &p, &escena(&p, true, true), T_CONTRALUZ, "noche_contraluz");
217    let noche_c_sin = filmar(&hal, &mut renderer, &p, &escena(&p, true, false), T_CONTRALUZ, "noche_contraluz_sin_estrella");
218    let dia = filmar(&hal, &mut renderer, &p, &escena(&p, false, true), T_SALUDO, "dia_saludo");
219    let dia_sin = filmar(&hal, &mut renderer, &p, &escena(&p, false, false), T_SALUDO, "dia_saludo_sin_estrella");
220    let noche = filmar(&hal, &mut renderer, &p, &escena(&p, true, true), T_SALUDO, "noche_saludo");
221    let noche_sin = filmar(&hal, &mut renderer, &p, &escena(&p, true, false), T_SALUDO, "noche_saludo_sin_estrella");
222
223    println!("cielo (franja de arriba, luminancia media 0..255)");
224    println!("  día   contraluz {:6.1}   saludo {:6.1}", cielo_medio(&dia_c), cielo_medio(&dia));
225    println!("  noche contraluz {:6.1}   saludo {:6.1}", cielo_medio(&noche_c), cielo_medio(&noche));
226    println!("estrellas en la mitad de arriba (contraluz)");
227    println!("  día {}   noche {}", estrellas(&dia_c), estrellas(&noche_c));
228    println!("píxel más claro del cielo (contraluz): día {:.0}   noche {:.0}", mas_claro(&dia_c), mas_claro(&noche_c));
229    println!("contraluz de noche: píxeles que cambian al sacar la estrella {}", cambiados(&noche_c, &noche_c_sin, 8.0));
230    println!("píxeles que cambian al sacar la estrella (plano general, umbral 8)");
231    let (cd, cn) = (cambiados(&dia, &dia_sin, 8.0), cambiados(&noche, &noche_sin, 8.0));
232    println!("  día {cd}   noche {cn}   ({:.1}×)", cn as f32 / cd.max(1) as f32);
233
234    // El decorado, plantado DELANTE de la cámara: detrás de la actriz en la
235    // dirección en que mira el plano (el palacio) y a un costado (el kiosko).
236    // Puesto respecto del recorrido caía fuera de cuadro y no medía nada.
237    let mut con = escena_en(&p, "mar", false, false);
238    let (frente, lado, ahi) = {
239        let e = Escenario::preparar(&hal.device, &hal.queue, &p, &con).expect("escenario");
240        let cam = e.camara_en(T_SALUDO);
241        let f = (cam.target - cam.eye) * llimphi_3d::glam::Vec3::new(1.0, 0.0, 1.0);
242        let f = f.normalize_or_zero();
243        let s = con.scripts()[0].sample(T_SALUDO);
244        (f, llimphi_3d::glam::Vec3::new(-f.z, 0.0, f.x), (s.gx, s.gz))
245    };
246    let punto = |adelante: f32, costado: f32| {
247        (ahi.0 + frente.x * adelante + lado.x * costado, ahi.1 + frente.z * adelante + lado.z * costado)
248    };
249    let (px, pz) = punto(26.0, 0.0);
250    let (kx, kz) = punto(10.0, -12.0);
251    con.decorado.push(Decorado::en(CasaTipo::Palacio, px, pz));
252    con.decorado.push(Decorado::en(CasaTipo::Kiosko, kx, kz));
253    let mar_con = filmar(&hal, &mut renderer, &p, &con, T_SALUDO, "mar_decorado");
254    let mar_sin = filmar(&hal, &mut renderer, &p, &escena_en(&p, "mar", false, false), T_SALUDO, "mar_sin_decorado");
255    println!("decorado (mar, plano general): píxeles que cambian al plantarlo {}", cambiados(&mar_con, &mar_sin, 8.0));
256    println!("agua en cuadro: mar {}   desierto {}", agua(&mar_sin), agua(&dia_sin));
257    // La luna, de noche: sólo para mirar (no hay número que diga «parece la luna»).
258    let _ = filmar(&hal, &mut renderer, &p, &escena_en(&p, "luna", true, true), T_CONTRALUZ, "luna_contraluz");
259}