Path: blob/main/crates/bevy_post_process/src/bloom/mod.rs
9366 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::{15schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems},16tonemapping::tonemapping,17};18use bevy_ecs::prelude::*;19use bevy_math::{ops, UVec2};20use bevy_render::{21camera::ExtractedCamera,22diagnostic::RecordDiagnostics,23extract_component::{24ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,25},26render_resource::*,27renderer::{RenderContext, RenderDevice, ViewQuery},28texture::{CachedTexture, TextureCache},29view::ViewTarget,30Render, RenderApp, RenderStartup, RenderSystems,31};32use downsampling_pipeline::{33prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,34BloomUniforms,35};36use upsampling_pipeline::{37prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,38};3940const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;4142#[derive(Default)]43pub struct BloomPlugin;4445impl Plugin for BloomPlugin {46fn build(&self, app: &mut App) {47embedded_asset!(app, "bloom.wgsl");4849app.add_plugins((50ExtractComponentPlugin::<Bloom>::default(),51UniformComponentPlugin::<BloomUniforms>::default(),52));5354let Some(render_app) = app.get_sub_app_mut(RenderApp) else {55return;56};57render_app58.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()59.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()60.add_systems(61RenderStartup,62(63init_bloom_downsampling_pipeline,64init_bloom_upscaling_pipeline,65),66)67.add_systems(68Render,69(70prepare_downsampling_pipeline.in_set(RenderSystems::Prepare),71prepare_upsampling_pipeline.in_set(RenderSystems::Prepare),72prepare_bloom_textures.in_set(RenderSystems::PrepareResources),73prepare_bloom_bind_groups.in_set(RenderSystems::PrepareBindGroups),74),75)76.add_systems(77Core3d,78bloom.before(tonemapping).in_set(Core3dSystems::PostProcess),79)80.add_systems(81Core2d,82bloom.before(tonemapping).in_set(Core2dSystems::PostProcess),83);84}85}8687pub fn bloom(88view: ViewQuery<(89&ExtractedCamera,90&ViewTarget,91&BloomTexture,92&BloomBindGroups,93&DynamicUniformIndex<BloomUniforms>,94&Bloom,95&UpsamplingPipelineIds,96&BloomDownsamplingPipelineIds,97)>,98downsampling_pipeline_res: Res<BloomDownsamplingPipeline>,99pipeline_cache: Res<PipelineCache>,100uniforms: Res<ComponentUniforms<BloomUniforms>>,101mut ctx: RenderContext,102) {103let (104camera,105view_target,106bloom_texture,107bind_groups,108uniform_index,109bloom_settings,110upsampling_pipeline_ids,111downsampling_pipeline_ids,112) = view.into_inner();113114if bloom_settings.intensity == 0.0 {115return;116}117118let (119Some(uniforms_binding),120Some(downsampling_first_pipeline),121Some(downsampling_pipeline),122Some(upsampling_pipeline),123Some(upsampling_final_pipeline),124) = (125uniforms.binding(),126pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),127pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),128pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),129pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),130)131else {132return;133};134135let view_texture = view_target.main_texture_view();136let view_texture_unsampled = view_target.get_unsampled_color_attachment();137138// Create the first downsampling bind group (reads from main texture)139let downsampling_first_bind_group = ctx.render_device().create_bind_group(140"bloom_downsampling_first_bind_group",141&pipeline_cache.get_bind_group_layout(&downsampling_pipeline_res.bind_group_layout),142&BindGroupEntries::sequential((143view_texture,144&bind_groups.sampler,145uniforms_binding.clone(),146)),147);148149let diagnostics = ctx.diagnostic_recorder();150let diagnostics = diagnostics.as_deref();151let time_span = diagnostics.time_span(ctx.command_encoder(), "bloom");152153let command_encoder = ctx.command_encoder();154command_encoder.push_debug_group("bloom");155156// First downsample pass157{158let view = &bloom_texture.view(0);159let mut downsampling_first_pass =160command_encoder.begin_render_pass(&RenderPassDescriptor {161label: Some("bloom_downsampling_first_pass"),162color_attachments: &[Some(RenderPassColorAttachment {163view,164depth_slice: None,165resolve_target: None,166ops: Operations::default(),167})],168depth_stencil_attachment: None,169timestamp_writes: None,170occlusion_query_set: None,171multiview_mask: None,172});173downsampling_first_pass.set_pipeline(downsampling_first_pipeline);174downsampling_first_pass.set_bind_group(1750,176&downsampling_first_bind_group,177&[uniform_index.index()],178);179downsampling_first_pass.draw(0..3, 0..1);180}181182// Other downsample passes183for mip in 1..bloom_texture.mip_count {184let view = &bloom_texture.view(mip);185let mut downsampling_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {186label: Some("bloom_downsampling_pass"),187color_attachments: &[Some(RenderPassColorAttachment {188view,189depth_slice: None,190resolve_target: None,191ops: Operations::default(),192})],193depth_stencil_attachment: None,194timestamp_writes: None,195occlusion_query_set: None,196multiview_mask: None,197});198downsampling_pass.set_pipeline(downsampling_pipeline);199downsampling_pass.set_bind_group(2000,201&bind_groups.downsampling_bind_groups[mip as usize - 1],202&[uniform_index.index()],203);204downsampling_pass.draw(0..3, 0..1);205}206207// Upsample passes except the final one208for mip in (1..bloom_texture.mip_count).rev() {209let view = &bloom_texture.view(mip - 1);210let mut upsampling_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {211label: Some("bloom_upsampling_pass"),212color_attachments: &[Some(RenderPassColorAttachment {213view,214depth_slice: None,215resolve_target: None,216ops: Operations {217load: LoadOp::Load,218store: StoreOp::Store,219},220})],221depth_stencil_attachment: None,222timestamp_writes: None,223occlusion_query_set: None,224multiview_mask: None,225});226upsampling_pass.set_pipeline(upsampling_pipeline);227upsampling_pass.set_bind_group(2280,229&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - mip - 1) as usize],230&[uniform_index.index()],231);232let blend = compute_blend_factor(233bloom_settings,234mip as f32,235(bloom_texture.mip_count - 1) as f32,236);237upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into());238upsampling_pass.draw(0..3, 0..1);239}240241// Final upsample pass242{243let mut upsampling_final_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {244label: Some("bloom_upsampling_final_pass"),245color_attachments: &[Some(view_texture_unsampled)],246depth_stencil_attachment: None,247timestamp_writes: None,248occlusion_query_set: None,249multiview_mask: None,250});251upsampling_final_pass.set_pipeline(upsampling_final_pipeline);252upsampling_final_pass.set_bind_group(2530,254&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],255&[uniform_index.index()],256);257if let Some(viewport) = camera.viewport.as_ref() {258upsampling_final_pass.set_viewport(259viewport.physical_position.x as f32,260viewport.physical_position.y as f32,261viewport.physical_size.x as f32,262viewport.physical_size.y as f32,263viewport.depth.start,264viewport.depth.end,265);266}267let blend = compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);268upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into());269upsampling_final_pass.draw(0..3, 0..1);270}271272command_encoder.pop_debug_group();273time_span.end(ctx.command_encoder());274}275276#[derive(Component)]277pub struct BloomTexture {278// First mip is half the screen resolution, successive mips are half the previous279#[cfg(any(280not(feature = "webgl"),281not(target_arch = "wasm32"),282feature = "webgpu"283))]284texture: CachedTexture,285// WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead286#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]287texture: Vec<CachedTexture>,288mip_count: u32,289}290291impl BloomTexture {292#[cfg(any(293not(feature = "webgl"),294not(target_arch = "wasm32"),295feature = "webgpu"296))]297fn view(&self, base_mip_level: u32) -> TextureView {298self.texture.texture.create_view(&TextureViewDescriptor {299base_mip_level,300mip_level_count: Some(1u32),301..Default::default()302})303}304#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]305fn view(&self, base_mip_level: u32) -> TextureView {306self.texture[base_mip_level as usize]307.texture308.create_view(&TextureViewDescriptor {309base_mip_level: 0,310mip_level_count: Some(1u32),311..Default::default()312})313}314}315316fn prepare_bloom_textures(317mut commands: Commands,318mut texture_cache: ResMut<TextureCache>,319render_device: Res<RenderDevice>,320views: Query<(Entity, &ExtractedCamera, &Bloom)>,321) {322for (entity, camera, bloom) in &views {323if let Some(viewport) = camera.physical_viewport_size {324// How many times we can halve the resolution minus one so we don't go unnecessarily low325let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;326let mip_height_ratio = if viewport.y != 0 {327bloom.max_mip_dimension as f32 / viewport.y as f32328} else {3290.330};331332let texture_descriptor = TextureDescriptor {333label: Some("bloom_texture"),334size: (viewport.as_vec2() * mip_height_ratio)335.round()336.as_uvec2()337.max(UVec2::ONE)338.to_extents(),339mip_level_count: mip_count,340sample_count: 1,341dimension: TextureDimension::D2,342format: BLOOM_TEXTURE_FORMAT,343usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,344view_formats: &[],345};346347#[cfg(any(348not(feature = "webgl"),349not(target_arch = "wasm32"),350feature = "webgpu"351))]352let texture = texture_cache.get(&render_device, texture_descriptor);353#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]354let texture: Vec<CachedTexture> = (0..mip_count)355.map(|mip| {356texture_cache.get(357&render_device,358TextureDescriptor {359size: Extent3d {360width: (texture_descriptor.size.width >> mip).max(1),361height: (texture_descriptor.size.height >> mip).max(1),362depth_or_array_layers: 1,363},364mip_level_count: 1,365..texture_descriptor.clone()366},367)368})369.collect();370371commands372.entity(entity)373.insert(BloomTexture { texture, mip_count });374}375}376}377378#[derive(Component)]379pub struct BloomBindGroups {380cache_key: (TextureId, BufferId),381downsampling_bind_groups: Box<[BindGroup]>,382upsampling_bind_groups: Box<[BindGroup]>,383sampler: Sampler,384}385386fn prepare_bloom_bind_groups(387mut commands: Commands,388render_device: Res<RenderDevice>,389downsampling_pipeline: Res<BloomDownsamplingPipeline>,390upsampling_pipeline: Res<BloomUpsamplingPipeline>,391views: Query<(Entity, &BloomTexture, Option<&BloomBindGroups>)>,392uniforms: Res<ComponentUniforms<BloomUniforms>>,393pipeline_cache: Res<PipelineCache>,394) {395let sampler = &downsampling_pipeline.sampler;396397for (entity, bloom_texture, bloom_bind_groups) in &views {398if let Some(b) = bloom_bind_groups399&& b.cache_key400== (401bloom_texture.texture.texture.id(),402uniforms.buffer().unwrap().id(),403)404{405continue;406}407408let bind_group_count = bloom_texture.mip_count as usize - 1;409410let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);411for mip in 1..bloom_texture.mip_count {412downsampling_bind_groups.push(render_device.create_bind_group(413"bloom_downsampling_bind_group",414&pipeline_cache.get_bind_group_layout(&downsampling_pipeline.bind_group_layout),415&BindGroupEntries::sequential((416&bloom_texture.view(mip - 1),417sampler,418uniforms.binding().unwrap(),419)),420));421}422423let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);424for mip in (0..bloom_texture.mip_count).rev() {425upsampling_bind_groups.push(render_device.create_bind_group(426"bloom_upsampling_bind_group",427&pipeline_cache.get_bind_group_layout(&upsampling_pipeline.bind_group_layout),428&BindGroupEntries::sequential((429&bloom_texture.view(mip),430sampler,431uniforms.binding().unwrap(),432)),433));434}435436commands.entity(entity).insert(BloomBindGroups {437cache_key: (438bloom_texture.texture.texture.id(),439uniforms.buffer().unwrap().id(),440),441downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),442upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),443sampler: sampler.clone(),444});445}446}447448/// Calculates blend intensities of blur pyramid levels449/// during the upsampling + compositing stage.450///451/// The function assumes all pyramid levels are upsampled and452/// blended into higher frequency ones using this function to453/// calculate blend levels every time. The final (highest frequency)454/// pyramid level in not blended into anything therefore this function455/// is not applied to it. As a result, the *mip* parameter of 0 indicates456/// the second-highest frequency pyramid level (in our case that is the457/// 0th mip of the bloom texture with the original image being the458/// actual highest frequency level).459///460/// Parameters:461/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).462/// * `max_mip` - the index of the lowest frequency pyramid level.463///464/// This function can be visually previewed for all values of *mip* (normalized) with tweakable465/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).466fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {467let mut lf_boost =468(1.0 - ops::powf(4691.0 - (mip / max_mip),4701.0 / (1.0 - bloom.low_frequency_boost_curvature),471)) * bloom.low_frequency_boost;472let high_pass_lq = 1.0473- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)474.clamp(0.0, 1.0);475lf_boost *= match bloom.composite_mode {476BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,477BloomCompositeMode::Additive => 1.0,478};479480(bloom.intensity + lf_boost) * high_pass_lq481}482483484