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().expect("bloom bindgroups write") = create_bindgroups(
510 &runtime.device,
511 &self.downsample_pipeline,
512 &slab_buffer,
513 &textures,
514 );
515 *self
516 .hdr_texture_downsample_bindgroup
517 .write()
518 .expect("bloom hdr downsample bindgroup write") = create_bindgroup(
519 &runtime.device,
520 &self.downsample_pipeline.get_bind_group_layout(0),
521 &slab_buffer,
522 hdr_texture,
523 );
524 *self.mix_texture.write().expect("bloom mix_texture write") = create_texture(
525 runtime,
526 resolution.x,
527 resolution.y,
528 Some("bloom mix"),
529 wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
530 );
531 *self
532 .mix_bindgroup
533 .write()
534 .expect("bloom mix_bindgroup write") = create_mix_bindgroup(
535 &runtime.device,
536 &self.mix_pipeline,
537 &slab_buffer,
538 hdr_texture,
539 &textures[0],
540 );
541 *self.textures.write().expect("bloom textures write") = textures;
542 }
543
544 pub fn get_mix_texture(&self) -> Texture {
549 self.mix_texture
551 .read()
552 .expect("bloom mix_texture read")
553 .clone()
554 }
555
556 pub(crate) fn render_downsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
557 struct DownsampleItem<'a> {
558 view: &'a wgpu::TextureView,
559 bindgroup: &'a wgpu::BindGroup,
560 pixel_size: Id<Vec2>,
561 }
562 let textures_guard = self.textures.read().expect("bloom textures read");
570 let hdr_texture_downsample_bindgroup_guard = self
571 .hdr_texture_downsample_bindgroup
572 .read()
573 .expect("bloom hdr downsample bindgroup read");
574 let hdr_texture_downsample_bindgroup: &wgpu::BindGroup =
575 &hdr_texture_downsample_bindgroup_guard;
576 let bindgroups_guard = self.bindgroups.read().expect("bloom bindgroups read");
577 let bindgroups =
578 std::iter::once(hdr_texture_downsample_bindgroup).chain(bindgroups_guard.iter());
579 let items = textures_guard
580 .iter()
581 .zip(bindgroups)
582 .zip(self.downsample_pixel_sizes.array().iter())
583 .map(|((tex, bindgroup), pixel_size)| DownsampleItem {
584 view: &tex.view,
585 bindgroup,
586 pixel_size,
587 });
588 for (
589 i,
590 DownsampleItem {
591 view,
592 bindgroup,
593 pixel_size,
594 },
595 ) in items.enumerate()
596 {
597 let title = format!("bloom downsample {i}");
598 let label = Some(title.as_str());
599 let mut encoder =
600 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
601 {
602 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
603 label,
604 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
605 view,
606 resolve_target: None,
607 ops: wgpu::Operations {
608 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
609 store: wgpu::StoreOp::Store,
610 },
611 depth_slice: None,
612 })],
613 depth_stencil_attachment: None,
614 timestamp_writes: None,
615 occlusion_query_set: None,
616 });
617 render_pass.set_pipeline(&self.downsample_pipeline);
618 render_pass.set_bind_group(0, Some(bindgroup), &[]);
619 let id = pixel_size.into();
620 render_pass.draw(0..6, id..id + 1);
621 }
622 queue.submit(std::iter::once(encoder.finish()));
623 }
624 }
625
626 fn render_upsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
627 struct UpsampleItem<'a> {
628 view: &'a wgpu::TextureView,
629 bindgroup: &'a wgpu::BindGroup,
630 }
631 let bindgroups_guard = self.bindgroups.read().expect("bloom bindgroups read");
635 let bindgroups = bindgroups_guard.iter().rev();
636 let textures_guard = self.textures.read().expect("bloom textures read");
639 let views = textures_guard.iter().rev().skip(1).map(|t| &t.view);
640 let items = bindgroups
641 .zip(views)
642 .map(|(bindgroup, view)| UpsampleItem { view, bindgroup });
643 for (i, UpsampleItem { view, bindgroup }) in items.enumerate() {
644 let title = format!("bloom upsample {}", textures_guard.len() - i - 1);
645 let label = Some(title.as_str());
646 let mut encoder =
647 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
648 {
649 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
650 label,
651 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
652 view,
653 resolve_target: None,
654 ops: wgpu::Operations {
655 load: wgpu::LoadOp::Load,
656 store: wgpu::StoreOp::Store,
657 },
658 depth_slice: None,
659 })],
660 depth_stencil_attachment: None,
661 timestamp_writes: None,
662 occlusion_query_set: None,
663 });
664 render_pass.set_pipeline(&self.upsample_pipeline);
665 render_pass.set_bind_group(0, Some(bindgroup), &[]);
666 let id = self.upsample_filter_radius.id().into();
667 render_pass.draw(0..6, id..id + 1);
668 }
669 queue.submit(std::iter::once(encoder.finish()));
670 }
671 }
672
673 fn render_mix(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
674 let label = Some("bloom mix");
675 let mix_texture = self.mix_texture.read().expect("bloom mix_texture read");
677 let mix_bindgroup = self.mix_bindgroup.read().expect("bloom mix_bindgroup read");
678 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
679 {
680 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
681 label,
682 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
683 view: &mix_texture.view,
684 resolve_target: None,
685 ops: wgpu::Operations {
686 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
687 store: wgpu::StoreOp::Store,
688 },
689 depth_slice: None,
690 })],
691 depth_stencil_attachment: None,
692 timestamp_writes: None,
693 occlusion_query_set: None,
694 });
695 render_pass.set_pipeline(&self.mix_pipeline);
696 render_pass.set_bind_group(0, Some(mix_bindgroup.deref()), &[]);
697 let id = self.mix_strength.id().into();
698 render_pass.draw(0..6, id..id + 1);
699 }
700
701 queue.submit(std::iter::once(encoder.finish()));
702 }
703
704 pub fn bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
705 self.slab.commit();
706 assert!(
707 self.slab_buffer.is_valid(),
708 "bloom slab buffer should never resize"
709 );
710 self.render_downsamples(device, queue);
711 self.render_upsamples(device, queue);
712 self.render_mix(device, queue);
713 }
714}
715
716#[cfg(test)]
717mod test {
718 use glam::Vec3;
719
720 use crate::{context::Context, test::BlockOnFuture};
721
722 use super::*;
723
724 #[test]
725 fn bloom_texture_sizes_sanity() {
726 let sizes = config_resolutions(UVec2::new(1024, 800)).collect::<Vec<_>>();
727 assert_eq!(
728 vec![
729 UVec2::new(1024, 800),
730 UVec2::new(512, 400),
731 UVec2::new(256, 200),
732 UVec2::new(128, 100),
733 UVec2::new(64, 50),
734 UVec2::new(32, 25),
735 UVec2::new(16, 12),
736 UVec2::new(8, 6),
737 UVec2::new(4, 3),
738 UVec2::new(2, 1),
739 ],
740 sizes
741 );
742 let pixel_sizes = config_resolutions(UVec2::new(1024, 800))
743 .map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32))
744 .collect::<Vec<_>>();
745 assert_eq!(
746 vec![
747 Vec2::new(0.0009765625, 0.00125),
748 Vec2::new(0.001953125, 0.0025),
749 Vec2::new(0.00390625, 0.005),
750 Vec2::new(0.0078125, 0.01),
751 Vec2::new(0.015625, 0.02),
752 Vec2::new(0.03125, 0.04),
753 Vec2::new(0.0625, 0.083333336),
754 Vec2::new(0.125, 0.16666667),
755 Vec2::new(0.25, 0.33333334),
756 Vec2::new(0.5, 1.0)
757 ],
758 pixel_sizes
759 );
760 }
761
762 #[test]
763 fn bloom_sanity() {
764 let width = 256;
765 let height = 128;
766 let ctx = Context::headless(width, height).block();
767 let stage = ctx.new_stage().with_bloom(false);
768 let projection = crate::camera::perspective(width as f32, height as f32);
769 let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);
770 let _camera = stage
771 .new_camera()
772 .with_projection_and_view(projection, view);
773 let skybox = stage
774 .new_skybox_from_path("../../img/hdr/night.hdr")
775 .unwrap();
776 stage.use_skybox(&skybox);
777 let ibl = stage.new_ibl(&skybox);
778 stage.use_ibl(&ibl);
779
780 let _doc = stage
781 .load_gltf_document_from_path("../../gltf/EmissiveStrengthTest.glb")
782 .unwrap();
783
784 let frame = ctx.get_next_frame().unwrap();
785 stage.render(&frame.view());
786 let img = frame.read_image().block().unwrap();
787 img_diff::assert_img_eq("bloom/without.png", img);
788 frame.present();
789
790 stage.set_has_bloom(true);
792 stage.set_bloom_mix_strength(0.1);
793 stage.set_bloom_filter_radius(2.0);
794 let frame = ctx.get_next_frame().unwrap();
795 stage.render(&frame.view());
796 let img = frame.read_image().block().unwrap();
797 img_diff::assert_img_eq("bloom/with.png", img);
798 }
799}