tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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}