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(#[automatically_derived]
impl ::core::default::Default for BloomPlugin {
    #[inline]
    fn default() -> Self { Self }
}Default)]
44pub struct BloomPlugin;
45
46impl Plugin for BloomPlugin {
47    fn build(&self, app: &mut App) {
48        {
    {
        let mut embedded =
            app.world_mut().resource_mut::<::bevy_asset::io::embedded::EmbeddedAssetRegistry>();
        let path =
            {
                let crate_name =
                    "bevy_post_process::bloom".split(':').next().unwrap();
                ::bevy_asset::io::embedded::_embedded_asset_path(crate_name,
                    "src".as_ref(), "src/bloom/mod.rs".as_ref(),
                    "bloom.wesl".as_ref())
            };
        let watched_path =
            ::bevy_asset::io::embedded::watched_path("src/bloom/mod.rs",
                "bloom.wesl");
        embedded.insert_asset(watched_path, &path,
            b"//! Bloom works by creating an intermediate texture with a bunch of mip levels, each half the size of the previous.\n//! You then downsample each mip (starting with the original texture) to the lower resolution mip under it, going in order.\n//! You then upsample each mip (starting from the smallest mip) and blend with the higher resolution mip above it (ending on the original texture).\n//!\n//! References:\n//! * [COD] - Next Generation Post Processing in Call of Duty - http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare\n//! * [PBB] - Physically Based Bloom - https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom\n\nstruct BloomUniforms {\n    threshold_precomputations: vec4<f32>,\n    viewport: vec4<f32>,\n    scale: vec2<f32>,\n    aspect: f32,\n};\n\n@group(0) @binding(0) var input_texture: texture_2d<f32>;\n@group(0) @binding(1) var s: sampler;\n\n@group(0) @binding(2) var<uniform> uniforms: BloomUniforms;\n\n@if(FIRST_DOWNSAMPLE)\n/// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4\nfn soft_threshold(color: vec3<f32>) -> vec3<f32> {\n    let brightness = max(color.r, max(color.g, color.b));\n    var softness = brightness - uniforms.threshold_precomputations.y;\n    softness = clamp(softness, 0.0, uniforms.threshold_precomputations.z);\n    softness = softness * softness * uniforms.threshold_precomputations.w;\n    var contribution = max(brightness - uniforms.threshold_precomputations.x, softness);\n    contribution /= max(brightness, 0.00001); // Prevent division by 0\n    return color * contribution;\n}\n\n/// luminance coefficients from Rec. 709.\n/// https://en.wikipedia.org/wiki/Rec._709\nfn tonemapping_luminance(v: vec3<f32>) -> f32 {\n    return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\n/// http://graphicrants.blogspot.com/2013/12/tone-mapping.html\nfn karis_average(color: vec3<f32>) -> f32 {\n    // Luminance calculated based on Rec. 709 color primaries.\n    // This must be done in *linear* color space.\n    let luma = tonemapping_luminance(color) / 4.0;\n    return 1.0 / (1.0 + luma);\n}\n\n/// [COD] slide 153\nfn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {\n    var a: vec3<f32>;\n    var b: vec3<f32>;\n    var c: vec3<f32>;\n    var d: vec3<f32>;\n    var e: vec3<f32>;\n    var f: vec3<f32>;\n    var g: vec3<f32>;\n    var h: vec3<f32>;\n    var i: vec3<f32>;\n    var j: vec3<f32>;\n    var k: vec3<f32>;\n    var l: vec3<f32>;\n    var m: vec3<f32>;\n@if(UNIFORM_SCALE) {\n    // This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be\n    // expressed with constant offsets.\n    //\n    // It\'s possible that this isn\'t meaningfully faster than the \"slow\" path. However, because it\n    // is hard to test performance on all platforms, and uniform bloom is the most common case, this\n    // path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero,\n    // cost to maintainability, but it does help me sleep at night.\n    a = textureSample(input_texture, s, uv, vec2<i32>(-2, 2)).rgb;\n    b = textureSample(input_texture, s, uv, vec2<i32>(0, 2)).rgb;\n    c = textureSample(input_texture, s, uv, vec2<i32>(2, 2)).rgb;\n    d = textureSample(input_texture, s, uv, vec2<i32>(-2, 0)).rgb;\n    e = textureSample(input_texture, s, uv).rgb;\n    f = textureSample(input_texture, s, uv, vec2<i32>(2, 0)).rgb;\n    g = textureSample(input_texture, s, uv, vec2<i32>(-2, -2)).rgb;\n    h = textureSample(input_texture, s, uv, vec2<i32>(0, -2)).rgb;\n    i = textureSample(input_texture, s, uv, vec2<i32>(2, -2)).rgb;\n    j = textureSample(input_texture, s, uv, vec2<i32>(-1, 1)).rgb;\n    k = textureSample(input_texture, s, uv, vec2<i32>(1, 1)).rgb;\n    l = textureSample(input_texture, s, uv, vec2<i32>(-1, -1)).rgb;\n    m = textureSample(input_texture, s, uv, vec2<i32>(1, -1)).rgb;\n} @else {\n    // This is the flexible, but potentially slower, path for non-uniform sampling. Because the\n    // sample is not a constant, and it can fall outside of the limits imposed on constant sample\n    // offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the\n    // texture.\n    //\n    // It isn\'t clear if this is meaningfully slower than using the offset syntax, the spec doesn\'t\n    // mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset\n    // syntax uses a const-expr implies that it allows some compiler optimizations - maybe more\n    // impactful on mobile?\n    let scale = uniforms.scale;\n    let ps = scale / vec2<f32>(textureDimensions(input_texture));\n    let pl = 2.0 * ps;\n    let ns = -1.0 * ps;\n    let nl = -2.0 * ps;\n    a = textureSample(input_texture, s, uv + vec2<f32>(nl.x, pl.y)).rgb;\n    b = textureSample(input_texture, s, uv + vec2<f32>(0.00, pl.y)).rgb;\n    c = textureSample(input_texture, s, uv + vec2<f32>(pl.x, pl.y)).rgb;\n    d = textureSample(input_texture, s, uv + vec2<f32>(nl.x, 0.00)).rgb;\n    e = textureSample(input_texture, s, uv).rgb;\n    f = textureSample(input_texture, s, uv + vec2<f32>(pl.x, 0.00)).rgb;\n    g = textureSample(input_texture, s, uv + vec2<f32>(nl.x, nl.y)).rgb;\n    h = textureSample(input_texture, s, uv + vec2<f32>(0.00, nl.y)).rgb;\n    i = textureSample(input_texture, s, uv + vec2<f32>(pl.x, nl.y)).rgb;\n    j = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ps.y)).rgb;\n    k = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ps.y)).rgb;\n    l = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ns.y)).rgb;\n    m = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ns.y)).rgb;\n}\n\n@if(FIRST_DOWNSAMPLE) {\n    // [COD] slide 168\n    //\n    // The first downsample pass reads from the rendered frame which may exhibit\n    // \'fireflies\' (individual very bright pixels) that should not cause the bloom effect.\n    //\n    // The first downsample uses a firefly-reduction method proposed by Brian Karis\n    // which takes a weighted-average of the samples to limit their luma range to [0, 1].\n    // This implementation matches the LearnOpenGL article [PBB].\n    var group0 = (a + b + d + e) * (0.125f / 4.0f);\n    var group1 = (b + c + e + f) * (0.125f / 4.0f);\n    var group2 = (d + e + g + h) * (0.125f / 4.0f);\n    var group3 = (e + f + h + i) * (0.125f / 4.0f);\n    var group4 = (j + k + l + m) * (0.5f / 4.0f);\n    group0 *= karis_average(group0);\n    group1 *= karis_average(group1);\n    group2 *= karis_average(group2);\n    group3 *= karis_average(group3);\n    group4 *= karis_average(group4);\n    return group0 + group1 + group2 + group3 + group4;\n} @else {\n    var sample = (a + c + g + i) * 0.03125;\n    sample += (b + d + f + h) * 0.0625;\n    sample += (e + j + k + l + m) * 0.125;\n    return sample;\n}\n}\n\n/// [COD] slide 162\nfn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {\n    // While this is probably technically incorrect, it makes nonuniform bloom smoother, without\n    // having any impact on uniform bloom, which simply evaluates to 1.0 here.\n    let frag_size = uniforms.scale / vec2<f32>(textureDimensions(input_texture));\n    let x = frag_size.x;\n    let y = frag_size.y;\n\n    let a = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y + y)).rgb;\n    let b = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y + y)).rgb;\n    let c = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y + y)).rgb;\n\n    let d = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y)).rgb;\n    let e = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y)).rgb;\n    let f = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y)).rgb;\n\n    let g = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y - y)).rgb;\n    let h = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y - y)).rgb;\n    let i = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y - y)).rgb;\n\n    var sample = e * 0.25;\n    sample += (b + d + f + h) * 0.125;\n    sample += (a + c + g + i) * 0.0625;\n\n    return sample;\n}\n\n@if(FIRST_DOWNSAMPLE)\n@fragment\nfn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32> {\n    let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw;\n    var sample = sample_input_13_tap(sample_uv);\n    // Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the\n    // downscaling and upscaling which would result in black boxes.\n    // The upper bound is to prevent NaNs.\n    // with f32::MAX (E+38) Chrome fails with \":value 340282346999999984391321947108527833088.0 cannot be represented as \'f32\'\"\n    sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+37));\n\n@if(USE_THRESHOLD)\n    sample = soft_threshold(sample);\n\n    return vec4<f32>(sample, 1.0);\n}\n\n@fragment\nfn downsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n    return vec4<f32>(sample_input_13_tap(uv), 1.0);\n}\n\n@fragment\nfn upsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n    return vec4<f32>(sample_input_3x3_tent(uv), 1.0);\n}\n");
    }
};embedded_asset!(app, "bloom.wesl");
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(impl bevy_ecs::component::Component for BloomTexture where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}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(impl bevy_ecs::component::Component for BloomBindGroups where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}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}