Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_post_process/src/bloom/mod.rs
6596 views
1
mod downsampling_pipeline;
2
mod settings;
3
mod upsampling_pipeline;
4
5
use bevy_image::ToExtents;
6
pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter};
7
8
use crate::bloom::{
9
downsampling_pipeline::init_bloom_downsampling_pipeline,
10
upsampling_pipeline::init_bloom_upscaling_pipeline,
11
};
12
use bevy_app::{App, Plugin};
13
use bevy_asset::embedded_asset;
14
use bevy_color::{Gray, LinearRgba};
15
use bevy_core_pipeline::{
16
core_2d::graph::{Core2d, Node2d},
17
core_3d::graph::{Core3d, Node3d},
18
};
19
use bevy_ecs::{prelude::*, query::QueryItem};
20
use bevy_math::{ops, UVec2};
21
use 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
};
34
use downsampling_pipeline::{
35
prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
36
BloomUniforms,
37
};
38
#[cfg(feature = "trace")]
39
use tracing::info_span;
40
use upsampling_pipeline::{
41
prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
42
};
43
44
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
45
46
#[derive(Default)]
47
pub struct BloomPlugin;
48
49
impl 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 bloom to the 3d render graph
81
.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core3d, Node3d::Bloom)
82
.add_render_graph_edges(
83
Core3d,
84
(Node3d::EndMainPass, Node3d::Bloom, Node3d::Tonemapping),
85
)
86
// Add bloom to the 2d render graph
87
.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)
88
.add_render_graph_edges(
89
Core2d,
90
(Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping),
91
);
92
}
93
}
94
95
#[derive(Default)]
96
struct BloomNode;
97
impl ViewNode for BloomNode {
98
type ViewQuery = (
99
&'static ExtractedCamera,
100
&'static ViewTarget,
101
&'static BloomTexture,
102
&'static BloomBindGroups,
103
&'static DynamicUniformIndex<BloomUniforms>,
104
&'static Bloom,
105
&'static UpsamplingPipelineIds,
106
&'static BloomDownsamplingPipelineIds,
107
);
108
109
// Atypically for a post-processing effect, we do not need to
110
// use a secondary texture normally provided by view_target.post_process_write(),
111
// instead we write into our own bloom texture and then directly back onto main.
112
fn run<'w>(
113
&self,
114
_graph: &mut RenderGraphContext,
115
render_context: &mut RenderContext<'w>,
116
(
117
camera,
118
view_target,
119
bloom_texture,
120
bind_groups,
121
uniform_index,
122
bloom_settings,
123
upsampling_pipeline_ids,
124
downsampling_pipeline_ids,
125
): QueryItem<'w, '_, Self::ViewQuery>,
126
world: &'w World,
127
) -> Result<(), NodeRunError> {
128
if bloom_settings.intensity == 0.0 {
129
return Ok(());
130
}
131
132
let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();
133
let pipeline_cache = world.resource::<PipelineCache>();
134
let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();
135
136
let (
137
Some(uniforms),
138
Some(downsampling_first_pipeline),
139
Some(downsampling_pipeline),
140
Some(upsampling_pipeline),
141
Some(upsampling_final_pipeline),
142
) = (
143
uniforms.binding(),
144
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),
145
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),
146
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),
147
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),
148
)
149
else {
150
return Ok(());
151
};
152
153
let view_texture = view_target.main_texture_view();
154
let view_texture_unsampled = view_target.get_unsampled_color_attachment();
155
let diagnostics = render_context.diagnostic_recorder();
156
157
render_context.add_command_buffer_generation_task(move |render_device| {
158
#[cfg(feature = "trace")]
159
let _bloom_span = info_span!("bloom").entered();
160
161
let mut command_encoder =
162
render_device.create_command_encoder(&CommandEncoderDescriptor {
163
label: Some("bloom_command_encoder"),
164
});
165
command_encoder.push_debug_group("bloom");
166
let time_span = diagnostics.time_span(&mut command_encoder, "bloom");
167
168
// First downsample pass
169
{
170
let downsampling_first_bind_group = render_device.create_bind_group(
171
"bloom_downsampling_first_bind_group",
172
&downsampling_pipeline_res.bind_group_layout,
173
&BindGroupEntries::sequential((
174
// Read from main texture directly
175
view_texture,
176
&bind_groups.sampler,
177
uniforms.clone(),
178
)),
179
);
180
181
let view = &bloom_texture.view(0);
182
let mut downsampling_first_pass =
183
command_encoder.begin_render_pass(&RenderPassDescriptor {
184
label: Some("bloom_downsampling_first_pass"),
185
color_attachments: &[Some(RenderPassColorAttachment {
186
view,
187
depth_slice: None,
188
resolve_target: None,
189
ops: Operations::default(),
190
})],
191
depth_stencil_attachment: None,
192
timestamp_writes: None,
193
occlusion_query_set: None,
194
});
195
downsampling_first_pass.set_pipeline(downsampling_first_pipeline);
196
downsampling_first_pass.set_bind_group(
197
0,
198
&downsampling_first_bind_group,
199
&[uniform_index.index()],
200
);
201
downsampling_first_pass.draw(0..3, 0..1);
202
}
203
204
// Other downsample passes
205
for mip in 1..bloom_texture.mip_count {
206
let view = &bloom_texture.view(mip);
207
let mut downsampling_pass =
208
command_encoder.begin_render_pass(&RenderPassDescriptor {
209
label: Some("bloom_downsampling_pass"),
210
color_attachments: &[Some(RenderPassColorAttachment {
211
view,
212
depth_slice: None,
213
resolve_target: None,
214
ops: Operations::default(),
215
})],
216
depth_stencil_attachment: None,
217
timestamp_writes: None,
218
occlusion_query_set: None,
219
});
220
downsampling_pass.set_pipeline(downsampling_pipeline);
221
downsampling_pass.set_bind_group(
222
0,
223
&bind_groups.downsampling_bind_groups[mip as usize - 1],
224
&[uniform_index.index()],
225
);
226
downsampling_pass.draw(0..3, 0..1);
227
}
228
229
// Upsample passes except the final one
230
for mip in (1..bloom_texture.mip_count).rev() {
231
let view = &bloom_texture.view(mip - 1);
232
let mut upsampling_pass =
233
command_encoder.begin_render_pass(&RenderPassDescriptor {
234
label: Some("bloom_upsampling_pass"),
235
color_attachments: &[Some(RenderPassColorAttachment {
236
view,
237
depth_slice: None,
238
resolve_target: None,
239
ops: Operations {
240
load: LoadOp::Load,
241
store: StoreOp::Store,
242
},
243
})],
244
depth_stencil_attachment: None,
245
timestamp_writes: None,
246
occlusion_query_set: None,
247
});
248
upsampling_pass.set_pipeline(upsampling_pipeline);
249
upsampling_pass.set_bind_group(
250
0,
251
&bind_groups.upsampling_bind_groups
252
[(bloom_texture.mip_count - mip - 1) as usize],
253
&[uniform_index.index()],
254
);
255
let blend = compute_blend_factor(
256
bloom_settings,
257
mip as f32,
258
(bloom_texture.mip_count - 1) as f32,
259
);
260
upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into());
261
upsampling_pass.draw(0..3, 0..1);
262
}
263
264
// Final upsample pass
265
// This is very similar to the above upsampling passes with the only difference
266
// being the pipeline (which itself is barely different) and the color attachment
267
{
268
let mut upsampling_final_pass =
269
command_encoder.begin_render_pass(&RenderPassDescriptor {
270
label: Some("bloom_upsampling_final_pass"),
271
color_attachments: &[Some(view_texture_unsampled)],
272
depth_stencil_attachment: None,
273
timestamp_writes: None,
274
occlusion_query_set: None,
275
});
276
upsampling_final_pass.set_pipeline(upsampling_final_pipeline);
277
upsampling_final_pass.set_bind_group(
278
0,
279
&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],
280
&[uniform_index.index()],
281
);
282
if let Some(viewport) = camera.viewport.as_ref() {
283
upsampling_final_pass.set_viewport(
284
viewport.physical_position.x as f32,
285
viewport.physical_position.y as f32,
286
viewport.physical_size.x as f32,
287
viewport.physical_size.y as f32,
288
viewport.depth.start,
289
viewport.depth.end,
290
);
291
}
292
let blend =
293
compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);
294
upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into());
295
upsampling_final_pass.draw(0..3, 0..1);
296
}
297
298
time_span.end(&mut command_encoder);
299
command_encoder.pop_debug_group();
300
command_encoder.finish()
301
});
302
303
Ok(())
304
}
305
}
306
307
#[derive(Component)]
308
struct BloomTexture {
309
// First mip is half the screen resolution, successive mips are half the previous
310
#[cfg(any(
311
not(feature = "webgl"),
312
not(target_arch = "wasm32"),
313
feature = "webgpu"
314
))]
315
texture: CachedTexture,
316
// WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead
317
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
318
texture: Vec<CachedTexture>,
319
mip_count: u32,
320
}
321
322
impl BloomTexture {
323
#[cfg(any(
324
not(feature = "webgl"),
325
not(target_arch = "wasm32"),
326
feature = "webgpu"
327
))]
328
fn view(&self, base_mip_level: u32) -> TextureView {
329
self.texture.texture.create_view(&TextureViewDescriptor {
330
base_mip_level,
331
mip_level_count: Some(1u32),
332
..Default::default()
333
})
334
}
335
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
336
fn view(&self, base_mip_level: u32) -> TextureView {
337
self.texture[base_mip_level as usize]
338
.texture
339
.create_view(&TextureViewDescriptor {
340
base_mip_level: 0,
341
mip_level_count: Some(1u32),
342
..Default::default()
343
})
344
}
345
}
346
347
fn prepare_bloom_textures(
348
mut commands: Commands,
349
mut texture_cache: ResMut<TextureCache>,
350
render_device: Res<RenderDevice>,
351
views: Query<(Entity, &ExtractedCamera, &Bloom)>,
352
) {
353
for (entity, camera, bloom) in &views {
354
if let Some(viewport) = camera.physical_viewport_size {
355
// How many times we can halve the resolution minus one so we don't go unnecessarily low
356
let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;
357
let mip_height_ratio = if viewport.y != 0 {
358
bloom.max_mip_dimension as f32 / viewport.y as f32
359
} else {
360
0.
361
};
362
363
let texture_descriptor = TextureDescriptor {
364
label: Some("bloom_texture"),
365
size: (viewport.as_vec2() * mip_height_ratio)
366
.round()
367
.as_uvec2()
368
.max(UVec2::ONE)
369
.to_extents(),
370
mip_level_count: mip_count,
371
sample_count: 1,
372
dimension: TextureDimension::D2,
373
format: BLOOM_TEXTURE_FORMAT,
374
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
375
view_formats: &[],
376
};
377
378
#[cfg(any(
379
not(feature = "webgl"),
380
not(target_arch = "wasm32"),
381
feature = "webgpu"
382
))]
383
let texture = texture_cache.get(&render_device, texture_descriptor);
384
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
385
let texture: Vec<CachedTexture> = (0..mip_count)
386
.map(|mip| {
387
texture_cache.get(
388
&render_device,
389
TextureDescriptor {
390
size: Extent3d {
391
width: (texture_descriptor.size.width >> mip).max(1),
392
height: (texture_descriptor.size.height >> mip).max(1),
393
depth_or_array_layers: 1,
394
},
395
mip_level_count: 1,
396
..texture_descriptor.clone()
397
},
398
)
399
})
400
.collect();
401
402
commands
403
.entity(entity)
404
.insert(BloomTexture { texture, mip_count });
405
}
406
}
407
}
408
409
#[derive(Component)]
410
struct BloomBindGroups {
411
downsampling_bind_groups: Box<[BindGroup]>,
412
upsampling_bind_groups: Box<[BindGroup]>,
413
sampler: Sampler,
414
}
415
416
fn prepare_bloom_bind_groups(
417
mut commands: Commands,
418
render_device: Res<RenderDevice>,
419
downsampling_pipeline: Res<BloomDownsamplingPipeline>,
420
upsampling_pipeline: Res<BloomUpsamplingPipeline>,
421
views: Query<(Entity, &BloomTexture)>,
422
uniforms: Res<ComponentUniforms<BloomUniforms>>,
423
) {
424
let sampler = &downsampling_pipeline.sampler;
425
426
for (entity, bloom_texture) in &views {
427
let bind_group_count = bloom_texture.mip_count as usize - 1;
428
429
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
430
for mip in 1..bloom_texture.mip_count {
431
downsampling_bind_groups.push(render_device.create_bind_group(
432
"bloom_downsampling_bind_group",
433
&downsampling_pipeline.bind_group_layout,
434
&BindGroupEntries::sequential((
435
&bloom_texture.view(mip - 1),
436
sampler,
437
uniforms.binding().unwrap(),
438
)),
439
));
440
}
441
442
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
443
for mip in (0..bloom_texture.mip_count).rev() {
444
upsampling_bind_groups.push(render_device.create_bind_group(
445
"bloom_upsampling_bind_group",
446
&upsampling_pipeline.bind_group_layout,
447
&BindGroupEntries::sequential((
448
&bloom_texture.view(mip),
449
sampler,
450
uniforms.binding().unwrap(),
451
)),
452
));
453
}
454
455
commands.entity(entity).insert(BloomBindGroups {
456
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
457
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
458
sampler: sampler.clone(),
459
});
460
}
461
}
462
463
/// Calculates blend intensities of blur pyramid levels
464
/// during the upsampling + compositing stage.
465
///
466
/// The function assumes all pyramid levels are upsampled and
467
/// blended into higher frequency ones using this function to
468
/// calculate blend levels every time. The final (highest frequency)
469
/// pyramid level in not blended into anything therefore this function
470
/// is not applied to it. As a result, the *mip* parameter of 0 indicates
471
/// the second-highest frequency pyramid level (in our case that is the
472
/// 0th mip of the bloom texture with the original image being the
473
/// actual highest frequency level).
474
///
475
/// Parameters:
476
/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).
477
/// * `max_mip` - the index of the lowest frequency pyramid level.
478
///
479
/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
480
/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
481
fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {
482
let mut lf_boost =
483
(1.0 - ops::powf(
484
1.0 - (mip / max_mip),
485
1.0 / (1.0 - bloom.low_frequency_boost_curvature),
486
)) * bloom.low_frequency_boost;
487
let high_pass_lq = 1.0
488
- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)
489
.clamp(0.0, 1.0);
490
lf_boost *= match bloom.composite_mode {
491
BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,
492
BloomCompositeMode::Additive => 1.0,
493
};
494
495
(bloom.intensity + lf_boost) * high_pass_lq
496
}
497
498