Path: blob/main/crates/bevy_post_process/src/bloom/mod.rs
6849 views
mod downsampling_pipeline;1mod settings;2mod upsampling_pipeline;34use bevy_image::ToExtents;5pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter};67use crate::bloom::{8downsampling_pipeline::init_bloom_downsampling_pipeline,9upsampling_pipeline::init_bloom_upscaling_pipeline,10};11use bevy_app::{App, Plugin};12use bevy_asset::embedded_asset;13use bevy_color::{Gray, LinearRgba};14use bevy_core_pipeline::{15core_2d::graph::{Core2d, Node2d},16core_3d::graph::{Core3d, Node3d},17};18use bevy_ecs::{prelude::*, query::QueryItem};19use bevy_math::{ops, UVec2};20use bevy_render::{21camera::ExtractedCamera,22diagnostic::RecordDiagnostics,23extract_component::{24ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,25},26render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},27render_resource::*,28renderer::{RenderContext, RenderDevice},29texture::{CachedTexture, TextureCache},30view::ViewTarget,31Render, RenderApp, RenderStartup, RenderSystems,32};33use downsampling_pipeline::{34prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,35BloomUniforms,36};37#[cfg(feature = "trace")]38use tracing::info_span;39use upsampling_pipeline::{40prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,41};4243const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;4445#[derive(Default)]46pub struct BloomPlugin;4748impl Plugin for BloomPlugin {49fn build(&self, app: &mut App) {50embedded_asset!(app, "bloom.wgsl");5152app.add_plugins((53ExtractComponentPlugin::<Bloom>::default(),54UniformComponentPlugin::<BloomUniforms>::default(),55));5657let Some(render_app) = app.get_sub_app_mut(RenderApp) else {58return;59};60render_app61.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()62.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()63.add_systems(64RenderStartup,65(66init_bloom_downsampling_pipeline,67init_bloom_upscaling_pipeline,68),69)70.add_systems(71Render,72(73prepare_downsampling_pipeline.in_set(RenderSystems::Prepare),74prepare_upsampling_pipeline.in_set(RenderSystems::Prepare),75prepare_bloom_textures.in_set(RenderSystems::PrepareResources),76prepare_bloom_bind_groups.in_set(RenderSystems::PrepareBindGroups),77),78)79// Add bloom to the 3d render graph80.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core3d, Node3d::Bloom)81.add_render_graph_edges(82Core3d,83(84Node3d::StartMainPassPostProcessing,85Node3d::Bloom,86Node3d::Tonemapping,87),88)89// Add bloom to the 2d render graph90.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)91.add_render_graph_edges(92Core2d,93(94Node2d::StartMainPassPostProcessing,95Node2d::Bloom,96Node2d::Tonemapping,97),98);99}100}101102#[derive(Default)]103struct BloomNode;104impl ViewNode for BloomNode {105type ViewQuery = (106&'static ExtractedCamera,107&'static ViewTarget,108&'static BloomTexture,109&'static BloomBindGroups,110&'static DynamicUniformIndex<BloomUniforms>,111&'static Bloom,112&'static UpsamplingPipelineIds,113&'static BloomDownsamplingPipelineIds,114);115116// Atypically for a post-processing effect, we do not need to117// use a secondary texture normally provided by view_target.post_process_write(),118// instead we write into our own bloom texture and then directly back onto main.119fn run<'w>(120&self,121_graph: &mut RenderGraphContext,122render_context: &mut RenderContext<'w>,123(124camera,125view_target,126bloom_texture,127bind_groups,128uniform_index,129bloom_settings,130upsampling_pipeline_ids,131downsampling_pipeline_ids,132): QueryItem<'w, '_, Self::ViewQuery>,133world: &'w World,134) -> Result<(), NodeRunError> {135if bloom_settings.intensity == 0.0 {136return Ok(());137}138139let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();140let pipeline_cache = world.resource::<PipelineCache>();141let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();142143let (144Some(uniforms),145Some(downsampling_first_pipeline),146Some(downsampling_pipeline),147Some(upsampling_pipeline),148Some(upsampling_final_pipeline),149) = (150uniforms.binding(),151pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),152pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),153pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),154pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),155)156else {157return Ok(());158};159160let view_texture = view_target.main_texture_view();161let view_texture_unsampled = view_target.get_unsampled_color_attachment();162let diagnostics = render_context.diagnostic_recorder();163164render_context.add_command_buffer_generation_task(move |render_device| {165#[cfg(feature = "trace")]166let _bloom_span = info_span!("bloom").entered();167168let mut command_encoder =169render_device.create_command_encoder(&CommandEncoderDescriptor {170label: Some("bloom_command_encoder"),171});172command_encoder.push_debug_group("bloom");173let time_span = diagnostics.time_span(&mut command_encoder, "bloom");174175// First downsample pass176{177let downsampling_first_bind_group = render_device.create_bind_group(178"bloom_downsampling_first_bind_group",179&downsampling_pipeline_res.bind_group_layout,180&BindGroupEntries::sequential((181// Read from main texture directly182view_texture,183&bind_groups.sampler,184uniforms.clone(),185)),186);187188let view = &bloom_texture.view(0);189let mut downsampling_first_pass =190command_encoder.begin_render_pass(&RenderPassDescriptor {191label: Some("bloom_downsampling_first_pass"),192color_attachments: &[Some(RenderPassColorAttachment {193view,194depth_slice: None,195resolve_target: None,196ops: Operations::default(),197})],198depth_stencil_attachment: None,199timestamp_writes: None,200occlusion_query_set: None,201});202downsampling_first_pass.set_pipeline(downsampling_first_pipeline);203downsampling_first_pass.set_bind_group(2040,205&downsampling_first_bind_group,206&[uniform_index.index()],207);208downsampling_first_pass.draw(0..3, 0..1);209}210211// Other downsample passes212for mip in 1..bloom_texture.mip_count {213let view = &bloom_texture.view(mip);214let mut downsampling_pass =215command_encoder.begin_render_pass(&RenderPassDescriptor {216label: Some("bloom_downsampling_pass"),217color_attachments: &[Some(RenderPassColorAttachment {218view,219depth_slice: None,220resolve_target: None,221ops: Operations::default(),222})],223depth_stencil_attachment: None,224timestamp_writes: None,225occlusion_query_set: None,226});227downsampling_pass.set_pipeline(downsampling_pipeline);228downsampling_pass.set_bind_group(2290,230&bind_groups.downsampling_bind_groups[mip as usize - 1],231&[uniform_index.index()],232);233downsampling_pass.draw(0..3, 0..1);234}235236// Upsample passes except the final one237for mip in (1..bloom_texture.mip_count).rev() {238let view = &bloom_texture.view(mip - 1);239let mut upsampling_pass =240command_encoder.begin_render_pass(&RenderPassDescriptor {241label: Some("bloom_upsampling_pass"),242color_attachments: &[Some(RenderPassColorAttachment {243view,244depth_slice: None,245resolve_target: None,246ops: Operations {247load: LoadOp::Load,248store: StoreOp::Store,249},250})],251depth_stencil_attachment: None,252timestamp_writes: None,253occlusion_query_set: None,254});255upsampling_pass.set_pipeline(upsampling_pipeline);256upsampling_pass.set_bind_group(2570,258&bind_groups.upsampling_bind_groups259[(bloom_texture.mip_count - mip - 1) as usize],260&[uniform_index.index()],261);262let blend = compute_blend_factor(263bloom_settings,264mip as f32,265(bloom_texture.mip_count - 1) as f32,266);267upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into());268upsampling_pass.draw(0..3, 0..1);269}270271// Final upsample pass272// This is very similar to the above upsampling passes with the only difference273// being the pipeline (which itself is barely different) and the color attachment274{275let mut upsampling_final_pass =276command_encoder.begin_render_pass(&RenderPassDescriptor {277label: Some("bloom_upsampling_final_pass"),278color_attachments: &[Some(view_texture_unsampled)],279depth_stencil_attachment: None,280timestamp_writes: None,281occlusion_query_set: None,282});283upsampling_final_pass.set_pipeline(upsampling_final_pipeline);284upsampling_final_pass.set_bind_group(2850,286&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],287&[uniform_index.index()],288);289if let Some(viewport) = camera.viewport.as_ref() {290upsampling_final_pass.set_viewport(291viewport.physical_position.x as f32,292viewport.physical_position.y as f32,293viewport.physical_size.x as f32,294viewport.physical_size.y as f32,295viewport.depth.start,296viewport.depth.end,297);298}299let blend =300compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);301upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into());302upsampling_final_pass.draw(0..3, 0..1);303}304305time_span.end(&mut command_encoder);306command_encoder.pop_debug_group();307command_encoder.finish()308});309310Ok(())311}312}313314#[derive(Component)]315struct BloomTexture {316// First mip is half the screen resolution, successive mips are half the previous317#[cfg(any(318not(feature = "webgl"),319not(target_arch = "wasm32"),320feature = "webgpu"321))]322texture: CachedTexture,323// WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead324#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]325texture: Vec<CachedTexture>,326mip_count: u32,327}328329impl BloomTexture {330#[cfg(any(331not(feature = "webgl"),332not(target_arch = "wasm32"),333feature = "webgpu"334))]335fn view(&self, base_mip_level: u32) -> TextureView {336self.texture.texture.create_view(&TextureViewDescriptor {337base_mip_level,338mip_level_count: Some(1u32),339..Default::default()340})341}342#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]343fn view(&self, base_mip_level: u32) -> TextureView {344self.texture[base_mip_level as usize]345.texture346.create_view(&TextureViewDescriptor {347base_mip_level: 0,348mip_level_count: Some(1u32),349..Default::default()350})351}352}353354fn prepare_bloom_textures(355mut commands: Commands,356mut texture_cache: ResMut<TextureCache>,357render_device: Res<RenderDevice>,358views: Query<(Entity, &ExtractedCamera, &Bloom)>,359) {360for (entity, camera, bloom) in &views {361if let Some(viewport) = camera.physical_viewport_size {362// How many times we can halve the resolution minus one so we don't go unnecessarily low363let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;364let mip_height_ratio = if viewport.y != 0 {365bloom.max_mip_dimension as f32 / viewport.y as f32366} else {3670.368};369370let texture_descriptor = TextureDescriptor {371label: Some("bloom_texture"),372size: (viewport.as_vec2() * mip_height_ratio)373.round()374.as_uvec2()375.max(UVec2::ONE)376.to_extents(),377mip_level_count: mip_count,378sample_count: 1,379dimension: TextureDimension::D2,380format: BLOOM_TEXTURE_FORMAT,381usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,382view_formats: &[],383};384385#[cfg(any(386not(feature = "webgl"),387not(target_arch = "wasm32"),388feature = "webgpu"389))]390let texture = texture_cache.get(&render_device, texture_descriptor);391#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]392let texture: Vec<CachedTexture> = (0..mip_count)393.map(|mip| {394texture_cache.get(395&render_device,396TextureDescriptor {397size: Extent3d {398width: (texture_descriptor.size.width >> mip).max(1),399height: (texture_descriptor.size.height >> mip).max(1),400depth_or_array_layers: 1,401},402mip_level_count: 1,403..texture_descriptor.clone()404},405)406})407.collect();408409commands410.entity(entity)411.insert(BloomTexture { texture, mip_count });412}413}414}415416#[derive(Component)]417struct BloomBindGroups {418downsampling_bind_groups: Box<[BindGroup]>,419upsampling_bind_groups: Box<[BindGroup]>,420sampler: Sampler,421}422423fn prepare_bloom_bind_groups(424mut commands: Commands,425render_device: Res<RenderDevice>,426downsampling_pipeline: Res<BloomDownsamplingPipeline>,427upsampling_pipeline: Res<BloomUpsamplingPipeline>,428views: Query<(Entity, &BloomTexture)>,429uniforms: Res<ComponentUniforms<BloomUniforms>>,430) {431let sampler = &downsampling_pipeline.sampler;432433for (entity, bloom_texture) in &views {434let bind_group_count = bloom_texture.mip_count as usize - 1;435436let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);437for mip in 1..bloom_texture.mip_count {438downsampling_bind_groups.push(render_device.create_bind_group(439"bloom_downsampling_bind_group",440&downsampling_pipeline.bind_group_layout,441&BindGroupEntries::sequential((442&bloom_texture.view(mip - 1),443sampler,444uniforms.binding().unwrap(),445)),446));447}448449let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);450for mip in (0..bloom_texture.mip_count).rev() {451upsampling_bind_groups.push(render_device.create_bind_group(452"bloom_upsampling_bind_group",453&upsampling_pipeline.bind_group_layout,454&BindGroupEntries::sequential((455&bloom_texture.view(mip),456sampler,457uniforms.binding().unwrap(),458)),459));460}461462commands.entity(entity).insert(BloomBindGroups {463downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),464upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),465sampler: sampler.clone(),466});467}468}469470/// Calculates blend intensities of blur pyramid levels471/// during the upsampling + compositing stage.472///473/// The function assumes all pyramid levels are upsampled and474/// blended into higher frequency ones using this function to475/// calculate blend levels every time. The final (highest frequency)476/// pyramid level in not blended into anything therefore this function477/// is not applied to it. As a result, the *mip* parameter of 0 indicates478/// the second-highest frequency pyramid level (in our case that is the479/// 0th mip of the bloom texture with the original image being the480/// actual highest frequency level).481///482/// Parameters:483/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).484/// * `max_mip` - the index of the lowest frequency pyramid level.485///486/// This function can be visually previewed for all values of *mip* (normalized) with tweakable487/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).488fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {489let mut lf_boost =490(1.0 - ops::powf(4911.0 - (mip / max_mip),4921.0 / (1.0 - bloom.low_frequency_boost_curvature),493)) * bloom.low_frequency_boost;494let high_pass_lq = 1.0495- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)496.clamp(0.0, 1.0);497lf_boost *= match bloom.composite_mode {498BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,499BloomCompositeMode::Additive => 1.0,500};501502(bloom.intensity + lf_boost) * high_pass_lq503}504505506