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 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 {
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 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 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 {
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 #[cfg(any(
281 not(feature = "webgl"),
282 not(target_arch = "wasm32"),
283 feature = "webgpu"
284 ))]
285 texture: CachedTexture,
286 #[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 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
468fn 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}