comfy_wgpu/bloom.rs
1use crate::*;
2
3// Blend factor & settings taken from bevy
4
5pub const BLOOM_MIP_LEVEL_COUNT: u32 = 10;
6
7const BLUR_DIR_ZERO: [u32; 4] = [0, 0, 0, 0];
8const BLUR_DIR_ONE: [u32; 4] = [1, 0, 0, 0];
9
10pub struct Bloom {
11 pub context: GraphicsContext,
12
13 pub format: wgpu::TextureFormat,
14 pub threshold: PostProcessingEffect,
15 pub mipmap_generator: MipmapGenerator,
16 pub blur_texture: BindableTexture,
17 pub mip_blur_pipeline: wgpu::RenderPipeline,
18
19 pub gaussian_pipeline: wgpu::RenderPipeline,
20
21 pub blur_direction_buffer_0: wgpu::Buffer,
22 pub blur_direction_buffer_1: wgpu::Buffer,
23 pub blur_direction_group_0: wgpu::BindGroup,
24 pub blur_direction_group_1: wgpu::BindGroup,
25 pub blur_direction_layout: wgpu::BindGroupLayout,
26
27 pub pingpong: [BindableTexture; 2],
28
29 pub lighting_params: Arc<wgpu::BindGroup>,
30}
31
32impl Bloom {
33 pub fn new(
34 context: &GraphicsContext,
35 shaders: &mut ShaderMap,
36 format: wgpu::TextureFormat,
37 lighting_params: Arc<wgpu::BindGroup>,
38 lighting_params_layout: &wgpu::BindGroupLayout,
39 ) -> Self {
40 let config = context.config.borrow();
41
42 let device = &context.device;
43 let texture_layout = &context.texture_layout;
44
45 let threshold_render_texture =
46 Texture::create_scaled_mip_filter_surface_texture(
47 device,
48 &config,
49 format,
50 1.0,
51 BLOOM_MIP_LEVEL_COUNT,
52 wgpu::FilterMode::Linear,
53 "Bloom Threshold Texture",
54 );
55
56 let threshold = {
57 let shader = create_engine_post_processing_shader!(
58 shaders,
59 "bloom-threshold"
60 );
61
62 PostProcessingEffect {
63 id: shader.id,
64 name: "Bloom Threshold".into(),
65 enabled: true,
66 bind_group: device.simple_bind_group(
67 Some("Bloom Threshold Bind Group"),
68 &threshold_render_texture,
69 texture_layout,
70 ),
71 render_texture: threshold_render_texture,
72 pipeline: create_post_processing_pipeline(
73 "Bloom Threshold",
74 device,
75 format,
76 &[texture_layout, lighting_params_layout],
77 shader,
78 wgpu::BlendState::REPLACE,
79 ),
80 }
81 };
82
83 let (width, height) = {
84 let config = context.config.borrow();
85 (config.width, config.height)
86 };
87
88 let blur_texture = BindableTexture::new(
89 device,
90 texture_layout,
91 &TextureCreationParams {
92 label: Some("Bloom Blue Texture"),
93 width,
94 height,
95 format,
96 ..Default::default()
97 },
98 );
99
100 let mip_blur_pipeline = create_post_processing_pipeline(
101 "Bloom Blur",
102 device,
103 format,
104 &[texture_layout],
105 create_engine_post_processing_shader!(shaders, "bloom-mip-blur"),
106 wgpu::BlendState {
107 color: wgpu::BlendComponent {
108 src_factor: wgpu::BlendFactor::Constant,
109 dst_factor: wgpu::BlendFactor::One,
110 operation: wgpu::BlendOperation::Add,
111 },
112 alpha: wgpu::BlendComponent::REPLACE,
113 },
114 );
115
116 let mipmap_generator = MipmapGenerator::new(device, format);
117
118 let params = TextureCreationParams {
119 label: Some("Bloom Ping Pong 0"),
120 width,
121 height,
122 format,
123 ..Default::default()
124 };
125
126 let pingpong = [
127 BindableTexture::new(
128 device,
129 texture_layout,
130 &TextureCreationParams {
131 label: Some("Bloom Ping Pong 0"),
132 ..params
133 },
134 ),
135 BindableTexture::new(
136 device,
137 texture_layout,
138 &TextureCreationParams {
139 label: Some("Bloom Ping Pong 1"),
140 ..params
141 },
142 ),
143 ];
144
145 let blur_direction_layout = context.device.create_bind_group_layout(
146 &wgpu::BindGroupLayoutDescriptor {
147 label: Some("Bloom Blur Direction Layout"),
148 entries: &[wgpu::BindGroupLayoutEntry {
149 binding: 0,
150 visibility: wgpu::ShaderStages::FRAGMENT,
151 ty: wgpu::BindingType::Buffer {
152 ty: wgpu::BufferBindingType::Uniform,
153 has_dynamic_offset: false,
154 min_binding_size: None,
155 },
156 count: None,
157 }],
158 },
159 );
160
161 let blur_direction_buffer_0 = context.device.create_buffer_init(
162 &wgpu::util::BufferInitDescriptor {
163 label: Some("Bloom Blur Direction Buffer = 0"),
164 contents: bytemuck::cast_slice(&[BLUR_DIR_ZERO]),
165 usage: wgpu::BufferUsages::UNIFORM,
166 },
167 );
168
169 let blur_direction_buffer_1 = context.device.create_buffer_init(
170 &wgpu::util::BufferInitDescriptor {
171 label: Some("Bloom Blur Direction Buffer = 1"),
172 contents: bytemuck::cast_slice(&[BLUR_DIR_ONE]),
173 usage: wgpu::BufferUsages::UNIFORM,
174 },
175 );
176
177 let blur_direction_group_0 =
178 context.device.create_bind_group(&wgpu::BindGroupDescriptor {
179 label: Some("Bloom Blur Direction Bind Group = 0"),
180 layout: &blur_direction_layout,
181 entries: &[wgpu::BindGroupEntry {
182 binding: 0,
183 resource: blur_direction_buffer_0.as_entire_binding(),
184 }],
185 });
186
187 let blur_direction_group_1 =
188 context.device.create_bind_group(&wgpu::BindGroupDescriptor {
189 label: Some("Bloom Blur Direction Bind Group = 1"),
190 layout: &blur_direction_layout,
191 entries: &[wgpu::BindGroupEntry {
192 binding: 0,
193 resource: blur_direction_buffer_1.as_entire_binding(),
194 }],
195 });
196
197 let gaussian_pipeline = create_post_processing_pipeline(
198 "Bloom Gaussian",
199 device,
200 format,
201 &[texture_layout, lighting_params_layout, &blur_direction_layout],
202 create_engine_post_processing_shader!(shaders, "bloom-gauss"),
203 wgpu::BlendState::REPLACE,
204 );
205
206
207 Self {
208 context: context.clone(),
209
210 format,
211 threshold,
212 mipmap_generator,
213 // mipmaps, mipmaps_bind_group,
214 blur_texture,
215 mip_blur_pipeline,
216
217 blur_direction_buffer_0,
218 blur_direction_buffer_1,
219 blur_direction_group_0,
220 blur_direction_group_1,
221
222 blur_direction_layout,
223
224 pingpong,
225 gaussian_pipeline,
226
227 lighting_params,
228 }
229 }
230
231 pub fn draw(
232 &self,
233 device: &wgpu::Device,
234 layout: &wgpu::BindGroupLayout,
235 first_pass_bind_group: &wgpu::BindGroup,
236 encoder: &mut wgpu::CommandEncoder,
237 ) {
238 draw_post_processing_output(
239 "Bloom Threshold",
240 encoder,
241 &self.threshold.pipeline,
242 first_pass_bind_group,
243 &self.lighting_params,
244 &self.threshold.render_texture.view,
245 true,
246 None,
247 );
248
249 {
250 let mut horizontal = true;
251 let mut first_iteration = true;
252
253 let amount = 20;
254
255 for iter in 0..amount {
256 let i = if horizontal { 1 } else { 0 };
257
258 let tex = if first_iteration {
259 &self.threshold.bind_group
260 } else {
261 &self.pingpong[if horizontal { 0 } else { 1 }].bind_group
262 };
263
264 // let horizontal_u: u32 = i as u32;
265 // draw_post_processing_output(
266 // encoder,
267 // &self.gaussian_pipeline,
268 // tex,
269 // &self.lighting_params,
270 // // &self.threshold.bind_group,
271 // &self.pingpong[i].texture.view,
272 // true,
273 // None,
274 // );
275
276 {
277 // self.context.queue.write_buffer(
278 // &self.blur_direction_buffer,
279 // 0,
280 // bytemuck::cast_slice(&[if horizontal { 0 } else { 1 }]),
281 // );
282
283 let mut render_pass = encoder.begin_render_pass(
284 &wgpu::RenderPassDescriptor {
285 label: Some(&format!(
286 "Bloom Pingpong {} Post Processing Render Pass",
287 iter
288 )),
289 color_attachments: &[Some(
290 wgpu::RenderPassColorAttachment {
291 view: &self.pingpong[i].texture.view,
292 resolve_target: None,
293 ops: wgpu::Operations {
294 load: wgpu::LoadOp::Clear(
295 wgpu::Color {
296 r: 0.0,
297 g: 0.0,
298 b: 0.0,
299 a: 1.0,
300 },
301 ),
302 store: wgpu::StoreOp::Store,
303 },
304 },
305 )],
306 depth_stencil_attachment: None,
307 timestamp_writes: None,
308 occlusion_query_set: None,
309 },
310 );
311
312 render_pass.set_pipeline(&self.gaussian_pipeline);
313 render_pass.set_bind_group(0, tex, &[]);
314 render_pass.set_bind_group(1, &self.lighting_params, &[]);
315 render_pass.set_bind_group(
316 2,
317 if horizontal {
318 &self.blur_direction_group_0
319 } else {
320 &self.blur_direction_group_1
321 },
322 &[],
323 );
324
325 render_pass.draw(0..3, 0..1);
326 }
327
328 horizontal = !horizontal;
329
330 if first_iteration {
331 first_iteration = false;
332 }
333 }
334 }
335
336 let use_mipmaps = false;
337
338 if use_mipmaps {
339 self.mipmap_generator.generate_mipmaps(
340 encoder,
341 device,
342 &self.threshold.render_texture.texture,
343 BLOOM_MIP_LEVEL_COUNT,
344 );
345
346 for i in 0..BLOOM_MIP_LEVEL_COUNT {
347 let mip_view =
348 self.threshold.render_texture.texture.create_view(
349 &wgpu::TextureViewDescriptor {
350 base_mip_level: i,
351 mip_level_count: Some(1),
352 ..Default::default()
353 },
354 );
355
356 let mip_bind_group =
357 device.create_bind_group(&wgpu::BindGroupDescriptor {
358 label: Some(&format!("Bloom Blur Bind Group {}", i)),
359 layout,
360 entries: &[
361 wgpu::BindGroupEntry {
362 binding: 0,
363 resource: wgpu::BindingResource::TextureView(
364 &mip_view,
365 ),
366 },
367 wgpu::BindGroupEntry {
368 binding: 1,
369 resource: wgpu::BindingResource::Sampler(
370 &self.threshold.render_texture.sampler,
371 ),
372 },
373 ],
374 });
375
376 // TODO: get rid of tweaks later
377 let blend_variant = tweak!(1);
378 let constant_blend = tweak!(0.5);
379
380 let blend = if blend_variant < 3 {
381 let settings = match blend_variant {
382 0 => BloomSettings::NATURAL,
383 1 => BloomSettings::SCREEN_BLUR,
384 2 => BloomSettings::OLD_SCHOOL,
385 _ => unreachable!(),
386 };
387
388 compute_blend_factor(
389 &settings,
390 i as f32,
391 BLOOM_MIP_LEVEL_COUNT as f32,
392 ) as f64
393 } else {
394 constant_blend
395 };
396
397 draw_post_processing_output(
398 &format!("Bloom Blur {}", i),
399 encoder,
400 &self.mip_blur_pipeline,
401 &mip_bind_group,
402 &self.lighting_params,
403 &self.blur_texture.texture.view,
404 i == 0,
405 Some(blend),
406 );
407 }
408 }
409 }
410
411 pub fn blit_final(
412 &self,
413 encoder: &mut wgpu::CommandEncoder,
414 shaders: &mut ShaderMap,
415 pipelines: &mut PipelineMap,
416 output_view: &wgpu::TextureView,
417 target_format: wgpu::TextureFormat,
418 params: &GlobalLightingParams,
419 ) {
420 let pipeline_name = format!("Bloom Merge {:?}", target_format);
421
422 let pipeline = if let Some(pipeline) = pipelines.get(&pipeline_name) {
423 pipeline
424 } else {
425 let pipeline = create_post_processing_pipeline(
426 &pipeline_name,
427 &self.context.device,
428 target_format,
429 &[&self.context.texture_layout],
430 create_engine_post_processing_shader!(shaders, "bloom-merge"),
431 wgpu::BlendState {
432 color: wgpu::BlendComponent {
433 src_factor: wgpu::BlendFactor::Constant,
434 dst_factor: wgpu::BlendFactor::OneMinusConstant,
435 operation: wgpu::BlendOperation::Add,
436 },
437 alpha: wgpu::BlendComponent::REPLACE,
438 },
439 );
440
441 pipelines.insert(pipeline_name.clone(), pipeline);
442 pipelines.get(&pipeline_name).unwrap()
443 };
444
445
446 draw_post_processing_output(
447 "Bloom Merge",
448 encoder,
449 pipeline,
450 if GlobalParams::get_int("bloom_alg") == 0 {
451 &self.blur_texture.bind_group
452 } else {
453 &self.pingpong[0].bind_group
454 },
455 &self.lighting_params,
456 output_view,
457 false,
458 Some(params.bloom_lerp as f64),
459 );
460 }
461}
462
463
464// {,
465// let mut encoder = self.device.simple_encoder("Bloom MipMaps");
466
467// encoder.copy_texture_to_texture(
468// wgpu::ImageCopyTexture {
469// aspect: wgpu::TextureAspect::All,
470// texture: &self.bloom.threshold.render_texture.texture,
471// mip_level: 0,
472// origin: wgpu::Origin3d::ZERO,
473// },
474// wgpu::ImageCopyTexture {
475// aspect: wgpu::TextureAspect::All,
476// texture: &self.bloom.mipmaps.texture,
477// mip_level: 0,
478// origin: wgpu::Origin3d::ZERO,
479// },
480// wgpu::Extent3d {
481// width: self.config.width,
482// height: self.config.height,
483// depth_or_array_layers: 1,
484// },
485// );
486//
487// let blur_texture = device.create_texture(&wgpu::TextureDescriptor {
488// label: Some("Bloom Blur Texture"),
489// size: wgpu::Extent3d {
490// width: config.width,
491// height: config.height,
492// depth_or_array_layers: 1,
493// },
494// mip_level_count: 1,
495// sample_count: 1,
496// dimension: wgpu::TextureDimension::D2,
497// format: wgpu::TextureFormat::Rgba32Float,
498// usage: wgpu::TextureUsages::TEXTURE_BINDING |
499// wgpu::TextureUsages::COPY_DST |
500// wgpu::TextureUsages::RENDER_ATTACHMENT,
501// view_formats: &[],
502// });
503
504
505/// Calculates blend intensities of blur pyramid levels
506/// during the upsampling + compositing stage.
507///
508/// The function assumes all pyramid levels are upsampled and
509/// blended into higher frequency ones using this function to
510/// calculate blend levels every time. The final (highest frequency)
511/// pyramid level in not blended into anything therefore this function
512/// is not applied to it. As a result, the *mip* parameter of 0 indicates
513/// the second-highest frequency pyramid level (in our case that is the
514/// 0th mip of the bloom texture with the original image being the
515/// actual highest frequency level).
516///
517/// Parameters:
518/// * *mip* - the index of the lower frequency pyramid level (0 - max_mip, where 0 indicates highest frequency mip but not the highest frequency image).
519/// * *max_mip* - the index of the lowest frequency pyramid level.
520///
521/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
522/// [`BloomSettings`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
523#[allow(clippy::doc_markdown)]
524fn compute_blend_factor(
525 bloom_settings: &BloomSettings,
526 mip: f32,
527 max_mip: f32,
528) -> f32 {
529 let mut lf_boost =
530 (1.0 - (1.0 - (mip / max_mip))
531 .powf(1.0 / (1.0 - bloom_settings.low_frequency_boost_curvature))) *
532 bloom_settings.low_frequency_boost;
533 let high_pass_lq = 1.0 -
534 (((mip / max_mip) - bloom_settings.high_pass_frequency) /
535 bloom_settings.high_pass_frequency)
536 .clamp(0.0, 1.0);
537 lf_boost *= match bloom_settings.composite_mode {
538 BloomCompositeMode::EnergyConserving => 1.0 - bloom_settings.intensity,
539 BloomCompositeMode::Additive => 1.0,
540 };
541
542 (bloom_settings.intensity + lf_boost) * high_pass_lq
543}
544
545
546/// Applies a bloom effect to an HDR-enabled 2d or 3d camera.
547///
548/// Bloom emulates an effect found in real cameras and the human eye,
549/// causing halos to appear around very bright parts of the scene.
550///
551/// See also <https://en.wikipedia.org/wiki/Bloom_(shader_effect)>.
552///
553/// # Usage Notes
554///
555/// **Bloom is currently not compatible with WebGL2.**
556///
557/// Often used in conjunction with `bevy_pbr::StandardMaterial::emissive` for 3d meshes.
558///
559/// Bloom is best used alongside a tonemapping function that desaturates bright colors,
560/// such as `TonyMcMapface`.
561///
562/// Bevy's implementation uses a parametric curve to blend between a set of
563/// blurred (lower frequency) images generated from the camera's view.
564/// See <https://starlederer.github.io/bloom/> for a visualization of the parametric curve
565/// used in Bevy as well as a visualization of the curve's respective scattering profile.
566#[allow(clippy::doc_markdown)]
567#[derive(Clone)]
568pub struct BloomSettings {
569 /// Controls the baseline of how much the image is scattered (default: 0.15).
570 ///
571 /// This parameter should be used only to control the strength of the bloom
572 /// for the scene as a whole. Increasing it too much will make the scene appear
573 /// blurry and over-exposed.
574 ///
575 /// To make a mesh glow brighter, rather than increase the bloom intensity,
576 /// you should increase the mesh's `emissive` value.
577 ///
578 /// # In energy-conserving mode
579 /// The value represents how likely the light is to scatter.
580 ///
581 /// The value should be between 0.0 and 1.0 where:
582 /// * 0.0 means no bloom
583 /// * 1.0 means the light is scattered as much as possible
584 ///
585 /// # In additive mode
586 /// The value represents how much scattered light is added to
587 /// the image to create the glow effect.
588 ///
589 /// In this configuration:
590 /// * 0.0 means no bloom
591 /// * > 0.0 means a proportionate amount of scattered light is added
592 pub intensity: f32,
593
594 /// Low frequency contribution boost.
595 /// Controls how much more likely the light
596 /// is to scatter completely sideways (low frequency image).
597 ///
598 /// Comparable to a low shelf boost on an equalizer.
599 ///
600 /// # In energy-conserving mode
601 /// The value should be between 0.0 and 1.0 where:
602 /// * 0.0 means low frequency light uses base intensity for blend factor calculation
603 /// * 1.0 means low frequency light contributes at full power
604 ///
605 /// # In additive mode
606 /// The value represents how much scattered light is added to
607 /// the image to create the glow effect.
608 ///
609 /// In this configuration:
610 /// * 0.0 means no bloom
611 /// * > 0.0 means a proportionate amount of scattered light is added
612 pub low_frequency_boost: f32,
613
614 /// Low frequency contribution boost curve.
615 /// Controls the curvature of the blend factor function
616 /// making frequencies next to the lowest ones contribute more.
617 ///
618 /// Somewhat comparable to the Q factor of an equalizer node.
619 ///
620 /// Valid range:
621 /// * 0.0 - base base intensity and boosted intensity are linearly interpolated
622 /// * 1.0 - all frequencies below maximum are at boosted intensity level
623 pub low_frequency_boost_curvature: f32,
624
625 /// Tightens how much the light scatters (default: 1.0).
626 ///
627 /// Valid range:
628 /// * 0.0 - maximum scattering angle is 0 degrees (no scattering)
629 /// * 1.0 - maximum scattering angle is 90 degrees
630 pub high_pass_frequency: f32,
631
632 pub prefilter_settings: BloomPrefilterSettings,
633
634 /// Controls whether bloom textures
635 /// are blended between or added to each other. Useful
636 /// if image brightening is desired and a must-change
637 /// if `prefilter_settings` are used.
638 ///
639 /// # Recommendation
640 /// Set to [`BloomCompositeMode::Additive`] if `prefilter_settings` are
641 /// configured in a non-energy-conserving way,
642 /// otherwise set to [`BloomCompositeMode::EnergyConserving`].
643 pub composite_mode: BloomCompositeMode,
644}
645
646impl BloomSettings {
647 /// The default bloom preset.
648 pub const NATURAL: Self = Self {
649 intensity: 0.15,
650 low_frequency_boost: 0.7,
651 low_frequency_boost_curvature: 0.95,
652 high_pass_frequency: 1.0,
653 prefilter_settings: BloomPrefilterSettings {
654 threshold: 0.0,
655 threshold_softness: 0.0,
656 },
657 composite_mode: BloomCompositeMode::EnergyConserving,
658 };
659
660 /// A preset that's similar to how older games did bloom.
661 pub const OLD_SCHOOL: Self = Self {
662 intensity: 0.05,
663 low_frequency_boost: 0.7,
664 low_frequency_boost_curvature: 0.95,
665 high_pass_frequency: 1.0,
666 prefilter_settings: BloomPrefilterSettings {
667 threshold: 0.6,
668 threshold_softness: 0.2,
669 },
670 composite_mode: BloomCompositeMode::Additive,
671 };
672
673 /// A preset that applies a very strong bloom, and blurs the whole screen.
674 pub const SCREEN_BLUR: Self = Self {
675 intensity: 1.0,
676 low_frequency_boost: 0.0,
677 low_frequency_boost_curvature: 0.0,
678 high_pass_frequency: 1.0 / 3.0,
679 prefilter_settings: BloomPrefilterSettings {
680 threshold: 0.0,
681 threshold_softness: 0.0,
682 },
683 composite_mode: BloomCompositeMode::EnergyConserving,
684 };
685}
686
687impl Default for BloomSettings {
688 fn default() -> Self {
689 Self::NATURAL
690 }
691}
692
693/// Applies a threshold filter to the input image to extract the brightest
694/// regions before blurring them and compositing back onto the original image.
695/// These settings are useful when emulating the 1990s-2000s game look.
696///
697/// # Considerations
698/// * Changing these settings creates a physically inaccurate image
699/// * Changing these settings makes it easy to make the final result look worse
700/// * Non-default prefilter settings should be used in conjunction with [`BloomCompositeMode::Additive`]
701#[derive(Default, Clone)]
702pub struct BloomPrefilterSettings {
703 /// Baseline of the quadratic threshold curve (default: 0.0).
704 ///
705 /// RGB values under the threshold curve will not contribute to the effect.
706 pub threshold: f32,
707
708 /// Controls how much to blend between the thresholded and non-thresholded colors (default: 0.0).
709 ///
710 /// 0.0 = Abrupt threshold, no blending
711 /// 1.0 = Fully soft threshold
712 ///
713 /// Values outside of the range [0.0, 1.0] will be clamped.
714 pub threshold_softness: f32,
715}
716
717#[derive(Clone, PartialEq, Eq, Hash, Copy)]
718pub enum BloomCompositeMode {
719 EnergyConserving,
720 Additive,
721}