tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc
llimphi-3d/src/postfx.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(3d): cosechar efectos genéricos de supay + refrescar voxel/director
1//! `PostFx` — **antialiasing por supersampling (SSAA) + bloom** genéricos como
2//! pase de post-proceso reutilizable sobre cualquier render 3D wgpu.
3//!
4//! Cosechado del renderer 2.5D de Doom (`supay-render-llimphi::wgpu3d`), donde
5//! estos efectos nacieron pegados a tipos del juego. Acá viven en su forma
6//! agnóstica: nada sabe de paredes, sprites ni WAD. Cualquier escena que se
7//! pueda dibujar en un pase wgpu (las mallas de [`Renderer3d`](crate::Renderer3d),
8//! los voxels de [`VoxelRenderer`](crate::VoxelRenderer), una carta 3D de cosmos,
9//! una galería de nahual) obtiene AA + glow «gratis» envolviendo su dibujo en
10//! [`PostFx::render_with`].
11//!
12//! ## Flujo (igual que el de supay, sin sus tipos)
13//!
14//! 1. La escena se rinde a un color intermedio a `supersample`× la resolución
15//! de salida (4× de fragmentos con factor 2) con su propio depth.
16//! 2. **Bright-pass + blur 5×5** de ese color → textura de bloom a media res
17//! (extrae lo que supera el umbral de luminancia y lo suaviza).
18//! 3. **Blit final**: baja la escena al target real con filtro lineal (eso es
19//! el antialiasing) y le suma el bloom (eso es el glow). Compone con
20//! `LoadOp::Load`, así no pisa la UI vello que ya esté debajo.
21//!
22//! ## Uso
23//!
24//! ```ignore
25//! let mut fx = PostFx::new(&device, color_format);
26//! // ... en gpu_paint_with(dev, q, enc, view, rect, _vp):
27//! fx.render_with(dev, q, enc, view, (w, h), wgpu::Color::BLACK, |pass| {
28//! // `pass` ya está a supersample× con color+depth; sólo dibujá.
29//! renderer3d.upload(q, w as f32 / h as f32, &camera); // antes del pase
30//! renderer3d.draw(pass);
31//! });
32//! ```
33
34use crate::scene::DEPTH_FORMAT;
35
36/// Parámetros del post-proceso. Los defaults replican el «look» de supay
37/// (supersample 2×, glow suave). Ajustables por escena: una carta astral quiere
38/// poco bloom, un FPS infernal mucho.
39#[derive(Clone, Copy, Debug)]
40pub struct PostFxConfig {
41 /// Factor de supersampling por eje. 2 = rinde a 2× y promedia al bajar
42 /// (antialiasing 4×). 1 = sin SSAA (sólo bloom). Útil bajarlo en GPUs flojas.
43 pub supersample: u32,
44 /// Cuánto se suma el bloom al blit final (0 = sin glow).
45 pub bloom_strength: f32,
46 /// Umbral de luminancia del bright-pass: por encima de esto un píxel sangra.
47 pub bloom_threshold: f32,
48 /// Rampa del bright-pass (knee): ancho de la transición sobre el umbral.
49 pub bloom_knee: f32,
50 /// Radio del blur del bright-pass, en téxeles de la textura de bloom.
51 pub bloom_radius: f32,
52}
53
54impl Default for PostFxConfig {
55 fn default() -> Self {
56 Self {
57 supersample: 2,
58 bloom_strength: 0.85,
59 bloom_threshold: 0.62,
60 bloom_knee: 0.38,
61 bloom_radius: 2.5,
62 }
63 }
64}
65
66/// Target intermedio de la escena (color supersampleado + depth) más el bind
67/// group que expone su color como textura para el bright-pass y el blit.
68struct SceneTarget {
69 w: u32,
70 h: u32,
71 color_view: wgpu::TextureView,
72 depth_view: wgpu::TextureView,
73 tex_bg: wgpu::BindGroup,
74}
75
76/// Target de bloom (media resolución): color + bind group para sumarlo en el blit.
77struct BloomTarget {
78 w: u32,
79 h: u32,
80 color_view: wgpu::TextureView,
81 tex_bg: wgpu::BindGroup,
82}
83
84/// Pase de post-proceso reutilizable: SSAA + bloom sobre cualquier render wgpu.
85/// Cachea pipelines/sampler/layouts y recrea los targets al cambiar de tamaño.
86pub struct PostFx {
87 cfg: PostFxConfig,
88 output_format: wgpu::TextureFormat,
89 tex_layout: wgpu::BindGroupLayout,
90 sampler: wgpu::Sampler,
91 /// Bright-pass + blur: escena → bloom. group0 = escena, group1 = uniform.
92 bright_pipeline: wgpu::RenderPipeline,
93 /// Blit final: escena + bloom → target. group0 = escena, 1 = bloom, 2 = uniform.
94 blit_pipeline: wgpu::RenderPipeline,
95 bright_uniform: wgpu::Buffer,
96 bright_uniform_bg: wgpu::BindGroup,
97 blit_uniform: wgpu::Buffer,
98 blit_uniform_bg: wgpu::BindGroup,
99 scene: Option<SceneTarget>,
100 bloom: Option<BloomTarget>,
101}
102
103impl PostFx {
104 /// Crea el post-proceso para el `output_format` del target final (el de la
105 /// textura intermedia del frame: `Rgba8Unorm` headless, el de la surface en
106 /// vivo). Config por defecto.
107 pub fn new(device: &wgpu::Device, output_format: wgpu::TextureFormat) -> Self {
108 Self::with_config(device, output_format, PostFxConfig::default())
109 }
110
111 /// Igual que [`Self::new`] con una [`PostFxConfig`] explícita.
112 pub fn with_config(
113 device: &wgpu::Device,
114 output_format: wgpu::TextureFormat,
115 cfg: PostFxConfig,
116 ) -> Self {
117 // Layout textura+sampler (compartido por escena y bloom como entradas).
118 let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
119 label: Some("llimphi-3d-postfx-tex-layout"),
120 entries: &[
121 wgpu::BindGroupLayoutEntry {
122 binding: 0,
123 visibility: wgpu::ShaderStages::FRAGMENT,
124 ty: wgpu::BindingType::Texture {
125 sample_type: wgpu::TextureSampleType::Float { filterable: true },
126 view_dimension: wgpu::TextureViewDimension::D2,
127 multisampled: false,
128 },
129 count: None,
130 },
131 wgpu::BindGroupLayoutEntry {
132 binding: 1,
133 visibility: wgpu::ShaderStages::FRAGMENT,
134 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
135 count: None,
136 },
137 ],
138 });
139
140 let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
141 label: Some("llimphi-3d-postfx-uniform-layout"),
142 entries: &[wgpu::BindGroupLayoutEntry {
143 binding: 0,
144 visibility: wgpu::ShaderStages::FRAGMENT,
145 ty: wgpu::BindingType::Buffer {
146 ty: wgpu::BufferBindingType::Uniform,
147 has_dynamic_offset: false,
148 min_binding_size: None,
149 },
150 count: None,
151 }],
152 });
153
154 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
155 label: Some("llimphi-3d-postfx-sampler"),
156 address_mode_u: wgpu::AddressMode::ClampToEdge,
157 address_mode_v: wgpu::AddressMode::ClampToEdge,
158 address_mode_w: wgpu::AddressMode::ClampToEdge,
159 mag_filter: wgpu::FilterMode::Linear,
160 min_filter: wgpu::FilterMode::Linear,
161 mipmap_filter: wgpu::FilterMode::Linear,
162 ..Default::default()
163 });
164
165 // Helper para pipelines fullscreen (triángulo cubre-pantalla, sin VBO).
166 let make_fullscreen = |layouts: &[&wgpu::BindGroupLayout],
167 wgsl: &str,
168 label: &str| {
169 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
170 label: Some(label),
171 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
172 });
173 let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
174 label: Some(label),
175 bind_group_layouts: layouts,
176 push_constant_ranges: &[],
177 });
178 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
179 label: Some(label),
180 layout: Some(&pl),
181 vertex: wgpu::VertexState {
182 module: &shader,
183 entry_point: Some("vs"),
184 compilation_options: Default::default(),
185 buffers: &[],
186 },
187 primitive: wgpu::PrimitiveState {
188 cull_mode: None,
189 ..Default::default()
190 },
191 depth_stencil: None,
192 multisample: Default::default(),
193 fragment: Some(wgpu::FragmentState {
194 module: &shader,
195 entry_point: Some("fs"),
196 compilation_options: Default::default(),
197 targets: &[Some(wgpu::ColorTargetState {
198 format: output_format,
199 blend: None,
200 write_mask: wgpu::ColorWrites::ALL,
201 })],
202 }),
203 multiview: None,
204 cache: None,
205 })
206 };
207
208 let bright_pipeline = make_fullscreen(
209 &[&tex_layout, &uniform_layout],
210 BRIGHT_WGSL,
211 "llimphi-3d-postfx-bright",
212 );
213 let blit_pipeline = make_fullscreen(
214 &[&tex_layout, &tex_layout, &uniform_layout],
215 BLIT_WGSL,
216 "llimphi-3d-postfx-blit",
217 );
218
219 let mk_uniform = |label: &str| {
220 device.create_buffer(&wgpu::BufferDescriptor {
221 label: Some(label),
222 size: 16, // un vec4<f32>
223 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
224 mapped_at_creation: false,
225 })
226 };
227 let bright_uniform = mk_uniform("llimphi-3d-postfx-bright-u");
228 let blit_uniform = mk_uniform("llimphi-3d-postfx-blit-u");
229 let mk_uniform_bg = |buf: &wgpu::Buffer, label: &str| {
230 device.create_bind_group(&wgpu::BindGroupDescriptor {
231 label: Some(label),
232 layout: &uniform_layout,
233 entries: &[wgpu::BindGroupEntry {
234 binding: 0,
235 resource: buf.as_entire_binding(),
236 }],
237 })
238 };
239 let bright_uniform_bg = mk_uniform_bg(&bright_uniform, "llimphi-3d-postfx-bright-u-bg");
240 let blit_uniform_bg = mk_uniform_bg(&blit_uniform, "llimphi-3d-postfx-blit-u-bg");
241
242 Self {
243 cfg,
244 output_format,
245 tex_layout,
246 sampler,
247 bright_pipeline,
248 blit_pipeline,
249 bright_uniform,
250 bright_uniform_bg,
251 blit_uniform,
252 blit_uniform_bg,
253 scene: None,
254 bloom: None,
255 }
256 }
257
258 /// Config actual (lectura).
259 pub fn config(&self) -> PostFxConfig {
260 self.cfg
261 }
262
263 /// Cambia la config (toma efecto el próximo frame). Si cambia `supersample`
264 /// los targets se recrean solos al verificar el tamaño.
265 pub fn set_config(&mut self, cfg: PostFxConfig) {
266 self.cfg = cfg;
267 }
268
269 /// Formato del target final con el que se creó.
270 pub fn output_format(&self) -> wgpu::TextureFormat {
271 self.output_format
272 }
273
274 /// Tamaño supersampleado al que el llamador debe rendir su escena, dado el
275 /// `output_size` final. (= `output × supersample`.) Útil para el aspect/UI.
276 pub fn scene_size(&self, output_size: (u32, u32)) -> (u32, u32) {
277 let ss = self.cfg.supersample.max(1);
278 (output_size.0 * ss, output_size.1 * ss)
279 }
280
281 /// Asegura los targets internos para una salida `output_size` y sube los
282 /// uniforms del frame. **Llamar antes** de [`Self::scene_pass`] y
283 /// [`Self::resolve`] con el mismo `output_size`.
284 pub fn prepare(
285 &mut self,
286 device: &wgpu::Device,
287 queue: &wgpu::Queue,
288 output_size: (u32, u32),
289 ) {
290 let (ow, oh) = output_size;
291 if ow == 0 || oh == 0 {
292 return;
293 }
294 let (rw, rh) = self.scene_size(output_size);
295 self.ensure_scene(device, rw, rh);
296 self.ensure_bloom(device, (ow / 2).max(1), (oh / 2).max(1));
297 self.upload_uniforms(queue);
298 }
299
300 /// Abre el pase de escena a resolución supersampleada (color limpiado a
301 /// `clear` + depth limpiado a lejano) y devuelve el [`wgpu::RenderPass`] para
302 /// que el llamador setee pipeline(s) y dibuje. Requiere [`Self::prepare`]
303 /// previo en el mismo frame. Al soltar el pase, llamar [`Self::resolve`].
304 ///
305 /// El depth es `Depth32Float` (igual que [`Renderer3d`](crate::Renderer3d) y
306 /// [`Scene3d`](crate::Scene3d)), así sus pipelines encajan sin cambios.
307 pub fn scene_pass<'p>(
308 &'p self,
309 encoder: &'p mut wgpu::CommandEncoder,
310 clear: wgpu::Color,
311 ) -> wgpu::RenderPass<'p> {
312 let scene = self
313 .scene
314 .as_ref()
315 .expect("PostFx::scene_pass sin prepare() previo");
316 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
317 label: Some("llimphi-3d-postfx-scene"),
318 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
319 view: &scene.color_view,
320 resolve_target: None,
321 depth_slice: None,
322 ops: wgpu::Operations {
323 load: wgpu::LoadOp::Clear(clear),
324 store: wgpu::StoreOp::Store,
325 },
326 })],
327 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
328 view: &scene.depth_view,
329 depth_ops: Some(wgpu::Operations {
330 load: wgpu::LoadOp::Clear(1.0),
331 store: wgpu::StoreOp::Store,
332 }),
333 stencil_ops: None,
334 }),
335 timestamp_writes: None,
336 occlusion_query_set: None,
337 })
338 }
339
340 /// Resuelve la escena ya dibujada: bright-pass + blur → bloom, luego blit
341 /// (bajada SSAA + suma de bloom) sobre `target`, preservando lo que haya
342 /// debajo (`LoadOp::Load`). Requiere [`Self::prepare`] + el pase de
343 /// [`Self::scene_pass`] ya soltado.
344 pub fn resolve(&self, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView) {
345 let scene_bg = match self.scene.as_ref() {
346 Some(s) => &s.tex_bg,
347 None => return,
348 };
349 let bloom = self.bloom.as_ref().unwrap();
350
351 // --- Bright-pass + blur: escena → textura de bloom (media res). ---
352 {
353 let mut bp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
354 label: Some("llimphi-3d-postfx-bright"),
355 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
356 view: &bloom.color_view,
357 resolve_target: None,
358 depth_slice: None,
359 ops: wgpu::Operations {
360 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
361 store: wgpu::StoreOp::Store,
362 },
363 })],
364 depth_stencil_attachment: None,
365 timestamp_writes: None,
366 occlusion_query_set: None,
367 });
368 bp.set_pipeline(&self.bright_pipeline);
369 bp.set_bind_group(0, scene_bg, &[]);
370 bp.set_bind_group(1, &self.bright_uniform_bg, &[]);
371 bp.draw(0..3, 0..1);
372 }
373
374 // --- Blit final: escena (SSAA) + bloom → target real (preserva debajo). ---
375 {
376 let mut blit = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
377 label: Some("llimphi-3d-postfx-blit"),
378 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
379 view: target,
380 resolve_target: None,
381 depth_slice: None,
382 ops: wgpu::Operations {
383 load: wgpu::LoadOp::Load,
384 store: wgpu::StoreOp::Store,
385 },
386 })],
387 depth_stencil_attachment: None,
388 timestamp_writes: None,
389 occlusion_query_set: None,
390 });
391 blit.set_pipeline(&self.blit_pipeline);
392 blit.set_bind_group(0, scene_bg, &[]);
393 blit.set_bind_group(1, &bloom.tex_bg, &[]);
394 blit.set_bind_group(2, &self.blit_uniform_bg, &[]);
395 blit.draw(0..3, 0..1);
396 }
397 }
398
399 fn upload_uniforms(&self, queue: &wgpu::Queue) {
400 // bright: (threshold, knee, radius, _)
401 let mut b = Vec::with_capacity(16);
402 for v in [
403 self.cfg.bloom_threshold,
404 self.cfg.bloom_knee.max(1e-4),
405 self.cfg.bloom_radius,
406 0.0,
407 ] {
408 b.extend_from_slice(&v.to_ne_bytes());
409 }
410 queue.write_buffer(&self.bright_uniform, 0, &b);
411 // blit: (bloom_strength, _, _, _)
412 let mut s = Vec::with_capacity(16);
413 for v in [self.cfg.bloom_strength, 0.0, 0.0, 0.0] {
414 s.extend_from_slice(&v.to_ne_bytes());
415 }
416 queue.write_buffer(&self.blit_uniform, 0, &s);
417 }
418
419 fn ensure_scene(&mut self, device: &wgpu::Device, w: u32, h: u32) {
420 if matches!(&self.scene, Some(s) if s.w == w && s.h == h) {
421 return;
422 }
423 let color = device.create_texture(&wgpu::TextureDescriptor {
424 label: Some("llimphi-3d-postfx-scene-color"),
425 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
426 mip_level_count: 1,
427 sample_count: 1,
428 dimension: wgpu::TextureDimension::D2,
429 format: wgpu::TextureFormat::Rgba8Unorm,
430 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
431 view_formats: &[],
432 });
433 let color_view = color.create_view(&Default::default());
434 let depth = device.create_texture(&wgpu::TextureDescriptor {
435 label: Some("llimphi-3d-postfx-scene-depth"),
436 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
437 mip_level_count: 1,
438 sample_count: 1,
439 dimension: wgpu::TextureDimension::D2,
440 format: DEPTH_FORMAT,
441 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
442 view_formats: &[],
443 });
444 let depth_view = depth.create_view(&Default::default());
445 let tex_bg = self.make_tex_bg(device, &color_view, "llimphi-3d-postfx-scene-bg");
446 self.scene = Some(SceneTarget { w, h, color_view, depth_view, tex_bg });
447 }
448
449 fn ensure_bloom(&mut self, device: &wgpu::Device, w: u32, h: u32) {
450 if matches!(&self.bloom, Some(b) if b.w == w && b.h == h) {
451 return;
452 }
453 let color = device.create_texture(&wgpu::TextureDescriptor {
454 label: Some("llimphi-3d-postfx-bloom-color"),
455 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
456 mip_level_count: 1,
457 sample_count: 1,
458 dimension: wgpu::TextureDimension::D2,
459 format: wgpu::TextureFormat::Rgba8Unorm,
460 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
461 view_formats: &[],
462 });
463 let color_view = color.create_view(&Default::default());
464 let tex_bg = self.make_tex_bg(device, &color_view, "llimphi-3d-postfx-bloom-bg");
465 self.bloom = Some(BloomTarget { w, h, color_view, tex_bg });
466 }
467
468 fn make_tex_bg(
469 &self,
470 device: &wgpu::Device,
471 view: &wgpu::TextureView,
472 label: &str,
473 ) -> wgpu::BindGroup {
474 device.create_bind_group(&wgpu::BindGroupDescriptor {
475 label: Some(label),
476 layout: &self.tex_layout,
477 entries: &[
478 wgpu::BindGroupEntry {
479 binding: 0,
480 resource: wgpu::BindingResource::TextureView(view),
481 },
482 wgpu::BindGroupEntry {
483 binding: 1,
484 resource: wgpu::BindingResource::Sampler(&self.sampler),
485 },
486 ],
487 })
488 }
489}
490
491/// Bright-pass + blur 5×5 de la escena → textura de bloom (media res). En una
492/// pasada: downsample bilinear + extracción de lo brillante + suavizado.
493/// `u.params = (threshold, knee, radius, _)`.
494pub const BRIGHT_WGSL: &str = r#"
495@group(0) @binding(0) var tex: texture_2d<f32>;
496@group(0) @binding(1) var samp: sampler;
497struct U { params: vec4<f32> };
498@group(1) @binding(0) var<uniform> u: U;
499
500struct VOut {
501 @builtin(position) clip: vec4<f32>,
502 @location(0) uv: vec2<f32>,
503};
504
505@vertex
506fn vs(@builtin(vertex_index) vi: u32) -> VOut {
507 var p = array<vec2<f32>, 3>(
508 vec2<f32>(-1.0, -1.0),
509 vec2<f32>( 3.0, -1.0),
510 vec2<f32>(-1.0, 3.0),
511 );
512 var o: VOut;
513 let c = p[vi];
514 o.clip = vec4<f32>(c, 0.0, 1.0);
515 o.uv = vec2<f32>(c.x * 0.5 + 0.5, 1.0 - (c.y * 0.5 + 0.5));
516 return o;
517}
518
519@fragment
520fn fs(in: VOut) -> @location(0) vec4<f32> {
521 let dim = vec2<f32>(textureDimensions(tex, 0));
522 let texel = 1.0 / dim;
523 let thresh = u.params.x;
524 let knee = u.params.y;
525 let radius = u.params.z;
526 var acc = vec3<f32>(0.0);
527 for (var j = -2; j <= 2; j = j + 1) {
528 for (var i = -2; i <= 2; i = i + 1) {
529 let o = vec2<f32>(f32(i), f32(j)) * texel * radius;
530 let c = textureSample(tex, samp, in.uv + o).rgb;
531 let l = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
532 // Bright-pass suave: lo que supera el umbral sangra según el knee.
533 let k = clamp((l - thresh) / knee, 0.0, 1.0);
534 acc += c * k;
535 }
536 }
537 return vec4<f32>(acc / 25.0, 1.0);
538}
539"#;
540
541/// Blit de bajada SSAA + suma de bloom: triángulo fullscreen que muestrea la
542/// escena supersampleada (filtro lineal = antialiasing) y le suma el bloom.
543/// `u.params.x = bloom_strength`.
544pub const BLIT_WGSL: &str = r#"
545@group(0) @binding(0) var tex: texture_2d<f32>;
546@group(0) @binding(1) var samp: sampler;
547@group(1) @binding(0) var bloom: texture_2d<f32>;
548@group(1) @binding(1) var bsamp: sampler;
549struct U { params: vec4<f32> };
550@group(2) @binding(0) var<uniform> u: U;
551
552struct VOut {
553 @builtin(position) clip: vec4<f32>,
554 @location(0) uv: vec2<f32>,
555};
556
557@vertex
558fn vs(@builtin(vertex_index) vi: u32) -> VOut {
559 var p = array<vec2<f32>, 3>(
560 vec2<f32>(-1.0, -1.0),
561 vec2<f32>( 3.0, -1.0),
562 vec2<f32>(-1.0, 3.0),
563 );
564 var o: VOut;
565 let c = p[vi];
566 o.clip = vec4<f32>(c, 0.0, 1.0);
567 o.uv = vec2<f32>(c.x * 0.5 + 0.5, 1.0 - (c.y * 0.5 + 0.5));
568 return o;
569}
570
571@fragment
572fn fs(in: VOut) -> @location(0) vec4<f32> {
573 let s = textureSample(tex, samp, in.uv).rgb;
574 let b = textureSample(bloom, bsamp, in.uv).rgb;
575 return vec4<f32>(s + b * u.params.x, 1.0);
576}
577"#;
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 /// Los shaders WGSL del post-proceso parsean y validan sin GPU (naga).
584 /// Ataja errores que de otro modo sólo saldrían al crear el pipeline.
585 #[test]
586 fn shaders_wgsl_validan() {
587 for (nombre, src) in [("BRIGHT_WGSL", BRIGHT_WGSL), ("BLIT_WGSL", BLIT_WGSL)] {
588 let module = naga::front::wgsl::parse_str(src)
589 .unwrap_or_else(|e| panic!("{nombre} no parsea: {e:?}"));
590 naga::valid::Validator::new(
591 naga::valid::ValidationFlags::all(),
592 naga::valid::Capabilities::all(),
593 )
594 .validate(&module)
595 .unwrap_or_else(|e| panic!("{nombre} no valida: {e:?}"));
596 }
597 }
598
599 #[test]
600 fn config_default_replica_supay() {
601 let c = PostFxConfig::default();
602 assert_eq!(c.supersample, 2);
603 assert!((c.bloom_strength - 0.85).abs() < 1e-6);
604 assert!((c.bloom_threshold - 0.62).abs() < 1e-6);
605 }
606}