diff --git a/assets/textures/lens_dirt_texture.png b/assets/textures/lens_dirt_texture.png new file mode 100644 index 0000000000000..65f6f4944eac9 Binary files /dev/null and b/assets/textures/lens_dirt_texture.png differ diff --git a/crates/bevy_post_process/src/bloom/bloom.wgsl b/crates/bevy_post_process/src/bloom/bloom.wgsl index 8a7c3833c820e..eef06ab75efdd 100644 --- a/crates/bevy_post_process/src/bloom/bloom.wgsl +++ b/crates/bevy_post_process/src/bloom/bloom.wgsl @@ -6,6 +6,12 @@ // * [COD] - Next Generation Post Processing in Call of Duty - http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare // * [PBB] - Physically Based Bloom - https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +#ifdef LENS_DIRT +#import bevy_post_process::lens_dirt::{LensDirtUniforms, lens_dirt_texture, lens_dirt_sampler, lens_dirt_uniforms} +#endif + struct BloomUniforms { threshold_precomputations: vec4, viewport: vec4, @@ -15,8 +21,8 @@ struct BloomUniforms { @group(0) @binding(0) var input_texture: texture_2d; @group(0) @binding(1) var s: sampler; - @group(0) @binding(2) var uniforms: BloomUniforms; +@group(0) @binding(3) var blend_factor: f32; #ifdef FIRST_DOWNSAMPLE // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 @@ -155,9 +161,10 @@ fn sample_input_3x3_tent(uv: vec2) -> vec3 { #ifdef FIRST_DOWNSAMPLE @fragment -fn downsample_first(@location(0) output_uv: vec2) -> @location(0) vec4 { - let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw; +fn downsample_first(in: FullscreenVertexOutput) -> @location(0) vec4 { + let sample_uv = uniforms.viewport.xy + in.uv * uniforms.viewport.zw; var sample = sample_input_13_tap(sample_uv); + // Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the // downscaling and upscaling which would result in black boxes. // The upper bound is to prevent NaNs. @@ -173,11 +180,24 @@ fn downsample_first(@location(0) output_uv: vec2) -> @location(0) vec4 #endif @fragment -fn downsample(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(sample_input_13_tap(uv), 1.0); +fn downsample(in: FullscreenVertexOutput) -> @location(0) vec4 { + return vec4(sample_input_13_tap(in.uv), 1.0); } @fragment -fn upsample(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(sample_input_3x3_tent(uv), 1.0); +fn upsample(in: FullscreenVertexOutput) -> @location(0) vec4 { + let bloom = sample_input_3x3_tent(in.uv); +#ifdef LENS_DIRT + let bloom_intensity = max(bloom.r, max(bloom.g, bloom.b)); + + let dirt = textureSample(lens_dirt_texture, lens_dirt_sampler, in.uv).r; + let amount = clamp(dirt * lens_dirt_uniforms.intensity * bloom_intensity, 0.0, 1.0); + + let result_bloom = mix(bloom.rgb, bloom.rgb * lens_dirt_uniforms.tint.rgb, amount); + let alpha = mix(blend_factor, 1.0, amount); + + return vec4(result_bloom, alpha); +#else + return vec4(bloom, blend_factor); +#endif } diff --git a/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs b/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs index 137c7d3d2eef6..f03337840801b 100644 --- a/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs @@ -1,13 +1,13 @@ -use bevy_core_pipeline::FullscreenShader; +use super::{settings::BloomUniforms, Bloom, BLOOM_TEXTURE_FORMAT}; -use super::{Bloom, BLOOM_TEXTURE_FORMAT}; use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::FullscreenShader; use bevy_ecs::{ prelude::{Component, Entity}, resource::Resource, system::{Commands, Query, Res, ResMut}, }; -use bevy_math::{Vec2, Vec4}; +use bevy_math::Vec2; use bevy_render::{ render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -42,17 +42,6 @@ pub struct BloomDownsamplingPipelineKeys { uniform_scale: bool, } -/// The uniform struct extracted from [`Bloom`] attached to a Camera. -/// Will be available for use in the Bloom shader. -#[derive(Component, ShaderType, Clone)] -pub struct BloomUniforms { - // Precomputed values used when thresholding, see https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 - pub threshold_precomputations: Vec4, - pub viewport: Vec4, - pub scale: Vec2, - pub aspect: f32, -} - pub fn init_bloom_downsampling_pipeline( mut commands: Commands, render_device: Res, diff --git a/crates/bevy_post_process/src/bloom/mod.rs b/crates/bevy_post_process/src/bloom/mod.rs index f22a612718921..7fdb6827e01cc 100644 --- a/crates/bevy_post_process/src/bloom/mod.rs +++ b/crates/bevy_post_process/src/bloom/mod.rs @@ -2,21 +2,30 @@ mod downsampling_pipeline; mod settings; mod upsampling_pipeline; -use bevy_image::ToExtents; +use crate::{ + bloom::{ + downsampling_pipeline::{ + init_bloom_downsampling_pipeline, prepare_downsampling_pipeline, + BloomDownsamplingPipeline, BloomDownsamplingPipelineIds, + }, + settings::BloomUniforms, + upsampling_pipeline::{ + init_bloom_upscaling_pipeline, prepare_upsampling_pipeline, BloomUpsamplingPipeline, + UpsamplingPipelineIds, + }, + }, + lens_dirt::{LensDirtBindGroup, LensDirtUniforms}, +}; pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter}; -use crate::bloom::{ - downsampling_pipeline::init_bloom_downsampling_pipeline, - upsampling_pipeline::init_bloom_upscaling_pipeline, -}; use bevy_app::{App, Plugin}; use bevy_asset::embedded_asset; -use bevy_color::{Gray, LinearRgba}; use bevy_core_pipeline::{ schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems}, tonemapping::tonemapping, }; use bevy_ecs::prelude::*; +use bevy_image::ToExtents; use bevy_math::{ops, UVec2}; use bevy_render::{ camera::ExtractedCamera, @@ -30,16 +39,11 @@ use bevy_render::{ view::ViewTarget, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems, }; -use downsampling_pipeline::{ - prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds, - BloomUniforms, -}; -use upsampling_pipeline::{ - prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds, -}; +use core::num::NonZero; const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat; +/// A plugin that adds support for the bloom effect to Bevy. #[derive(Default)] pub struct BloomPlugin; @@ -95,6 +99,8 @@ pub fn bloom( &Bloom, &UpsamplingPipelineIds, &BloomDownsamplingPipelineIds, + Option<&LensDirtBindGroup>, + Option<&DynamicUniformIndex>, )>, downsampling_pipeline_res: Res, pipeline_cache: Res, @@ -110,12 +116,18 @@ pub fn bloom( bloom_settings, upsampling_pipeline_ids, downsampling_pipeline_ids, + maybe_lens_dirt_bind_group, + maybe_lens_dirt_uniform_index, ) = view.into_inner(); if bloom_settings.intensity == 0.0 || !camera.hdr { return; } + let final_pipeline_id = maybe_lens_dirt_bind_group + .and(upsampling_pipeline_ids.id_final_dirt) + .unwrap_or(upsampling_pipeline_ids.id_final); + let ( Some(uniforms_binding), Some(downsampling_first_pipeline), @@ -127,7 +139,7 @@ pub fn bloom( pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first), pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main), pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main), - pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final), + pipeline_cache.get_render_pipeline(final_pipeline_id), ) else { return; @@ -230,12 +242,6 @@ pub fn bloom( &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - mip - 1) as usize], &[uniform_index.index()], ); - let blend = compute_blend_factor( - bloom_settings, - mip as f32, - (bloom_texture.mip_count - 1) as f32, - ); - upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into()); upsampling_pass.draw(0..3, 0..1); } @@ -252,9 +258,18 @@ pub fn bloom( upsampling_final_pass.set_pipeline(upsampling_final_pipeline); upsampling_final_pass.set_bind_group( 0, - &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize], + &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize].clone(), &[uniform_index.index()], ); + if let (Some(lens_dirt_bind_group), Some(lens_dirt_uniform_index)) = + (maybe_lens_dirt_bind_group, maybe_lens_dirt_uniform_index) + { + upsampling_final_pass.set_bind_group( + 1, + &lens_dirt_bind_group.0, + &[lens_dirt_uniform_index.index()], + ); + } if let Some(viewport) = camera.viewport.as_ref() { upsampling_final_pass.set_viewport( viewport.physical_position.x as f32, @@ -265,8 +280,6 @@ pub fn bloom( viewport.depth.end, ); } - let blend = compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32); - upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into()); upsampling_final_pass.draw(0..3, 0..1); } @@ -396,13 +409,13 @@ fn prepare_bloom_bind_groups( render_device: Res, downsampling_pipeline: Res, upsampling_pipeline: Res, - views: Query<(Entity, &BloomTexture, Option<&BloomBindGroups>)>, + views: Query<(Entity, &BloomTexture, &Bloom, Option<&BloomBindGroups>)>, uniforms: Res>, pipeline_cache: Res, ) { let sampler = &downsampling_pipeline.sampler; - for (entity, bloom_texture, bloom_bind_groups) in &views { + for (entity, bloom_texture, bloom, bloom_bind_groups) in &views { #[cfg(any( not(feature = "webgl"), not(target_arch = "wasm32"), @@ -443,8 +456,11 @@ fn prepare_bloom_bind_groups( )); } - let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count); + let mut upsampling_bind_groups = Vec::with_capacity(bloom_texture.mip_count as usize); for mip in (0..bloom_texture.mip_count).rev() { + let blend_factor = + compute_blend_factor(bloom, mip as f32, (bloom_texture.mip_count - 1) as f32); + upsampling_bind_groups.push(render_device.create_bind_group( "bloom_upsampling_bind_group", &pipeline_cache.get_bind_group_layout(&upsampling_pipeline.bind_group_layout), @@ -452,6 +468,15 @@ fn prepare_bloom_bind_groups( &bloom_texture.view(mip), sampler, uniforms.binding().unwrap(), + BufferBinding { + buffer: &render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("bloom_blend_factor"), + contents: &blend_factor.to_ne_bytes(), + usage: BufferUsages::STORAGE, + }), + offset: 0, + size: NonZero::::new(4), + }, )), )); } diff --git a/crates/bevy_post_process/src/bloom/settings.rs b/crates/bevy_post_process/src/bloom/settings.rs index fc6ada8d82e7e..dd9b2b9cc520b 100644 --- a/crates/bevy_post_process/src/bloom/settings.rs +++ b/crates/bevy_post_process/src/bloom/settings.rs @@ -1,4 +1,3 @@ -use super::downsampling_pipeline::BloomUniforms; use bevy_camera::{Camera, Hdr}; use bevy_ecs::{ prelude::Component, @@ -7,7 +6,9 @@ use bevy_ecs::{ }; use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{extract_component::ExtractComponent, sync_component::SyncComponent}; +use bevy_render::{ + extract_component::ExtractComponent, render_resource::ShaderType, sync_component::SyncComponent, +}; /// Applies a bloom effect to an HDR-enabled 2d or 3d camera. /// @@ -213,9 +214,10 @@ pub struct BloomPrefilter { pub threshold_softness: f32, } -#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, Copy)] -#[reflect(Clone, Hash, PartialEq)] +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, Copy, Default)] +#[reflect(Clone, Hash, PartialEq, Default)] pub enum BloomCompositeMode { + #[default] EnergyConserving, Additive, } @@ -253,10 +255,10 @@ impl ExtractComponent for Bloom { viewport: UVec4::new(origin.x, origin.y, size.x, size.y).as_vec4() / UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y) .as_vec4(), + scale: bloom.scale, aspect: AspectRatio::try_from_pixels(size.x, size.y) .expect("Valid screen size values for Bloom settings") .ratio(), - scale: bloom.scale, }; Some((bloom.clone(), uniform)) @@ -265,3 +267,14 @@ impl ExtractComponent for Bloom { } } } + +/// The uniform struct extracted from [`Bloom`] attached to a Camera. +/// Will be available for use in the Bloom shader. +#[derive(Component, ShaderType, Clone)] +pub struct BloomUniforms { + // Precomputed values used when thresholding, see https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 + pub threshold_precomputations: Vec4, + pub viewport: Vec4, + pub scale: Vec2, + pub aspect: f32, +} diff --git a/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs b/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs index dec001239951f..21228528ad10b 100644 --- a/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs @@ -1,9 +1,8 @@ -use bevy_core_pipeline::FullscreenShader; +use super::{settings::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT}; +use crate::lens_dirt::{create_lens_dirt_bind_group_layout, LensDirt}; -use super::{ - downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT, -}; use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::FullscreenShader; use bevy_ecs::{ prelude::{Component, Entity}, resource::Resource, @@ -11,23 +10,26 @@ use bevy_ecs::{ }; use bevy_render::{ render_resource::{ - binding_types::{sampler, texture_2d, uniform_buffer}, + binding_types::{sampler, storage_buffer_read_only_sized, texture_2d, uniform_buffer}, *, }, view::ExtractedView, }; use bevy_shader::Shader; use bevy_utils::default; +use core::num::NonZero; #[derive(Component)] pub struct UpsamplingPipelineIds { pub id_main: CachedRenderPipelineId, pub id_final: CachedRenderPipelineId, + pub id_final_dirt: Option, } #[derive(Resource)] pub struct BloomUpsamplingPipeline { pub bind_group_layout: BindGroupLayoutDescriptor, + pub lens_dirt_bind_group_layout: BindGroupLayoutDescriptor, /// The asset handle for the fullscreen vertex shader. pub fullscreen_shader: FullscreenShader, /// The fragment shader asset handle. @@ -38,6 +40,7 @@ pub struct BloomUpsamplingPipeline { pub struct BloomUpsamplingPipelineKeys { composite_mode: BloomCompositeMode, target_format: TextureFormat, + lens_dirt: bool, } pub fn init_bloom_upscaling_pipeline( @@ -50,18 +53,19 @@ pub fn init_bloom_upscaling_pipeline( &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, ( - // Input texture texture_2d(TextureSampleType::Float { filterable: true }), - // Sampler sampler(SamplerBindingType::Filtering), - // BloomUniforms uniform_buffer::(true), + storage_buffer_read_only_sized(false, NonZero::::new(4)), ), ), ); + let lens_dirt_bind_group_layout = create_lens_dirt_bind_group_layout(); + commands.insert_resource(BloomUpsamplingPipeline { bind_group_layout, + lens_dirt_bind_group_layout, fullscreen_shader: fullscreen_shader.clone(), fragment_shader: load_embedded_asset!(asset_server.as_ref(), "bloom.wgsl"), }); @@ -72,43 +76,37 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { let color_blend = match key.composite_mode { - BloomCompositeMode::EnergyConserving => { - // At the time of developing this we decided to blend our - // blur pyramid levels using native WGPU render pass blend - // constants. They are set in the bloom node's run function. - // This seemed like a good approach at the time which allowed - // us to perform complex calculations for blend levels on the CPU, - // however, we missed the fact that this prevented us from using - // textures to customize bloom appearance on individual parts - // of the screen and create effects such as lens dirt or - // screen blur behind certain UI elements. - // - // TODO: Use alpha instead of blend constants and move - // compute_blend_factor to the shader. The shader - // will likely need to know current mip number or - // mip "angle" (original texture is 0deg, max mip is 90deg) - // so make sure you give it that as a uniform. - // That does have to be provided per each pass unlike other - // uniforms that are set once. - BlendComponent { - src_factor: BlendFactor::Constant, - dst_factor: BlendFactor::OneMinusConstant, - operation: BlendOperation::Add, - } - } + BloomCompositeMode::EnergyConserving => BlendComponent { + src_factor: BlendFactor::SrcAlpha, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, BloomCompositeMode::Additive => BlendComponent { - src_factor: BlendFactor::Constant, + src_factor: BlendFactor::SrcAlpha, dst_factor: BlendFactor::One, operation: BlendOperation::Add, }, }; + let (layout, shader_defs) = if key.lens_dirt { + ( + vec![ + self.bind_group_layout.clone(), + self.lens_dirt_bind_group_layout.clone(), + ], + vec!["LENS_DIRT".into()], + ) + } else { + (vec![self.bind_group_layout.clone()], vec![]) + }; + RenderPipelineDescriptor { label: Some("bloom_upsampling_pipeline".into()), - layout: vec![self.bind_group_layout.clone()], + layout, vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), + shader_defs, entry_point: Some("upsample".into()), targets: vec![Some(ColorTargetState { format: key.target_format, @@ -134,15 +132,16 @@ pub fn prepare_upsampling_pipeline( pipeline_cache: Res, mut pipelines: ResMut>, pipeline: Res, - views: Query<(&ExtractedView, Entity, &Bloom)>, + views: Query<(&ExtractedView, Entity, &Bloom, Option<&LensDirt>)>, ) { - for (view, entity, bloom) in &views { + for (view, entity, bloom, maybe_lens_dirt) in &views { let pipeline_id = pipelines.specialize( &pipeline_cache, &pipeline, BloomUpsamplingPipelineKeys { composite_mode: bloom.composite_mode, target_format: BLOOM_TEXTURE_FORMAT, + lens_dirt: false, }, ); @@ -152,12 +151,26 @@ pub fn prepare_upsampling_pipeline( BloomUpsamplingPipelineKeys { composite_mode: bloom.composite_mode, target_format: view.target_format, + lens_dirt: false, }, ); + let pipeline_final_dirt_id = maybe_lens_dirt.is_some().then(|| { + pipelines.specialize( + &pipeline_cache, + &pipeline, + BloomUpsamplingPipelineKeys { + composite_mode: bloom.composite_mode, + target_format: view.target_format, + lens_dirt: true, + }, + ) + }); + commands.entity(entity).insert(UpsamplingPipelineIds { id_main: pipeline_id, id_final: pipeline_final_id, + id_final_dirt: pipeline_final_dirt_id, }); } } diff --git a/crates/bevy_post_process/src/lens_dirt/lens_dirt.wgsl b/crates/bevy_post_process/src/lens_dirt/lens_dirt.wgsl new file mode 100644 index 0000000000000..41116d96426cb --- /dev/null +++ b/crates/bevy_post_process/src/lens_dirt/lens_dirt.wgsl @@ -0,0 +1,10 @@ +#define_import_path bevy_post_process::lens_dirt + +struct LensDirtUniforms { + intensity: f32, + tint: vec3, +}; + +@group(1) @binding(0) var lens_dirt_texture: texture_2d; +@group(1) @binding(1) var lens_dirt_sampler: sampler; +@group(1) @binding(2) var lens_dirt_uniforms: LensDirtUniforms; diff --git a/crates/bevy_post_process/src/lens_dirt/mod.rs b/crates/bevy_post_process/src/lens_dirt/mod.rs new file mode 100644 index 0000000000000..7f0a8e8e6b8e4 --- /dev/null +++ b/crates/bevy_post_process/src/lens_dirt/mod.rs @@ -0,0 +1,175 @@ +use bevy_app::Plugin; +use bevy_asset::Handle; +use bevy_camera::Camera; +use bevy_color::{Color, ColorToComponents}; +use bevy_ecs::{ + component::Component, + entity::Entity, + prelude::Resource, + query::{QueryItem, With}, + reflect::ReflectComponent, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res}, +}; +use bevy_image::Image; +use bevy_math::Vec3; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::RenderAssets, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntries, SamplerBindingType, ShaderStages, ShaderType, TextureSampleType, + }, + renderer::RenderDevice, + sync_component::SyncComponent, + texture::{FallbackImage, GpuImage}, + uniform::{ComponentUniforms, UniformComponentPlugin}, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::load_shader_library; + +/// A plugin that adds support for the lens dirt effect to Bevy. +#[derive(Default)] +pub struct LensDirtPlugin; + +impl Plugin for LensDirtPlugin { + fn build(&self, app: &mut bevy_app::App) { + load_shader_library!(app, "lens_dirt.wgsl"); + + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_systems(RenderStartup, init_lens_dirt_bind_group) + .add_systems( + Render, + prepare_lens_dirt_bind_group.in_set(RenderSystems::PrepareBindGroups), + ); + } +} + +#[derive(Resource)] +pub struct LensDirtBindGroupLayout(pub BindGroupLayout); + +#[derive(Component)] +pub struct LensDirtBindGroup(pub BindGroup); + +pub fn create_lens_dirt_bind_group_layout() -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "lens_dirt_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + uniform_buffer::(true), + ), + ), + ) +} + +fn init_lens_dirt_bind_group(mut commands: Commands, render_device: Res) { + let bind_group_layout = render_device.create_bind_group_layout( + "lens_dirt_bind_group_layout", + &create_lens_dirt_bind_group_layout().entries, + ); + + commands.insert_resource(LensDirtBindGroupLayout(bind_group_layout)); +} + +fn prepare_lens_dirt_bind_group( + mut commands: Commands, + render_device: Res, + lens_dirt_layout: Res, + gpu_images: Res>, + uniforms: Res>, + fallback: Res, + views: Query<(Entity, &LensDirt)>, +) { + let Some(uniform_binding) = uniforms.binding() else { + return; + }; + for (entity, lens_dirt) in &views { + let dirt_image = gpu_images.get(&lens_dirt.texture).unwrap_or(&fallback.d2); + let bind_group = render_device.create_bind_group( + "lens_dirt_bind_group", + &lens_dirt_layout.0, + &BindGroupEntries::sequential(( + &dirt_image.texture_view, + &dirt_image.sampler, + uniform_binding.clone(), + )), + ); + commands + .entity(entity) + .insert(LensDirtBindGroup(bind_group)); + } +} + +/// A component that enables a lens dirt effect when added to a camera. +/// Simulates the effect of dirt on the lens. +/// +/// Currently, the lens dirt only interacts with the bloom effect. +#[derive(Component, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +pub struct LensDirt { + /// The lens dirt texture. Set to `Some` to enable the effect. + pub texture: Handle, + + /// How strongly the lens dirt appears (default: 1.0). + /// + /// Valid range: 0.0 to 1.0 where: + /// * 0.0 - No dirt visible + /// * 1.0 - Full dirt intensity + pub intensity: f32, + + /// Color tint applied to the lens dirt (default: `Color::WHITE`). + /// + /// Use this to match the dirt effect to your scene's lighting or mood. + pub tint: Color, +} + +impl Default for LensDirt { + fn default() -> Self { + Self { + texture: Handle::default(), + intensity: 1.0, + tint: Color::default(), + } + } +} + +impl SyncComponent for LensDirt { + type Target = (Self, LensDirt); +} + +impl ExtractComponent for LensDirt { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = (Self, LensDirtUniforms); + + fn extract_component(lens_dirt: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(( + lens_dirt.clone(), + LensDirtUniforms { + intensity: lens_dirt.intensity, + tint: lens_dirt.tint.to_linear().to_vec3(), + }, + )) + } +} + +/// The uniform struct extracted from [`LensDirt`] attached to a Camera. +#[derive(Component, ShaderType, Clone)] +pub struct LensDirtUniforms { + pub intensity: f32, + pub tint: Vec3, +} diff --git a/crates/bevy_post_process/src/lib.rs b/crates/bevy_post_process/src/lib.rs index 848d93d36cb99..d5fb3b8270770 100644 --- a/crates/bevy_post_process/src/lib.rs +++ b/crates/bevy_post_process/src/lib.rs @@ -10,12 +10,13 @@ pub mod auto_exposure; pub mod bloom; pub mod dof; pub mod effect_stack; +pub mod lens_dirt; pub mod motion_blur; pub mod msaa_writeback; use crate::{ bloom::BloomPlugin, dof::DepthOfFieldPlugin, effect_stack::EffectStackPlugin, - motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, + lens_dirt::LensDirtPlugin, motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, }; use bevy_app::{App, Plugin}; use bevy_shader::load_shader_library; @@ -34,6 +35,7 @@ impl Plugin for PostProcessPlugin { MotionBlurPlugin, DepthOfFieldPlugin, EffectStackPlugin, + LensDirtPlugin, )); } } diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index 982b59745fd45..b4ca9c39cd4ca 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -3,7 +3,10 @@ use bevy::{ core_pipeline::tonemapping::Tonemapping, math::ops, - post_process::bloom::{Bloom, BloomCompositeMode}, + post_process::{ + bloom::{Bloom, BloomCompositeMode}, + lens_dirt::LensDirt, + }, prelude::*, }; use std::{ @@ -23,6 +26,7 @@ fn setup_scene( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, + asset_server: Res, ) { commands.spawn(( Camera3d::default(), @@ -33,6 +37,11 @@ fn setup_scene( Tonemapping::TonyMcMapface, // 1. Using a tonemapper that desaturates to white is recommended Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), Bloom::NATURAL, // 2. Enable bloom for the camera + LensDirt { + // https://opengameart.org/content/lens-dirt-texture-pack-3png + texture: asset_server.load("textures/lens_dirt_texture.png"), + ..default() + }, )); let material_emissive1 = materials.add(StandardMaterial { @@ -95,7 +104,7 @@ fn setup_scene( // ------------------------------------------------------------------------------------------------ fn update_bloom_settings( - camera: Single<(Entity, Option<&mut Bloom>), With>, + camera: Single<(Entity, Option<&mut Bloom>, &mut LensDirt), With>, mut text: Single<&mut Text>, mut commands: Commands, keycode: Res>, @@ -104,7 +113,7 @@ fn update_bloom_settings( let bloom = camera.into_inner(); match bloom { - (entity, Some(mut bloom)) => { + (entity, Some(mut bloom), mut lens_dirt) => { text.0 = "Bloom (Toggle: Space)\n".to_string(); text.push_str(&format!("(Q/A) Intensity: {:.2}\n", bloom.intensity)); text.push_str(&format!( @@ -136,6 +145,11 @@ fn update_bloom_settings( )); text.push_str(&format!("(I/K) Horizontal Scale: {:.2}\n", bloom.scale.x)); + text.push_str(&format!( + "(O/L) Lens Dirt intensity: {:.2}\n", + lens_dirt.intensity + )); + if keycode.just_pressed(KeyCode::Space) { commands.entity(entity).remove::(); } @@ -205,9 +219,18 @@ fn update_bloom_settings( bloom.scale.x += dt * 2.0; } bloom.scale.x = bloom.scale.x.clamp(0.0, 8.0); + + if keycode.pressed(KeyCode::KeyL) { + lens_dirt.intensity -= dt / 10.0; + } + if keycode.pressed(KeyCode::KeyO) { + lens_dirt.intensity += dt / 10.0; + } + + lens_dirt.intensity = lens_dirt.intensity.clamp(0.0, 1.0); } - (entity, None) => { + (entity, None, _) => { text.0 = "Bloom: Off (Toggle: Space)".to_string(); if keycode.just_pressed(KeyCode::Space) {