bevy_core_pipeline/bloom/
mod.rs

1mod downsampling_pipeline;
2mod settings;
3mod upsampling_pipeline;
4
5use bevy_color::{Gray, LinearRgba};
6#[allow(deprecated)]
7pub use settings::{
8    Bloom, BloomCompositeMode, BloomPrefilter, BloomPrefilterSettings, BloomSettings,
9};
10
11use crate::{
12    core_2d::graph::{Core2d, Node2d},
13    core_3d::graph::{Core3d, Node3d},
14};
15use bevy_app::{App, Plugin};
16use bevy_asset::{load_internal_asset, Handle};
17use bevy_ecs::{prelude::*, query::QueryItem};
18use bevy_math::{ops, UVec2};
19use bevy_render::{
20    camera::ExtractedCamera,
21    diagnostic::RecordDiagnostics,
22    extract_component::{
23        ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,
24    },
25    render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
26    render_resource::*,
27    renderer::{RenderContext, RenderDevice},
28    texture::{CachedTexture, TextureCache},
29    view::ViewTarget,
30    Render, RenderApp, RenderSet,
31};
32use downsampling_pipeline::{
33    prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
34    BloomUniforms,
35};
36use upsampling_pipeline::{
37    prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
38};
39
40const BLOOM_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(929599476923908);
41
42const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
43
44pub struct BloomPlugin;
45
46impl Plugin for BloomPlugin {
47    fn build(&self, app: &mut App) {
48        load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl);
49
50        app.register_type::<Bloom>();
51        app.register_type::<BloomPrefilter>();
52        app.register_type::<BloomCompositeMode>();
53        app.add_plugins((
54            ExtractComponentPlugin::<Bloom>::default(),
55            UniformComponentPlugin::<BloomUniforms>::default(),
56        ));
57
58        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
59            return;
60        };
61        render_app
62            .init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
63            .init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
64            .add_systems(
65                Render,
66                (
67                    prepare_downsampling_pipeline.in_set(RenderSet::Prepare),
68                    prepare_upsampling_pipeline.in_set(RenderSet::Prepare),
69                    prepare_bloom_textures.in_set(RenderSet::PrepareResources),
70                    prepare_bloom_bind_groups.in_set(RenderSet::PrepareBindGroups),
71                ),
72            )
73            // Add bloom to the 3d render graph
74            .add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core3d, Node3d::Bloom)
75            .add_render_graph_edges(
76                Core3d,
77                (Node3d::EndMainPass, Node3d::Bloom, Node3d::Tonemapping),
78            )
79            // Add bloom to the 2d render graph
80            .add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)
81            .add_render_graph_edges(
82                Core2d,
83                (Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping),
84            );
85    }
86
87    fn finish(&self, app: &mut App) {
88        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
89            return;
90        };
91        render_app
92            .init_resource::<BloomDownsamplingPipeline>()
93            .init_resource::<BloomUpsamplingPipeline>();
94    }
95}
96
97#[derive(Default)]
98struct BloomNode;
99impl ViewNode for BloomNode {
100    type ViewQuery = (
101        &'static ExtractedCamera,
102        &'static ViewTarget,
103        &'static BloomTexture,
104        &'static BloomBindGroups,
105        &'static DynamicUniformIndex<BloomUniforms>,
106        &'static Bloom,
107        &'static UpsamplingPipelineIds,
108        &'static BloomDownsamplingPipelineIds,
109    );
110
111    // Atypically for a post-processing effect, we do not need to
112    // use a secondary texture normally provided by view_target.post_process_write(),
113    // instead we write into our own bloom texture and then directly back onto main.
114    fn run(
115        &self,
116        _graph: &mut RenderGraphContext,
117        render_context: &mut RenderContext,
118        (
119            camera,
120            view_target,
121            bloom_texture,
122            bind_groups,
123            uniform_index,
124            bloom_settings,
125            upsampling_pipeline_ids,
126            downsampling_pipeline_ids,
127        ): QueryItem<Self::ViewQuery>,
128        world: &World,
129    ) -> Result<(), NodeRunError> {
130        if bloom_settings.intensity == 0.0 {
131            return Ok(());
132        }
133
134        let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();
135        let pipeline_cache = world.resource::<PipelineCache>();
136        let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();
137
138        let (
139            Some(uniforms),
140            Some(downsampling_first_pipeline),
141            Some(downsampling_pipeline),
142            Some(upsampling_pipeline),
143            Some(upsampling_final_pipeline),
144        ) = (
145            uniforms.binding(),
146            pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),
147            pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),
148            pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),
149            pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),
150        )
151        else {
152            return Ok(());
153        };
154
155        render_context.command_encoder().push_debug_group("bloom");
156
157        let diagnostics = render_context.diagnostic_recorder();
158        let time_span = diagnostics.time_span(render_context.command_encoder(), "bloom");
159
160        // First downsample pass
161        {
162            let downsampling_first_bind_group = render_context.render_device().create_bind_group(
163                "bloom_downsampling_first_bind_group",
164                &downsampling_pipeline_res.bind_group_layout,
165                &BindGroupEntries::sequential((
166                    // Read from main texture directly
167                    view_target.main_texture_view(),
168                    &bind_groups.sampler,
169                    uniforms.clone(),
170                )),
171            );
172
173            let view = &bloom_texture.view(0);
174            let mut downsampling_first_pass =
175                render_context.begin_tracked_render_pass(RenderPassDescriptor {
176                    label: Some("bloom_downsampling_first_pass"),
177                    color_attachments: &[Some(RenderPassColorAttachment {
178                        view,
179                        resolve_target: None,
180                        ops: Operations::default(),
181                    })],
182                    depth_stencil_attachment: None,
183                    timestamp_writes: None,
184                    occlusion_query_set: None,
185                });
186            downsampling_first_pass.set_render_pipeline(downsampling_first_pipeline);
187            downsampling_first_pass.set_bind_group(
188                0,
189                &downsampling_first_bind_group,
190                &[uniform_index.index()],
191            );
192            downsampling_first_pass.draw(0..3, 0..1);
193        }
194
195        // Other downsample passes
196        for mip in 1..bloom_texture.mip_count {
197            let view = &bloom_texture.view(mip);
198            let mut downsampling_pass =
199                render_context.begin_tracked_render_pass(RenderPassDescriptor {
200                    label: Some("bloom_downsampling_pass"),
201                    color_attachments: &[Some(RenderPassColorAttachment {
202                        view,
203                        resolve_target: None,
204                        ops: Operations::default(),
205                    })],
206                    depth_stencil_attachment: None,
207                    timestamp_writes: None,
208                    occlusion_query_set: None,
209                });
210            downsampling_pass.set_render_pipeline(downsampling_pipeline);
211            downsampling_pass.set_bind_group(
212                0,
213                &bind_groups.downsampling_bind_groups[mip as usize - 1],
214                &[uniform_index.index()],
215            );
216            downsampling_pass.draw(0..3, 0..1);
217        }
218
219        // Upsample passes except the final one
220        for mip in (1..bloom_texture.mip_count).rev() {
221            let view = &bloom_texture.view(mip - 1);
222            let mut upsampling_pass =
223                render_context.begin_tracked_render_pass(RenderPassDescriptor {
224                    label: Some("bloom_upsampling_pass"),
225                    color_attachments: &[Some(RenderPassColorAttachment {
226                        view,
227                        resolve_target: None,
228                        ops: Operations {
229                            load: LoadOp::Load,
230                            store: StoreOp::Store,
231                        },
232                    })],
233                    depth_stencil_attachment: None,
234                    timestamp_writes: None,
235                    occlusion_query_set: None,
236                });
237            upsampling_pass.set_render_pipeline(upsampling_pipeline);
238            upsampling_pass.set_bind_group(
239                0,
240                &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - mip - 1) as usize],
241                &[uniform_index.index()],
242            );
243            let blend = compute_blend_factor(
244                bloom_settings,
245                mip as f32,
246                (bloom_texture.mip_count - 1) as f32,
247            );
248            upsampling_pass.set_blend_constant(LinearRgba::gray(blend));
249            upsampling_pass.draw(0..3, 0..1);
250        }
251
252        // Final upsample pass
253        // This is very similar to the above upsampling passes with the only difference
254        // being the pipeline (which itself is barely different) and the color attachment
255        {
256            let mut upsampling_final_pass =
257                render_context.begin_tracked_render_pass(RenderPassDescriptor {
258                    label: Some("bloom_upsampling_final_pass"),
259                    color_attachments: &[Some(view_target.get_unsampled_color_attachment())],
260                    depth_stencil_attachment: None,
261                    timestamp_writes: None,
262                    occlusion_query_set: None,
263                });
264            upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline);
265            upsampling_final_pass.set_bind_group(
266                0,
267                &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],
268                &[uniform_index.index()],
269            );
270            if let Some(viewport) = camera.viewport.as_ref() {
271                upsampling_final_pass.set_camera_viewport(viewport);
272            }
273            let blend =
274                compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);
275            upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend));
276            upsampling_final_pass.draw(0..3, 0..1);
277        }
278
279        time_span.end(render_context.command_encoder());
280        render_context.command_encoder().pop_debug_group();
281
282        Ok(())
283    }
284}
285
286#[derive(Component)]
287struct BloomTexture {
288    // First mip is half the screen resolution, successive mips are half the previous
289    #[cfg(any(
290        not(feature = "webgl"),
291        not(target_arch = "wasm32"),
292        feature = "webgpu"
293    ))]
294    texture: CachedTexture,
295    // WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead
296    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
297    texture: Vec<CachedTexture>,
298    mip_count: u32,
299}
300
301impl BloomTexture {
302    #[cfg(any(
303        not(feature = "webgl"),
304        not(target_arch = "wasm32"),
305        feature = "webgpu"
306    ))]
307    fn view(&self, base_mip_level: u32) -> TextureView {
308        self.texture.texture.create_view(&TextureViewDescriptor {
309            base_mip_level,
310            mip_level_count: Some(1u32),
311            ..Default::default()
312        })
313    }
314    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
315    fn view(&self, base_mip_level: u32) -> TextureView {
316        self.texture[base_mip_level as usize]
317            .texture
318            .create_view(&TextureViewDescriptor {
319                base_mip_level: 0,
320                mip_level_count: Some(1u32),
321                ..Default::default()
322            })
323    }
324}
325
326fn prepare_bloom_textures(
327    mut commands: Commands,
328    mut texture_cache: ResMut<TextureCache>,
329    render_device: Res<RenderDevice>,
330    views: Query<(Entity, &ExtractedCamera, &Bloom)>,
331) {
332    for (entity, camera, bloom) in &views {
333        if let Some(UVec2 {
334            x: width,
335            y: height,
336        }) = camera.physical_viewport_size
337        {
338            // How many times we can halve the resolution minus one so we don't go unnecessarily low
339            let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;
340            let mip_height_ratio = if height != 0 {
341                bloom.max_mip_dimension as f32 / height as f32
342            } else {
343                0.
344            };
345
346            let texture_descriptor = TextureDescriptor {
347                label: Some("bloom_texture"),
348                size: Extent3d {
349                    width: ((width as f32 * mip_height_ratio).round() as u32).max(1),
350                    height: ((height as f32 * mip_height_ratio).round() as u32).max(1),
351                    depth_or_array_layers: 1,
352                },
353                mip_level_count: mip_count,
354                sample_count: 1,
355                dimension: TextureDimension::D2,
356                format: BLOOM_TEXTURE_FORMAT,
357                usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
358                view_formats: &[],
359            };
360
361            #[cfg(any(
362                not(feature = "webgl"),
363                not(target_arch = "wasm32"),
364                feature = "webgpu"
365            ))]
366            let texture = texture_cache.get(&render_device, texture_descriptor);
367            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
368            let texture: Vec<CachedTexture> = (0..mip_count)
369                .map(|mip| {
370                    texture_cache.get(
371                        &render_device,
372                        TextureDescriptor {
373                            size: Extent3d {
374                                width: (texture_descriptor.size.width >> mip).max(1),
375                                height: (texture_descriptor.size.height >> mip).max(1),
376                                depth_or_array_layers: 1,
377                            },
378                            mip_level_count: 1,
379                            ..texture_descriptor.clone()
380                        },
381                    )
382                })
383                .collect();
384
385            commands
386                .entity(entity)
387                .insert(BloomTexture { texture, mip_count });
388        }
389    }
390}
391
392#[derive(Component)]
393struct BloomBindGroups {
394    downsampling_bind_groups: Box<[BindGroup]>,
395    upsampling_bind_groups: Box<[BindGroup]>,
396    sampler: Sampler,
397}
398
399fn prepare_bloom_bind_groups(
400    mut commands: Commands,
401    render_device: Res<RenderDevice>,
402    downsampling_pipeline: Res<BloomDownsamplingPipeline>,
403    upsampling_pipeline: Res<BloomUpsamplingPipeline>,
404    views: Query<(Entity, &BloomTexture)>,
405    uniforms: Res<ComponentUniforms<BloomUniforms>>,
406) {
407    let sampler = &downsampling_pipeline.sampler;
408
409    for (entity, bloom_texture) in &views {
410        let bind_group_count = bloom_texture.mip_count as usize - 1;
411
412        let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
413        for mip in 1..bloom_texture.mip_count {
414            downsampling_bind_groups.push(render_device.create_bind_group(
415                "bloom_downsampling_bind_group",
416                &downsampling_pipeline.bind_group_layout,
417                &BindGroupEntries::sequential((
418                    &bloom_texture.view(mip - 1),
419                    sampler,
420                    uniforms.binding().unwrap(),
421                )),
422            ));
423        }
424
425        let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
426        for mip in (0..bloom_texture.mip_count).rev() {
427            upsampling_bind_groups.push(render_device.create_bind_group(
428                "bloom_upsampling_bind_group",
429                &upsampling_pipeline.bind_group_layout,
430                &BindGroupEntries::sequential((
431                    &bloom_texture.view(mip),
432                    sampler,
433                    uniforms.binding().unwrap(),
434                )),
435            ));
436        }
437
438        commands.entity(entity).insert(BloomBindGroups {
439            downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
440            upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
441            sampler: sampler.clone(),
442        });
443    }
444}
445
446/// Calculates blend intensities of blur pyramid levels
447/// during the upsampling + compositing stage.
448///
449/// The function assumes all pyramid levels are upsampled and
450/// blended into higher frequency ones using this function to
451/// calculate blend levels every time. The final (highest frequency)
452/// pyramid level in not blended into anything therefore this function
453/// is not applied to it. As a result, the *mip* parameter of 0 indicates
454/// the second-highest frequency pyramid level (in our case that is the
455/// 0th mip of the bloom texture with the original image being the
456/// actual highest frequency level).
457///
458/// Parameters:
459/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).
460/// * `max_mip` - the index of the lowest frequency pyramid level.
461///
462/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
463/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
464fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {
465    let mut lf_boost =
466        (1.0 - ops::powf(
467            1.0 - (mip / max_mip),
468            1.0 / (1.0 - bloom.low_frequency_boost_curvature),
469        )) * bloom.low_frequency_boost;
470    let high_pass_lq = 1.0
471        - (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)
472            .clamp(0.0, 1.0);
473    lf_boost *= match bloom.composite_mode {
474        BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,
475        BloomCompositeMode::Additive => 1.0,
476    };
477
478    (bloom.intensity + lf_boost) * high_pass_lq
479}