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_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_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 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 {
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 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 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 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 {
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 #[cfg(any(
320 not(feature = "webgl"),
321 not(target_arch = "wasm32"),
322 feature = "webgpu"
323 ))]
324 texture: CachedTexture,
325 #[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 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
488fn 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}