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
9366 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
schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems},
17
tonemapping::tonemapping,
18
};
19
use bevy_ecs::prelude::*;
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_resource::*,
28
renderer::{RenderContext, RenderDevice, ViewQuery},
29
texture::{CachedTexture, TextureCache},
30
view::ViewTarget,
31
Render, RenderApp, RenderStartup, RenderSystems,
32
};
33
use downsampling_pipeline::{
34
prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
35
BloomUniforms,
36
};
37
use upsampling_pipeline::{
38
prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
39
};
40
41
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
42
43
#[derive(Default)]
44
pub struct BloomPlugin;
45
46
impl 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_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
60
.init_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
88
pub 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 {
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(Component)]
278
pub 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
292
impl 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
317
fn 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(Component)]
380
pub struct BloomBindGroups {
381
cache_key: (TextureId, BufferId),
382
downsampling_bind_groups: Box<[BindGroup]>,
383
upsampling_bind_groups: Box<[BindGroup]>,
384
sampler: Sampler,
385
}
386
387
fn prepare_bloom_bind_groups(
388
mut commands: Commands,
389
render_device: Res<RenderDevice>,
390
downsampling_pipeline: Res<BloomDownsamplingPipeline>,
391
upsampling_pipeline: Res<BloomUpsamplingPipeline>,
392
views: Query<(Entity, &BloomTexture, Option<&BloomBindGroups>)>,
393
uniforms: Res<ComponentUniforms<BloomUniforms>>,
394
pipeline_cache: Res<PipelineCache>,
395
) {
396
let sampler = &downsampling_pipeline.sampler;
397
398
for (entity, bloom_texture, bloom_bind_groups) in &views {
399
if let Some(b) = bloom_bind_groups
400
&& b.cache_key
401
== (
402
bloom_texture.texture.texture.id(),
403
uniforms.buffer().unwrap().id(),
404
)
405
{
406
continue;
407
}
408
409
let bind_group_count = bloom_texture.mip_count as usize - 1;
410
411
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
412
for mip in 1..bloom_texture.mip_count {
413
downsampling_bind_groups.push(render_device.create_bind_group(
414
"bloom_downsampling_bind_group",
415
&pipeline_cache.get_bind_group_layout(&downsampling_pipeline.bind_group_layout),
416
&BindGroupEntries::sequential((
417
&bloom_texture.view(mip - 1),
418
sampler,
419
uniforms.binding().unwrap(),
420
)),
421
));
422
}
423
424
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
425
for mip in (0..bloom_texture.mip_count).rev() {
426
upsampling_bind_groups.push(render_device.create_bind_group(
427
"bloom_upsampling_bind_group",
428
&pipeline_cache.get_bind_group_layout(&upsampling_pipeline.bind_group_layout),
429
&BindGroupEntries::sequential((
430
&bloom_texture.view(mip),
431
sampler,
432
uniforms.binding().unwrap(),
433
)),
434
));
435
}
436
437
commands.entity(entity).insert(BloomBindGroups {
438
cache_key: (
439
bloom_texture.texture.texture.id(),
440
uniforms.buffer().unwrap().id(),
441
),
442
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
443
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
444
sampler: sampler.clone(),
445
});
446
}
447
}
448
449
/// Calculates blend intensities of blur pyramid levels
450
/// during the upsampling + compositing stage.
451
///
452
/// The function assumes all pyramid levels are upsampled and
453
/// blended into higher frequency ones using this function to
454
/// calculate blend levels every time. The final (highest frequency)
455
/// pyramid level in not blended into anything therefore this function
456
/// is not applied to it. As a result, the *mip* parameter of 0 indicates
457
/// the second-highest frequency pyramid level (in our case that is the
458
/// 0th mip of the bloom texture with the original image being the
459
/// actual highest frequency level).
460
///
461
/// Parameters:
462
/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).
463
/// * `max_mip` - the index of the lowest frequency pyramid level.
464
///
465
/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
466
/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
467
fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {
468
let mut lf_boost =
469
(1.0 - ops::powf(
470
1.0 - (mip / max_mip),
471
1.0 / (1.0 - bloom.low_frequency_boost_curvature),
472
)) * bloom.low_frequency_boost;
473
let high_pass_lq = 1.0
474
- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)
475
.clamp(0.0, 1.0);
476
lf_boost *= match bloom.composite_mode {
477
BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,
478
BloomCompositeMode::Additive => 1.0,
479
};
480
481
(bloom.intensity + lf_boost) * high_pass_lq
482
}
483
484