1use core::ops::Deref;
3use std::sync::{Arc, RwLock};
4
5use craballoc::{
6 prelude::{Hybrid, HybridArray, SlabAllocator},
7 runtime::WgpuRuntime,
8 slab::SlabBuffer,
9};
10use crabslab::Id;
11use glam::{UVec2, Vec2};
12
13use crate::texture::{self, Texture};
14
15fn create_bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout {
16 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
17 label,
18 entries: &[
19 wgpu::BindGroupLayoutEntry {
20 binding: 0,
21 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
22 ty: wgpu::BindingType::Buffer {
23 ty: wgpu::BufferBindingType::Storage { read_only: true },
24 has_dynamic_offset: false,
25 min_binding_size: None,
26 },
27 count: None,
28 },
29 wgpu::BindGroupLayoutEntry {
30 binding: 1,
31 visibility: wgpu::ShaderStages::FRAGMENT,
32 ty: wgpu::BindingType::Texture {
33 sample_type: wgpu::TextureSampleType::Float { filterable: true },
34 view_dimension: wgpu::TextureViewDimension::D2,
35 multisampled: false,
36 },
37 count: None,
38 },
39 wgpu::BindGroupLayoutEntry {
40 binding: 2,
41 visibility: wgpu::ShaderStages::FRAGMENT,
42 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
43 count: None,
44 },
45 ],
46 })
47}
48
49fn create_bloom_downsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
50 let label = Some("bloom downsample");
51 let bindgroup_layout = create_bindgroup_layout(device, label);
52 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
53 label,
54 bind_group_layouts: &[&bindgroup_layout],
55 push_constant_ranges: &[],
56 });
57 let vertex_module = crate::linkage::bloom_vertex::linkage(device);
58 let fragment_module = crate::linkage::bloom_downsample_fragment::linkage(device);
59 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
60 label,
61 layout: Some(&layout),
62 vertex: wgpu::VertexState {
63 module: &vertex_module.module,
64 entry_point: Some(vertex_module.entry_point),
65 buffers: &[],
66 compilation_options: Default::default(),
67 },
68 primitive: wgpu::PrimitiveState {
69 topology: wgpu::PrimitiveTopology::TriangleList,
70 strip_index_format: None,
71 front_face: wgpu::FrontFace::Ccw,
72 cull_mode: None,
73 unclipped_depth: false,
74 polygon_mode: wgpu::PolygonMode::Fill,
75 conservative: false,
76 },
77 depth_stencil: None,
78 multisample: wgpu::MultisampleState::default(),
79 fragment: Some(wgpu::FragmentState {
80 module: &fragment_module.module,
81 entry_point: Some(fragment_module.entry_point),
82 targets: &[Some(wgpu::ColorTargetState {
83 format: wgpu::TextureFormat::Rgba16Float,
84 blend: None,
85 write_mask: wgpu::ColorWrites::all(),
86 })],
87 compilation_options: Default::default(),
88 }),
89 multiview: None,
90 cache: None,
91 })
92}
93
94fn create_bloom_upsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
95 let label = Some("bloom upsample");
96 let bindgroup_layout = create_bindgroup_layout(device, label);
97 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
98 label,
99 bind_group_layouts: &[&bindgroup_layout],
100 push_constant_ranges: &[],
101 });
102 let vertex_module = crate::linkage::bloom_vertex::linkage(device);
103 let fragment_module = crate::linkage::bloom_upsample_fragment::linkage(device);
104 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
105 label,
106 layout: Some(&layout),
107 vertex: wgpu::VertexState {
108 module: &vertex_module.module,
109 entry_point: Some(vertex_module.entry_point),
110 buffers: &[],
111 compilation_options: Default::default(),
112 },
113 primitive: wgpu::PrimitiveState {
114 topology: wgpu::PrimitiveTopology::TriangleList,
115 strip_index_format: None,
116 front_face: wgpu::FrontFace::Ccw,
117 cull_mode: None,
118 unclipped_depth: false,
119 polygon_mode: wgpu::PolygonMode::Fill,
120 conservative: false,
121 },
122 depth_stencil: None,
123 multisample: wgpu::MultisampleState::default(),
124 fragment: Some(wgpu::FragmentState {
125 module: &fragment_module.module,
126 entry_point: Some(fragment_module.entry_point),
127 targets: &[Some(wgpu::ColorTargetState {
128 format: wgpu::TextureFormat::Rgba16Float,
129 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
130 write_mask: wgpu::ColorWrites::all(),
131 })],
132 compilation_options: Default::default(),
133 }),
134 multiview: None,
135 cache: None,
136 })
137}
138
139fn config_resolutions(resolution: UVec2) -> impl Iterator<Item = UVec2> {
140 let num_textures = resolution.x.min(resolution.y).ilog2();
141 (0..=num_textures).map(move |i| UVec2::new(resolution.x >> i, resolution.y >> i))
142}
143
144fn create_texture(
145 runtime: impl AsRef<WgpuRuntime>,
146 width: u32,
147 height: u32,
148 label: Option<&str>,
149 extra_usages: wgpu::TextureUsages,
150) -> texture::Texture {
151 let sampler = runtime
152 .as_ref()
153 .device
154 .create_sampler(&wgpu::SamplerDescriptor {
155 label,
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 Texture::new_with(
165 runtime,
166 label,
167 Some(
168 wgpu::TextureUsages::RENDER_ATTACHMENT
169 | wgpu::TextureUsages::TEXTURE_BINDING
170 | wgpu::TextureUsages::COPY_SRC
171 | extra_usages,
172 ),
173 Some(sampler),
174 wgpu::TextureFormat::Rgba16Float,
175 4,
176 2,
177 width,
178 height,
179 1,
180 &[],
181 )
182}
183
184fn create_textures(runtime: impl AsRef<WgpuRuntime>, resolution: UVec2) -> Vec<texture::Texture> {
185 let resolutions = config_resolutions(resolution).collect::<Vec<_>>();
186 log::trace!(
187 "creating {} bloom textures at resolution {resolution}",
188 resolutions.len()
189 );
190 let mut textures = vec![];
191 for (
192 i,
193 UVec2 {
194 x: width,
195 y: height,
196 },
197 ) in resolutions.into_iter().skip(1).enumerate()
198 {
199 let title = format!("bloom texture[{i}]");
200 let label = Some(title.as_str());
201 let texture = create_texture(
202 runtime.as_ref(),
203 width,
204 height,
205 label,
206 wgpu::TextureUsages::empty(),
207 );
208 textures.push(texture);
209 }
210 textures
211}
212
213fn create_bindgroup(
214 device: &wgpu::Device,
215 layout: &wgpu::BindGroupLayout,
216 buffer: &wgpu::Buffer,
217 tex: &Texture,
218) -> wgpu::BindGroup {
219 let label = Some("bloom");
220 device.create_bind_group(&wgpu::BindGroupDescriptor {
221 label,
222 layout,
223 entries: &[
224 wgpu::BindGroupEntry {
225 binding: 0,
226 resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),
227 },
228 wgpu::BindGroupEntry {
229 binding: 1,
230 resource: wgpu::BindingResource::TextureView(&tex.view),
231 },
232 wgpu::BindGroupEntry {
233 binding: 2,
234 resource: wgpu::BindingResource::Sampler(&tex.sampler),
235 },
236 ],
237 })
238}
239
240fn create_bindgroups(
241 device: &wgpu::Device,
242 pipeline: &wgpu::RenderPipeline,
243 buffer: &wgpu::Buffer,
244 textures: &[Texture],
245) -> Vec<wgpu::BindGroup> {
246 let layout = pipeline.get_bind_group_layout(0);
247 textures
248 .iter()
249 .map(|tex| create_bindgroup(device, &layout, buffer, tex))
250 .collect()
251}
252
253fn create_mix_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
254 let label = Some("bloom mix");
255 let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
256 label,
257 entries: &[
258 wgpu::BindGroupLayoutEntry {
259 binding: 0,
260 visibility: wgpu::ShaderStages::FRAGMENT,
261 ty: wgpu::BindingType::Buffer {
262 ty: wgpu::BufferBindingType::Storage { read_only: true },
263 has_dynamic_offset: false,
264 min_binding_size: None,
265 },
266 count: None,
267 },
268 wgpu::BindGroupLayoutEntry {
269 binding: 1,
270 visibility: wgpu::ShaderStages::FRAGMENT,
271 ty: wgpu::BindingType::Texture {
272 sample_type: wgpu::TextureSampleType::Float { filterable: true },
273 view_dimension: wgpu::TextureViewDimension::D2,
274 multisampled: false,
275 },
276 count: None,
277 },
278 wgpu::BindGroupLayoutEntry {
279 binding: 2,
280 visibility: wgpu::ShaderStages::FRAGMENT,
281 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
282 count: None,
283 },
284 wgpu::BindGroupLayoutEntry {
285 binding: 3,
286 visibility: wgpu::ShaderStages::FRAGMENT,
287 ty: wgpu::BindingType::Texture {
288 sample_type: wgpu::TextureSampleType::Float { filterable: true },
289 view_dimension: wgpu::TextureViewDimension::D2,
290 multisampled: false,
291 },
292 count: None,
293 },
294 wgpu::BindGroupLayoutEntry {
295 binding: 4,
296 visibility: wgpu::ShaderStages::FRAGMENT,
297 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
298 count: None,
299 },
300 ],
301 });
302 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
303 label,
304 bind_group_layouts: &[&bindgroup_layout],
305 push_constant_ranges: &[],
306 });
307 let vertex_module = crate::linkage::bloom_vertex::linkage(device);
308 let fragment_module = crate::linkage::bloom_mix_fragment::linkage(device);
309 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
310 label,
311 layout: Some(&layout),
312 vertex: wgpu::VertexState {
313 module: &vertex_module.module,
314 entry_point: Some(vertex_module.entry_point),
315 buffers: &[],
316 compilation_options: Default::default(),
317 },
318 primitive: wgpu::PrimitiveState {
319 topology: wgpu::PrimitiveTopology::TriangleList,
320 strip_index_format: None,
321 front_face: wgpu::FrontFace::Ccw,
322 cull_mode: None,
323 unclipped_depth: false,
324 polygon_mode: wgpu::PolygonMode::Fill,
325 conservative: false,
326 },
327 depth_stencil: None,
328 multisample: wgpu::MultisampleState::default(),
329 fragment: Some(wgpu::FragmentState {
330 module: &fragment_module.module,
331 entry_point: Some(fragment_module.entry_point),
332 targets: &[Some(wgpu::ColorTargetState {
333 format: wgpu::TextureFormat::Rgba16Float,
334 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
335 write_mask: wgpu::ColorWrites::all(),
336 })],
337 compilation_options: Default::default(),
338 }),
339 multiview: None,
340 cache: None,
341 })
342}
343
344fn create_mix_bindgroup(
345 device: &wgpu::Device,
346 pipeline: &wgpu::RenderPipeline,
347 slab_buffer: &wgpu::Buffer,
348 hdr_texture: &Texture,
349 bloom_texture: &Texture,
350) -> wgpu::BindGroup {
351 device.create_bind_group(&wgpu::BindGroupDescriptor {
352 label: Some("bloom mix"),
353 layout: &pipeline.get_bind_group_layout(0),
354 entries: &[
355 wgpu::BindGroupEntry {
356 binding: 0,
357 resource: wgpu::BindingResource::Buffer(slab_buffer.as_entire_buffer_binding()),
358 },
359 wgpu::BindGroupEntry {
360 binding: 1,
361 resource: wgpu::BindingResource::TextureView(&hdr_texture.view),
362 },
363 wgpu::BindGroupEntry {
364 binding: 2,
365 resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler),
366 },
367 wgpu::BindGroupEntry {
368 binding: 3,
369 resource: wgpu::BindingResource::TextureView(&bloom_texture.view),
370 },
371 wgpu::BindGroupEntry {
372 binding: 4,
373 resource: wgpu::BindingResource::Sampler(&bloom_texture.sampler),
374 },
375 ],
376 })
377}
378
379#[derive(Clone)]
386pub struct Bloom {
387 slab: SlabAllocator<WgpuRuntime>,
388 slab_buffer: SlabBuffer<wgpu::Buffer>,
389
390 downsample_pixel_sizes: HybridArray<Vec2>,
391 downsample_pipeline: Arc<wgpu::RenderPipeline>,
392
393 upsample_filter_radius: Hybrid<Vec2>,
394 upsample_pipeline: Arc<wgpu::RenderPipeline>,
395
396 textures: Arc<RwLock<Vec<texture::Texture>>>,
397 bindgroups: Arc<RwLock<Vec<wgpu::BindGroup>>>,
398 hdr_texture_downsample_bindgroup: Arc<RwLock<wgpu::BindGroup>>,
399
400 mix_pipeline: Arc<wgpu::RenderPipeline>,
401 mix_bindgroup: Arc<RwLock<wgpu::BindGroup>>,
402 mix_texture: Arc<RwLock<Texture>>,
403 mix_strength: Hybrid<f32>,
404}
405
406impl Bloom {
407 pub fn new(runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) -> Self {
408 let runtime = runtime.as_ref();
409 let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());
410
411 let slab = SlabAllocator::new(runtime, "bloom-slab", wgpu::BufferUsages::empty());
412 let downsample_pixel_sizes = slab.new_array(
413 config_resolutions(resolution).map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32)),
414 );
415 let upsample_filter_radius =
416 slab.new_value(1.0 / Vec2::new(resolution.x as f32, resolution.y as f32));
417 let mix_strength = slab.new_value(0.04f32);
418 let slab_buffer = slab.commit();
419
420 let downsample_pipeline = Arc::new(create_bloom_downsample_pipeline(&runtime.device));
421 let upsample_pipeline = Arc::new(create_bloom_upsample_pipeline(&runtime.device));
422 let mix_pipeline = Arc::new(create_mix_pipeline(&runtime.device));
423
424 let hdr_texture_downsample_bindgroup = create_bindgroup(
425 &runtime.device,
426 &downsample_pipeline.get_bind_group_layout(0),
427 &slab_buffer,
428 hdr_texture,
429 );
430 let mix_texture = create_texture(
431 runtime,
432 resolution.x,
433 resolution.y,
434 Some("bloom mix"),
435 wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
436 );
437
438 let textures = create_textures(runtime, resolution);
439 let bindgroups = create_bindgroups(
440 &runtime.device,
441 &downsample_pipeline,
442 &slab_buffer,
443 &textures,
444 );
445
446 let mix_bindgroup = create_mix_bindgroup(
447 &runtime.device,
448 &mix_pipeline,
449 &slab_buffer,
450 hdr_texture,
451 &textures[0],
452 );
453
454 Self {
455 slab,
456 slab_buffer,
457 downsample_pixel_sizes,
458 downsample_pipeline,
459 upsample_filter_radius,
460 upsample_pipeline,
461 textures: Arc::new(RwLock::new(textures)),
462 bindgroups: Arc::new(RwLock::new(bindgroups)),
463 hdr_texture_downsample_bindgroup: Arc::new(RwLock::new(
464 hdr_texture_downsample_bindgroup,
465 )),
466 mix_pipeline,
467 mix_texture: Arc::new(RwLock::new(mix_texture)),
468 mix_bindgroup: Arc::new(RwLock::new(mix_bindgroup)),
469 mix_strength,
470 }
471 }
472
473 pub(crate) fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {
474 &self.slab
475 }
476
477 pub fn set_mix_strength(&self, strength: f32) {
478 self.mix_strength.set(strength);
479 }
480
481 pub fn get_mix_strength(&self) -> f32 {
482 self.mix_strength.get()
483 }
484
485 pub fn set_filter_radius(&self, filter_radius: f32) {
487 let size = self.get_size();
488 let filter_radius = Vec2::new(filter_radius / size.x as f32, filter_radius / size.y as f32);
489 self.upsample_filter_radius.set(filter_radius);
490 }
491
492 pub fn get_filter_radius(&self) -> Vec2 {
493 self.upsample_filter_radius.get()
494 }
495
496 pub fn get_size(&self) -> UVec2 {
497 let mix_texture = self.get_mix_texture();
498 UVec2::new(mix_texture.width(), mix_texture.height())
499 }
500
501 pub fn set_hdr_texture(&self, runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) {
503 let slab_buffer = self.slab.get_buffer().unwrap();
505 let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());
506 let runtime = runtime.as_ref();
507 let textures = create_textures(runtime, resolution);
508
509 *self.bindgroups.write().unwrap() = create_bindgroups(
510 &runtime.device,
511 &self.downsample_pipeline,
512 &slab_buffer,
513 &textures,
514 );
515 *self.hdr_texture_downsample_bindgroup.write().unwrap() = create_bindgroup(
516 &runtime.device,
517 &self.downsample_pipeline.get_bind_group_layout(0),
518 &slab_buffer,
519 hdr_texture,
520 );
521 *self.mix_texture.write().unwrap() = create_texture(
522 runtime,
523 resolution.x,
524 resolution.y,
525 Some("bloom mix"),
526 wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
527 );
528 *self.mix_bindgroup.write().unwrap() = create_mix_bindgroup(
529 &runtime.device,
530 &self.mix_pipeline,
531 &slab_buffer,
532 hdr_texture,
533 &textures[0],
534 );
535 *self.textures.write().unwrap() = textures;
536 }
537
538 pub fn get_mix_texture(&self) -> Texture {
543 self.mix_texture.read().unwrap().clone()
545 }
546
547 pub(crate) fn render_downsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
548 struct DownsampleItem<'a> {
549 view: &'a wgpu::TextureView,
550 bindgroup: &'a wgpu::BindGroup,
551 pixel_size: Id<Vec2>,
552 }
553 let textures_guard = self.textures.read().unwrap();
560 let hdr_texture_downsample_bindgroup_guard =
561 self.hdr_texture_downsample_bindgroup.read().unwrap();
562 let hdr_texture_downsample_bindgroup: &wgpu::BindGroup =
563 &hdr_texture_downsample_bindgroup_guard;
564 let bindgroups_guard = self.bindgroups.read().unwrap();
565 let bindgroups =
566 std::iter::once(hdr_texture_downsample_bindgroup).chain(bindgroups_guard.iter());
567 let items = textures_guard
568 .iter()
569 .zip(bindgroups)
570 .zip(self.downsample_pixel_sizes.array().iter())
571 .map(|((tex, bindgroup), pixel_size)| DownsampleItem {
572 view: &tex.view,
573 bindgroup,
574 pixel_size,
575 });
576 for (
577 i,
578 DownsampleItem {
579 view,
580 bindgroup,
581 pixel_size,
582 },
583 ) in items.enumerate()
584 {
585 let title = format!("bloom downsample {i}");
586 let label = Some(title.as_str());
587 let mut encoder =
588 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
589 {
590 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
591 label,
592 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
593 view,
594 resolve_target: None,
595 ops: wgpu::Operations {
596 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
597 store: wgpu::StoreOp::Store,
598 },
599 depth_slice: None,
600 })],
601 depth_stencil_attachment: None,
602 timestamp_writes: None,
603 occlusion_query_set: None,
604 });
605 render_pass.set_pipeline(&self.downsample_pipeline);
606 render_pass.set_bind_group(0, Some(bindgroup), &[]);
607 let id = pixel_size.into();
608 render_pass.draw(0..6, id..id + 1);
609 }
610 queue.submit(std::iter::once(encoder.finish()));
611 }
612 }
613
614 fn render_upsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
615 struct UpsampleItem<'a> {
616 view: &'a wgpu::TextureView,
617 bindgroup: &'a wgpu::BindGroup,
618 }
619 let bindgroups_guard = self.bindgroups.read().unwrap();
623 let bindgroups = bindgroups_guard.iter().rev();
624 let textures_guard = self.textures.read().unwrap();
627 let views = textures_guard.iter().rev().skip(1).map(|t| &t.view);
628 let items = bindgroups
629 .zip(views)
630 .map(|(bindgroup, view)| UpsampleItem { view, bindgroup });
631 for (i, UpsampleItem { view, bindgroup }) in items.enumerate() {
632 let title = format!("bloom upsample {}", textures_guard.len() - i - 1);
633 let label = Some(title.as_str());
634 let mut encoder =
635 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
636 {
637 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
638 label,
639 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
640 view,
641 resolve_target: None,
642 ops: wgpu::Operations {
643 load: wgpu::LoadOp::Load,
644 store: wgpu::StoreOp::Store,
645 },
646 depth_slice: None,
647 })],
648 depth_stencil_attachment: None,
649 timestamp_writes: None,
650 occlusion_query_set: None,
651 });
652 render_pass.set_pipeline(&self.upsample_pipeline);
653 render_pass.set_bind_group(0, Some(bindgroup), &[]);
654 let id = self.upsample_filter_radius.id().into();
655 render_pass.draw(0..6, id..id + 1);
656 }
657 queue.submit(std::iter::once(encoder.finish()));
658 }
659 }
660
661 fn render_mix(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
662 let label = Some("bloom mix");
663 let mix_texture = self.mix_texture.read().unwrap();
665 let mix_bindgroup = self.mix_bindgroup.read().unwrap();
666 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
667 {
668 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
669 label,
670 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
671 view: &mix_texture.view,
672 resolve_target: None,
673 ops: wgpu::Operations {
674 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
675 store: wgpu::StoreOp::Store,
676 },
677 depth_slice: None,
678 })],
679 depth_stencil_attachment: None,
680 timestamp_writes: None,
681 occlusion_query_set: None,
682 });
683 render_pass.set_pipeline(&self.mix_pipeline);
684 render_pass.set_bind_group(0, Some(mix_bindgroup.deref()), &[]);
685 let id = self.mix_strength.id().into();
686 render_pass.draw(0..6, id..id + 1);
687 }
688
689 queue.submit(std::iter::once(encoder.finish()));
690 }
691
692 pub fn bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
693 self.slab.commit();
694 assert!(
695 self.slab_buffer.is_valid(),
696 "bloom slab buffer should never resize"
697 );
698 self.render_downsamples(device, queue);
699 self.render_upsamples(device, queue);
700 self.render_mix(device, queue);
701 }
702}
703
704#[cfg(test)]
705mod test {
706 use glam::Vec3;
707
708 use crate::{context::Context, test::BlockOnFuture};
709
710 use super::*;
711
712 #[test]
713 fn bloom_texture_sizes_sanity() {
714 let sizes = config_resolutions(UVec2::new(1024, 800)).collect::<Vec<_>>();
715 assert_eq!(
716 vec![
717 UVec2::new(1024, 800),
718 UVec2::new(512, 400),
719 UVec2::new(256, 200),
720 UVec2::new(128, 100),
721 UVec2::new(64, 50),
722 UVec2::new(32, 25),
723 UVec2::new(16, 12),
724 UVec2::new(8, 6),
725 UVec2::new(4, 3),
726 UVec2::new(2, 1),
727 ],
728 sizes
729 );
730 let pixel_sizes = config_resolutions(UVec2::new(1024, 800))
731 .map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32))
732 .collect::<Vec<_>>();
733 assert_eq!(
734 vec![
735 Vec2::new(0.0009765625, 0.00125),
736 Vec2::new(0.001953125, 0.0025),
737 Vec2::new(0.00390625, 0.005),
738 Vec2::new(0.0078125, 0.01),
739 Vec2::new(0.015625, 0.02),
740 Vec2::new(0.03125, 0.04),
741 Vec2::new(0.0625, 0.083333336),
742 Vec2::new(0.125, 0.16666667),
743 Vec2::new(0.25, 0.33333334),
744 Vec2::new(0.5, 1.0)
745 ],
746 pixel_sizes
747 );
748 }
749
750 #[test]
751 fn bloom_sanity() {
752 let width = 256;
753 let height = 128;
754 let ctx = Context::headless(width, height).block();
755 let stage = ctx.new_stage().with_bloom(false);
756 let projection = crate::camera::perspective(width as f32, height as f32);
757 let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);
758 let _camera = stage
759 .new_camera()
760 .with_projection_and_view(projection, view);
761 let skybox = stage
762 .new_skybox_from_path("../../img/hdr/night.hdr")
763 .unwrap();
764 stage.use_skybox(&skybox);
765 let ibl = stage.new_ibl(&skybox);
766 stage.use_ibl(&ibl);
767
768 let _doc = stage
769 .load_gltf_document_from_path("../../gltf/EmissiveStrengthTest.glb")
770 .unwrap();
771
772 let frame = ctx.get_next_frame().unwrap();
773 stage.render(&frame.view());
774 let img = frame.read_image().block().unwrap();
775 img_diff::assert_img_eq("bloom/without.png", img);
776 frame.present();
777
778 stage.set_has_bloom(true);
780 stage.set_bloom_mix_strength(0.1);
781 stage.set_bloom_filter_radius(2.0);
782 let frame = ctx.get_next_frame().unwrap();
783 stage.render(&frame.view());
784 let img = frame.read_image().block().unwrap();
785 img_diff::assert_img_eq("bloom/with.png", img);
786 }
787}