bevy_post_process/bloom/
mod.rs

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