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}