Skip to main content

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