1mod downsampling_pipeline;
2mod settings;
3mod upsampling_pipeline;
4
5use bevy_color::{Gray, LinearRgba};
6#[allow(deprecated)]
7pub use settings::{
8 Bloom, BloomCompositeMode, BloomPrefilter, BloomPrefilterSettings, BloomSettings,
9};
10
11use crate::{
12 core_2d::graph::{Core2d, Node2d},
13 core_3d::graph::{Core3d, Node3d},
14};
15use bevy_app::{App, Plugin};
16use bevy_asset::{load_internal_asset, Handle};
17use bevy_ecs::{prelude::*, query::QueryItem};
18use bevy_math::{ops, UVec2};
19use bevy_render::{
20 camera::ExtractedCamera,
21 diagnostic::RecordDiagnostics,
22 extract_component::{
23 ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,
24 },
25 render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
26 render_resource::*,
27 renderer::{RenderContext, RenderDevice},
28 texture::{CachedTexture, TextureCache},
29 view::ViewTarget,
30 Render, RenderApp, RenderSet,
31};
32use downsampling_pipeline::{
33 prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
34 BloomUniforms,
35};
36use upsampling_pipeline::{
37 prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
38};
39
40const BLOOM_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(929599476923908);
41
42const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
43
44pub struct BloomPlugin;
45
46impl Plugin for BloomPlugin {
47 fn build(&self, app: &mut App) {
48 load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl);
49
50 app.register_type::<Bloom>();
51 app.register_type::<BloomPrefilter>();
52 app.register_type::<BloomCompositeMode>();
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 Render,
66 (
67 prepare_downsampling_pipeline.in_set(RenderSet::Prepare),
68 prepare_upsampling_pipeline.in_set(RenderSet::Prepare),
69 prepare_bloom_textures.in_set(RenderSet::PrepareResources),
70 prepare_bloom_bind_groups.in_set(RenderSet::PrepareBindGroups),
71 ),
72 )
73 .add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core3d, Node3d::Bloom)
75 .add_render_graph_edges(
76 Core3d,
77 (Node3d::EndMainPass, Node3d::Bloom, Node3d::Tonemapping),
78 )
79 .add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)
81 .add_render_graph_edges(
82 Core2d,
83 (Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping),
84 );
85 }
86
87 fn finish(&self, app: &mut App) {
88 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
89 return;
90 };
91 render_app
92 .init_resource::<BloomDownsamplingPipeline>()
93 .init_resource::<BloomUpsamplingPipeline>();
94 }
95}
96
97#[derive(Default)]
98struct BloomNode;
99impl ViewNode for BloomNode {
100 type ViewQuery = (
101 &'static ExtractedCamera,
102 &'static ViewTarget,
103 &'static BloomTexture,
104 &'static BloomBindGroups,
105 &'static DynamicUniformIndex<BloomUniforms>,
106 &'static Bloom,
107 &'static UpsamplingPipelineIds,
108 &'static BloomDownsamplingPipelineIds,
109 );
110
111 fn run(
115 &self,
116 _graph: &mut RenderGraphContext,
117 render_context: &mut RenderContext,
118 (
119 camera,
120 view_target,
121 bloom_texture,
122 bind_groups,
123 uniform_index,
124 bloom_settings,
125 upsampling_pipeline_ids,
126 downsampling_pipeline_ids,
127 ): QueryItem<Self::ViewQuery>,
128 world: &World,
129 ) -> Result<(), NodeRunError> {
130 if bloom_settings.intensity == 0.0 {
131 return Ok(());
132 }
133
134 let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();
135 let pipeline_cache = world.resource::<PipelineCache>();
136 let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();
137
138 let (
139 Some(uniforms),
140 Some(downsampling_first_pipeline),
141 Some(downsampling_pipeline),
142 Some(upsampling_pipeline),
143 Some(upsampling_final_pipeline),
144 ) = (
145 uniforms.binding(),
146 pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),
147 pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),
148 pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),
149 pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),
150 )
151 else {
152 return Ok(());
153 };
154
155 render_context.command_encoder().push_debug_group("bloom");
156
157 let diagnostics = render_context.diagnostic_recorder();
158 let time_span = diagnostics.time_span(render_context.command_encoder(), "bloom");
159
160 {
162 let downsampling_first_bind_group = render_context.render_device().create_bind_group(
163 "bloom_downsampling_first_bind_group",
164 &downsampling_pipeline_res.bind_group_layout,
165 &BindGroupEntries::sequential((
166 view_target.main_texture_view(),
168 &bind_groups.sampler,
169 uniforms.clone(),
170 )),
171 );
172
173 let view = &bloom_texture.view(0);
174 let mut downsampling_first_pass =
175 render_context.begin_tracked_render_pass(RenderPassDescriptor {
176 label: Some("bloom_downsampling_first_pass"),
177 color_attachments: &[Some(RenderPassColorAttachment {
178 view,
179 resolve_target: None,
180 ops: Operations::default(),
181 })],
182 depth_stencil_attachment: None,
183 timestamp_writes: None,
184 occlusion_query_set: None,
185 });
186 downsampling_first_pass.set_render_pipeline(downsampling_first_pipeline);
187 downsampling_first_pass.set_bind_group(
188 0,
189 &downsampling_first_bind_group,
190 &[uniform_index.index()],
191 );
192 downsampling_first_pass.draw(0..3, 0..1);
193 }
194
195 for mip in 1..bloom_texture.mip_count {
197 let view = &bloom_texture.view(mip);
198 let mut downsampling_pass =
199 render_context.begin_tracked_render_pass(RenderPassDescriptor {
200 label: Some("bloom_downsampling_pass"),
201 color_attachments: &[Some(RenderPassColorAttachment {
202 view,
203 resolve_target: None,
204 ops: Operations::default(),
205 })],
206 depth_stencil_attachment: None,
207 timestamp_writes: None,
208 occlusion_query_set: None,
209 });
210 downsampling_pass.set_render_pipeline(downsampling_pipeline);
211 downsampling_pass.set_bind_group(
212 0,
213 &bind_groups.downsampling_bind_groups[mip as usize - 1],
214 &[uniform_index.index()],
215 );
216 downsampling_pass.draw(0..3, 0..1);
217 }
218
219 for mip in (1..bloom_texture.mip_count).rev() {
221 let view = &bloom_texture.view(mip - 1);
222 let mut upsampling_pass =
223 render_context.begin_tracked_render_pass(RenderPassDescriptor {
224 label: Some("bloom_upsampling_pass"),
225 color_attachments: &[Some(RenderPassColorAttachment {
226 view,
227 resolve_target: None,
228 ops: Operations {
229 load: LoadOp::Load,
230 store: StoreOp::Store,
231 },
232 })],
233 depth_stencil_attachment: None,
234 timestamp_writes: None,
235 occlusion_query_set: None,
236 });
237 upsampling_pass.set_render_pipeline(upsampling_pipeline);
238 upsampling_pass.set_bind_group(
239 0,
240 &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - mip - 1) as usize],
241 &[uniform_index.index()],
242 );
243 let blend = compute_blend_factor(
244 bloom_settings,
245 mip as f32,
246 (bloom_texture.mip_count - 1) as f32,
247 );
248 upsampling_pass.set_blend_constant(LinearRgba::gray(blend));
249 upsampling_pass.draw(0..3, 0..1);
250 }
251
252 {
256 let mut upsampling_final_pass =
257 render_context.begin_tracked_render_pass(RenderPassDescriptor {
258 label: Some("bloom_upsampling_final_pass"),
259 color_attachments: &[Some(view_target.get_unsampled_color_attachment())],
260 depth_stencil_attachment: None,
261 timestamp_writes: None,
262 occlusion_query_set: None,
263 });
264 upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline);
265 upsampling_final_pass.set_bind_group(
266 0,
267 &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],
268 &[uniform_index.index()],
269 );
270 if let Some(viewport) = camera.viewport.as_ref() {
271 upsampling_final_pass.set_camera_viewport(viewport);
272 }
273 let blend =
274 compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);
275 upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend));
276 upsampling_final_pass.draw(0..3, 0..1);
277 }
278
279 time_span.end(render_context.command_encoder());
280 render_context.command_encoder().pop_debug_group();
281
282 Ok(())
283 }
284}
285
286#[derive(Component)]
287struct BloomTexture {
288 #[cfg(any(
290 not(feature = "webgl"),
291 not(target_arch = "wasm32"),
292 feature = "webgpu"
293 ))]
294 texture: CachedTexture,
295 #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
297 texture: Vec<CachedTexture>,
298 mip_count: u32,
299}
300
301impl BloomTexture {
302 #[cfg(any(
303 not(feature = "webgl"),
304 not(target_arch = "wasm32"),
305 feature = "webgpu"
306 ))]
307 fn view(&self, base_mip_level: u32) -> TextureView {
308 self.texture.texture.create_view(&TextureViewDescriptor {
309 base_mip_level,
310 mip_level_count: Some(1u32),
311 ..Default::default()
312 })
313 }
314 #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
315 fn view(&self, base_mip_level: u32) -> TextureView {
316 self.texture[base_mip_level as usize]
317 .texture
318 .create_view(&TextureViewDescriptor {
319 base_mip_level: 0,
320 mip_level_count: Some(1u32),
321 ..Default::default()
322 })
323 }
324}
325
326fn prepare_bloom_textures(
327 mut commands: Commands,
328 mut texture_cache: ResMut<TextureCache>,
329 render_device: Res<RenderDevice>,
330 views: Query<(Entity, &ExtractedCamera, &Bloom)>,
331) {
332 for (entity, camera, bloom) in &views {
333 if let Some(UVec2 {
334 x: width,
335 y: height,
336 }) = camera.physical_viewport_size
337 {
338 let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;
340 let mip_height_ratio = if height != 0 {
341 bloom.max_mip_dimension as f32 / height as f32
342 } else {
343 0.
344 };
345
346 let texture_descriptor = TextureDescriptor {
347 label: Some("bloom_texture"),
348 size: Extent3d {
349 width: ((width as f32 * mip_height_ratio).round() as u32).max(1),
350 height: ((height as f32 * mip_height_ratio).round() as u32).max(1),
351 depth_or_array_layers: 1,
352 },
353 mip_level_count: mip_count,
354 sample_count: 1,
355 dimension: TextureDimension::D2,
356 format: BLOOM_TEXTURE_FORMAT,
357 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
358 view_formats: &[],
359 };
360
361 #[cfg(any(
362 not(feature = "webgl"),
363 not(target_arch = "wasm32"),
364 feature = "webgpu"
365 ))]
366 let texture = texture_cache.get(&render_device, texture_descriptor);
367 #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
368 let texture: Vec<CachedTexture> = (0..mip_count)
369 .map(|mip| {
370 texture_cache.get(
371 &render_device,
372 TextureDescriptor {
373 size: Extent3d {
374 width: (texture_descriptor.size.width >> mip).max(1),
375 height: (texture_descriptor.size.height >> mip).max(1),
376 depth_or_array_layers: 1,
377 },
378 mip_level_count: 1,
379 ..texture_descriptor.clone()
380 },
381 )
382 })
383 .collect();
384
385 commands
386 .entity(entity)
387 .insert(BloomTexture { texture, mip_count });
388 }
389 }
390}
391
392#[derive(Component)]
393struct BloomBindGroups {
394 downsampling_bind_groups: Box<[BindGroup]>,
395 upsampling_bind_groups: Box<[BindGroup]>,
396 sampler: Sampler,
397}
398
399fn prepare_bloom_bind_groups(
400 mut commands: Commands,
401 render_device: Res<RenderDevice>,
402 downsampling_pipeline: Res<BloomDownsamplingPipeline>,
403 upsampling_pipeline: Res<BloomUpsamplingPipeline>,
404 views: Query<(Entity, &BloomTexture)>,
405 uniforms: Res<ComponentUniforms<BloomUniforms>>,
406) {
407 let sampler = &downsampling_pipeline.sampler;
408
409 for (entity, bloom_texture) in &views {
410 let bind_group_count = bloom_texture.mip_count as usize - 1;
411
412 let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
413 for mip in 1..bloom_texture.mip_count {
414 downsampling_bind_groups.push(render_device.create_bind_group(
415 "bloom_downsampling_bind_group",
416 &downsampling_pipeline.bind_group_layout,
417 &BindGroupEntries::sequential((
418 &bloom_texture.view(mip - 1),
419 sampler,
420 uniforms.binding().unwrap(),
421 )),
422 ));
423 }
424
425 let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
426 for mip in (0..bloom_texture.mip_count).rev() {
427 upsampling_bind_groups.push(render_device.create_bind_group(
428 "bloom_upsampling_bind_group",
429 &upsampling_pipeline.bind_group_layout,
430 &BindGroupEntries::sequential((
431 &bloom_texture.view(mip),
432 sampler,
433 uniforms.binding().unwrap(),
434 )),
435 ));
436 }
437
438 commands.entity(entity).insert(BloomBindGroups {
439 downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
440 upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
441 sampler: sampler.clone(),
442 });
443 }
444}
445
446fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {
465 let mut lf_boost =
466 (1.0 - ops::powf(
467 1.0 - (mip / max_mip),
468 1.0 / (1.0 - bloom.low_frequency_boost_curvature),
469 )) * bloom.low_frequency_boost;
470 let high_pass_lq = 1.0
471 - (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)
472 .clamp(0.0, 1.0);
473 lf_boost *= match bloom.composite_mode {
474 BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,
475 BloomCompositeMode::Additive => 1.0,
476 };
477
478 (bloom.intensity + lf_boost) * high_pass_lq
479}