Path: blob/main/crates/bevy_post_process/src/bloom/mod.rs
6596 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(Node3d::EndMainPass, Node3d::Bloom, Node3d::Tonemapping),84)85// Add bloom to the 2d render graph86.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)87.add_render_graph_edges(88Core2d,89(Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping),90);91}92}9394#[derive(Default)]95struct BloomNode;96impl ViewNode for BloomNode {97type ViewQuery = (98&'static ExtractedCamera,99&'static ViewTarget,100&'static BloomTexture,101&'static BloomBindGroups,102&'static DynamicUniformIndex<BloomUniforms>,103&'static Bloom,104&'static UpsamplingPipelineIds,105&'static BloomDownsamplingPipelineIds,106);107108// Atypically for a post-processing effect, we do not need to109// use a secondary texture normally provided by view_target.post_process_write(),110// instead we write into our own bloom texture and then directly back onto main.111fn run<'w>(112&self,113_graph: &mut RenderGraphContext,114render_context: &mut RenderContext<'w>,115(116camera,117view_target,118bloom_texture,119bind_groups,120uniform_index,121bloom_settings,122upsampling_pipeline_ids,123downsampling_pipeline_ids,124): QueryItem<'w, '_, Self::ViewQuery>,125world: &'w World,126) -> Result<(), NodeRunError> {127if bloom_settings.intensity == 0.0 {128return Ok(());129}130131let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();132let pipeline_cache = world.resource::<PipelineCache>();133let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();134135let (136Some(uniforms),137Some(downsampling_first_pipeline),138Some(downsampling_pipeline),139Some(upsampling_pipeline),140Some(upsampling_final_pipeline),141) = (142uniforms.binding(),143pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),144pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),145pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),146pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),147)148else {149return Ok(());150};151152let view_texture = view_target.main_texture_view();153let view_texture_unsampled = view_target.get_unsampled_color_attachment();154let diagnostics = render_context.diagnostic_recorder();155156render_context.add_command_buffer_generation_task(move |render_device| {157#[cfg(feature = "trace")]158let _bloom_span = info_span!("bloom").entered();159160let mut command_encoder =161render_device.create_command_encoder(&CommandEncoderDescriptor {162label: Some("bloom_command_encoder"),163});164command_encoder.push_debug_group("bloom");165let time_span = diagnostics.time_span(&mut command_encoder, "bloom");166167// First downsample pass168{169let downsampling_first_bind_group = render_device.create_bind_group(170"bloom_downsampling_first_bind_group",171&downsampling_pipeline_res.bind_group_layout,172&BindGroupEntries::sequential((173// Read from main texture directly174view_texture,175&bind_groups.sampler,176uniforms.clone(),177)),178);179180let view = &bloom_texture.view(0);181let mut downsampling_first_pass =182command_encoder.begin_render_pass(&RenderPassDescriptor {183label: Some("bloom_downsampling_first_pass"),184color_attachments: &[Some(RenderPassColorAttachment {185view,186depth_slice: None,187resolve_target: None,188ops: Operations::default(),189})],190depth_stencil_attachment: None,191timestamp_writes: None,192occlusion_query_set: None,193});194downsampling_first_pass.set_pipeline(downsampling_first_pipeline);195downsampling_first_pass.set_bind_group(1960,197&downsampling_first_bind_group,198&[uniform_index.index()],199);200downsampling_first_pass.draw(0..3, 0..1);201}202203// Other downsample passes204for mip in 1..bloom_texture.mip_count {205let view = &bloom_texture.view(mip);206let mut downsampling_pass =207command_encoder.begin_render_pass(&RenderPassDescriptor {208label: Some("bloom_downsampling_pass"),209color_attachments: &[Some(RenderPassColorAttachment {210view,211depth_slice: None,212resolve_target: None,213ops: Operations::default(),214})],215depth_stencil_attachment: None,216timestamp_writes: None,217occlusion_query_set: None,218});219downsampling_pass.set_pipeline(downsampling_pipeline);220downsampling_pass.set_bind_group(2210,222&bind_groups.downsampling_bind_groups[mip as usize - 1],223&[uniform_index.index()],224);225downsampling_pass.draw(0..3, 0..1);226}227228// Upsample passes except the final one229for mip in (1..bloom_texture.mip_count).rev() {230let view = &bloom_texture.view(mip - 1);231let mut upsampling_pass =232command_encoder.begin_render_pass(&RenderPassDescriptor {233label: Some("bloom_upsampling_pass"),234color_attachments: &[Some(RenderPassColorAttachment {235view,236depth_slice: None,237resolve_target: None,238ops: Operations {239load: LoadOp::Load,240store: StoreOp::Store,241},242})],243depth_stencil_attachment: None,244timestamp_writes: None,245occlusion_query_set: None,246});247upsampling_pass.set_pipeline(upsampling_pipeline);248upsampling_pass.set_bind_group(2490,250&bind_groups.upsampling_bind_groups251[(bloom_texture.mip_count - mip - 1) as usize],252&[uniform_index.index()],253);254let blend = compute_blend_factor(255bloom_settings,256mip as f32,257(bloom_texture.mip_count - 1) as f32,258);259upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into());260upsampling_pass.draw(0..3, 0..1);261}262263// Final upsample pass264// This is very similar to the above upsampling passes with the only difference265// being the pipeline (which itself is barely different) and the color attachment266{267let mut upsampling_final_pass =268command_encoder.begin_render_pass(&RenderPassDescriptor {269label: Some("bloom_upsampling_final_pass"),270color_attachments: &[Some(view_texture_unsampled)],271depth_stencil_attachment: None,272timestamp_writes: None,273occlusion_query_set: None,274});275upsampling_final_pass.set_pipeline(upsampling_final_pipeline);276upsampling_final_pass.set_bind_group(2770,278&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],279&[uniform_index.index()],280);281if let Some(viewport) = camera.viewport.as_ref() {282upsampling_final_pass.set_viewport(283viewport.physical_position.x as f32,284viewport.physical_position.y as f32,285viewport.physical_size.x as f32,286viewport.physical_size.y as f32,287viewport.depth.start,288viewport.depth.end,289);290}291let blend =292compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);293upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into());294upsampling_final_pass.draw(0..3, 0..1);295}296297time_span.end(&mut command_encoder);298command_encoder.pop_debug_group();299command_encoder.finish()300});301302Ok(())303}304}305306#[derive(Component)]307struct BloomTexture {308// First mip is half the screen resolution, successive mips are half the previous309#[cfg(any(310not(feature = "webgl"),311not(target_arch = "wasm32"),312feature = "webgpu"313))]314texture: CachedTexture,315// WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead316#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]317texture: Vec<CachedTexture>,318mip_count: u32,319}320321impl BloomTexture {322#[cfg(any(323not(feature = "webgl"),324not(target_arch = "wasm32"),325feature = "webgpu"326))]327fn view(&self, base_mip_level: u32) -> TextureView {328self.texture.texture.create_view(&TextureViewDescriptor {329base_mip_level,330mip_level_count: Some(1u32),331..Default::default()332})333}334#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]335fn view(&self, base_mip_level: u32) -> TextureView {336self.texture[base_mip_level as usize]337.texture338.create_view(&TextureViewDescriptor {339base_mip_level: 0,340mip_level_count: Some(1u32),341..Default::default()342})343}344}345346fn prepare_bloom_textures(347mut commands: Commands,348mut texture_cache: ResMut<TextureCache>,349render_device: Res<RenderDevice>,350views: Query<(Entity, &ExtractedCamera, &Bloom)>,351) {352for (entity, camera, bloom) in &views {353if let Some(viewport) = camera.physical_viewport_size {354// How many times we can halve the resolution minus one so we don't go unnecessarily low355let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;356let mip_height_ratio = if viewport.y != 0 {357bloom.max_mip_dimension as f32 / viewport.y as f32358} else {3590.360};361362let texture_descriptor = TextureDescriptor {363label: Some("bloom_texture"),364size: (viewport.as_vec2() * mip_height_ratio)365.round()366.as_uvec2()367.max(UVec2::ONE)368.to_extents(),369mip_level_count: mip_count,370sample_count: 1,371dimension: TextureDimension::D2,372format: BLOOM_TEXTURE_FORMAT,373usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,374view_formats: &[],375};376377#[cfg(any(378not(feature = "webgl"),379not(target_arch = "wasm32"),380feature = "webgpu"381))]382let texture = texture_cache.get(&render_device, texture_descriptor);383#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]384let texture: Vec<CachedTexture> = (0..mip_count)385.map(|mip| {386texture_cache.get(387&render_device,388TextureDescriptor {389size: Extent3d {390width: (texture_descriptor.size.width >> mip).max(1),391height: (texture_descriptor.size.height >> mip).max(1),392depth_or_array_layers: 1,393},394mip_level_count: 1,395..texture_descriptor.clone()396},397)398})399.collect();400401commands402.entity(entity)403.insert(BloomTexture { texture, mip_count });404}405}406}407408#[derive(Component)]409struct BloomBindGroups {410downsampling_bind_groups: Box<[BindGroup]>,411upsampling_bind_groups: Box<[BindGroup]>,412sampler: Sampler,413}414415fn prepare_bloom_bind_groups(416mut commands: Commands,417render_device: Res<RenderDevice>,418downsampling_pipeline: Res<BloomDownsamplingPipeline>,419upsampling_pipeline: Res<BloomUpsamplingPipeline>,420views: Query<(Entity, &BloomTexture)>,421uniforms: Res<ComponentUniforms<BloomUniforms>>,422) {423let sampler = &downsampling_pipeline.sampler;424425for (entity, bloom_texture) in &views {426let bind_group_count = bloom_texture.mip_count as usize - 1;427428let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);429for mip in 1..bloom_texture.mip_count {430downsampling_bind_groups.push(render_device.create_bind_group(431"bloom_downsampling_bind_group",432&downsampling_pipeline.bind_group_layout,433&BindGroupEntries::sequential((434&bloom_texture.view(mip - 1),435sampler,436uniforms.binding().unwrap(),437)),438));439}440441let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);442for mip in (0..bloom_texture.mip_count).rev() {443upsampling_bind_groups.push(render_device.create_bind_group(444"bloom_upsampling_bind_group",445&upsampling_pipeline.bind_group_layout,446&BindGroupEntries::sequential((447&bloom_texture.view(mip),448sampler,449uniforms.binding().unwrap(),450)),451));452}453454commands.entity(entity).insert(BloomBindGroups {455downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),456upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),457sampler: sampler.clone(),458});459}460}461462/// Calculates blend intensities of blur pyramid levels463/// during the upsampling + compositing stage.464///465/// The function assumes all pyramid levels are upsampled and466/// blended into higher frequency ones using this function to467/// calculate blend levels every time. The final (highest frequency)468/// pyramid level in not blended into anything therefore this function469/// is not applied to it. As a result, the *mip* parameter of 0 indicates470/// the second-highest frequency pyramid level (in our case that is the471/// 0th mip of the bloom texture with the original image being the472/// actual highest frequency level).473///474/// Parameters:475/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).476/// * `max_mip` - the index of the lowest frequency pyramid level.477///478/// This function can be visually previewed for all values of *mip* (normalized) with tweakable479/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).480fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {481let mut lf_boost =482(1.0 - ops::powf(4831.0 - (mip / max_mip),4841.0 / (1.0 - bloom.low_frequency_boost_curvature),485)) * bloom.low_frequency_boost;486let high_pass_lq = 1.0487- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)488.clamp(0.0, 1.0);489lf_boost *= match bloom.composite_mode {490BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,491BloomCompositeMode::Additive => 1.0,492};493494(bloom.intensity + lf_boost) * high_pass_lq495}496497498