diff --git a/crates/bevy_post_process/src/effect_stack/bindings.wgsl b/crates/bevy_post_process/src/effect_stack/bindings.wgsl new file mode 100644 index 0000000000000..5f974182f9521 --- /dev/null +++ b/crates/bevy_post_process/src/effect_stack/bindings.wgsl @@ -0,0 +1,59 @@ +// The bindings of the effects. +#define_import_path bevy_post_process::effect_stack::bindings + + +const EPSILON: f32 = 1.19209290e-07; + +// The source framebuffer texture. +@group(0) @binding(0) var source_texture: texture_2d; +// The sampler was used to sample the source framebuffer texture. +@group(0) @binding(1) var source_sampler: sampler; +// The 1D lookup table for chromatic aberration. +@group(0) @binding(2) var chromatic_aberration_lut_texture: texture_2d; +// The sampler was used to sample that lookup table. +@group(0) @binding(3) var chromatic_aberration_lut_sampler: sampler; +// The settings were supplied by the developer. +@group(0) @binding(4) var chromatic_aberration_settings: ChromaticAberrationSettings; +// The settings were supplied by the developer. +@group(0) @binding(5) var vignette_settings: VignetteSettings; +// The film grain texture. +@group(0) @binding(6) var film_grain_texture: texture_2d; +// The sampler was used to sample the film grain texture. +@group(0) @binding(7) var film_grain_sampler: sampler; +// The settings were supplied by the developer. +@group(0) @binding(8) var film_grain_settings: FilmGrainSettings; + +// See `bevy_post_process::effect_stack::ChromaticAberration` for more +// information on these fields. +struct ChromaticAberrationSettings { + intensity: f32, + max_samples: u32, + unused_a: u32, + unused_b: u32, +} + +// See `bevy_post_process::effect_stack::Vignette` for more +// information on these fields. +struct VignetteSettings { + intensity: f32, + radius: f32, + smoothness: f32, + roundness: f32, + center: vec2, + edge_compensation: f32, + unused: u32, + color: vec4 +} + +// See `bevy_post_process::effect_stack::FilmGrain` for more +// information on these fields. +struct FilmGrainSettings { + intensity: f32, + shadows_intensity: f32, + midtones_intensity: f32, + highlights_intensity: f32, + shadows_threshold: f32, + highlights_threshold: f32, + grain_size: f32, + frame:u32 +} diff --git a/crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl b/crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl index 9f7a5c540c8d3..82b21ca8afa2b 100644 --- a/crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl +++ b/crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl @@ -4,25 +4,7 @@ #define_import_path bevy_post_process::effect_stack::chromatic_aberration -// See `bevy_post_process::effect_stack::ChromaticAberration` for more -// information on these fields. -struct ChromaticAberrationSettings { - intensity: f32, - max_samples: u32, - unused_a: u32, - unused_b: u32, -} - -// The source framebuffer texture. -@group(0) @binding(0) var source_texture: texture_2d; -// The sampler used to sample the source framebuffer texture. -@group(0) @binding(1) var source_sampler: sampler; -// The 1D lookup table for chromatic aberration. -@group(0) @binding(2) var chromatic_aberration_lut_texture: texture_2d; -// The sampler used to sample that lookup table. -@group(0) @binding(3) var chromatic_aberration_lut_sampler: sampler; -// The settings supplied by the developer. -@group(0) @binding(4) var chromatic_aberration_settings: ChromaticAberrationSettings; +#import bevy_post_process::effect_stack::bindings::{source_texture, source_sampler, chromatic_aberration_lut_texture, chromatic_aberration_lut_sampler, chromatic_aberration_settings, ChromaticAberrationSettings} fn chromatic_aberration(start_pos: vec2) -> vec3 { // Radial chromatic aberration implemented using the *Inside* technique: diff --git a/crates/bevy_post_process/src/effect_stack/film_grain.rs b/crates/bevy_post_process/src/effect_stack/film_grain.rs new file mode 100644 index 0000000000000..946838400d5b7 --- /dev/null +++ b/crates/bevy_post_process/src/effect_stack/film_grain.rs @@ -0,0 +1,148 @@ +use bevy_asset::Handle; +use bevy_camera::Camera; +use bevy_ecs::{ + component::Component, + query::{QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + system::lifetimeless::Read, +}; +use bevy_image::Image; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{extract_component::ExtractComponent, render_resource::ShaderType}; + +/// The placeholder data for the default film grain texture. +/// +/// Not used for the actual effect, but to signal the shader if a texture was provided. +pub(super) static DEFAULT_FILM_GRAIN_TEXTURE_DATA: [u8; 4] = [255, 255, 255, 255]; + +/// The default film grain intensity amount. +const DEFAULT_FILM_GRAIN_INTENSITY: f32 = 0.05; +/// The default film grain shadows intensity amount. +const DEFAULT_FILM_GRAIN_SHADOWS_INTENSITY: f32 = 1.0; +/// The default film grain midtones intensity amount. +const DEFAULT_FILM_GRAIN_MIDTONES_INTENSITY: f32 = 0.5; +/// The default film grain highlight intensity amount. +const DEFAULT_FILM_GRAIN_HIGHLIGHTS_INTENSITY: f32 = 0.1; +/// The default film grain shadows threshold amount. +const DEFAULT_FILM_GRAIN_SHADOWS_THRESHOLD: f32 = 0.25; +/// The default film grain highlights threshold amount. +const DEFAULT_FILM_GRAIN_HIGHLIGHT_THRESHOLD: f32 = 0.75; +/// The default film grain grain size amount. +const DEFAULT_FILM_GRAIN_GRAIN_SIZE: f32 = 1.0; + +#[derive(Resource)] +pub(super) struct DefaultFilmGrainTexture(pub(super) Handle); + +#[derive(Reflect, Component, Clone)] +#[reflect(Component, Default, Clone)] +pub struct FilmGrain { + /// The overall intensity of the film grain effect. + /// + /// The recommended range is 0.0 to 0.20. + /// + /// Range: `0.0` to `1.0` + /// The default value is 0.05. + pub intensity: f32, + + /// The intensity of the film grain in shadow areas. + /// + /// Range: `0.0` to `1.0`. + /// The default value is 1.0. + pub shadows_intensity: f32, + + /// The intensity of the film grain in midtone areas. + /// + /// Range: `0.0` to `1.0`. + /// The default value is 0.5. + pub midtones_intensity: f32, + + /// The intensity of the film grain in highlight areas. + /// + /// Range: `0.0` to `1.0`. + /// The default value is 0.1. + pub highlights_intensity: f32, + + /// The threshold separating shadows from midtones. + /// + /// Pixels below this value are considered shadows. This value should be + /// lower than `highlights_threshold`. + /// + /// Range: `0.0` to `1.0`. + /// The default value is 0.25. + pub shadows_threshold: f32, + + /// The threshold separating highlights from midtones. + /// + /// Pixels above this value are considered highlights. This value should be + /// higher than `shadows_threshold`. + /// + /// Range: `0.0` to `1.0` + /// The default value is 0.75 + pub highlights_threshold: f32, + + /// The size of the film grain particles. + /// + /// The default value is 1.0 + pub grain_size: f32, + + /// A user-provided texture to use for the film grain. + /// + /// By default (if None), a default 1x1 placeholder texture is used. + /// This signals the shader to generate film grain procedurally instead of sampling from a texture. + /// + /// Note: User should not pass a 1x1 texture manually, + /// as it will be treated as invalid and trigger the same procedural fallback. + pub texture: Option>, +} + +impl Default for FilmGrain { + fn default() -> Self { + Self { + intensity: DEFAULT_FILM_GRAIN_INTENSITY, + shadows_intensity: DEFAULT_FILM_GRAIN_SHADOWS_INTENSITY, + midtones_intensity: DEFAULT_FILM_GRAIN_MIDTONES_INTENSITY, + highlights_intensity: DEFAULT_FILM_GRAIN_HIGHLIGHTS_INTENSITY, + shadows_threshold: DEFAULT_FILM_GRAIN_SHADOWS_THRESHOLD, + highlights_threshold: DEFAULT_FILM_GRAIN_HIGHLIGHT_THRESHOLD, + grain_size: DEFAULT_FILM_GRAIN_GRAIN_SIZE, + texture: None, + } + } +} + +impl ExtractComponent for FilmGrain { + type QueryData = Read; + + type QueryFilter = With; + + type Out = FilmGrain; + + fn extract_component(film_grain: QueryItem<'_, '_, Self::QueryData>) -> Option { + if film_grain.intensity > 0.0 { + Some(film_grain.clone()) + } else { + None + } + } +} + +#[derive(ShaderType, Default)] +pub struct FilmGrainUniform { + /// The overall intensity of the film grain effect. + pub(super) intensity: f32, + /// The intensity of the film grain in shadow areas. + pub(super) shadows_intensity: f32, + /// The intensity of the film grain in midtone areas. + pub(super) midtones_intensity: f32, + /// The intensity of the film grain in highlight areas. + pub(super) highlights_intensity: f32, + /// The threshold separating shadows from midtones. + pub(super) shadows_threshold: f32, + /// The threshold separating highlights from midtones. + pub(super) highlights_threshold: f32, + /// The size of the film grain particles. + pub(super) grain_size: f32, + /// The current frame number, used to animate the noise pattern over time. + pub(super) frame: u32, +} diff --git a/crates/bevy_post_process/src/effect_stack/film_grain.wgsl b/crates/bevy_post_process/src/effect_stack/film_grain.wgsl new file mode 100644 index 0000000000000..85b606b51929d --- /dev/null +++ b/crates/bevy_post_process/src/effect_stack/film_grain.wgsl @@ -0,0 +1,116 @@ +// The film grain postprocessing effect. + +#define_import_path bevy_post_process::effect_stack::film_grain + +#import bevy_post_process::effect_stack::bindings::{source_texture, film_grain_texture, film_grain_sampler, film_grain_settings, FilmGrainSettings, EPSILON} + +// Reference: https://www.shadertoy.com/view/4tXyWN +fn hash(p: vec2) -> f32 { + var p_mut = p; + p_mut *= vec2(73333, 7777); + p_mut.x ^= 3333777777u >> (p_mut.x >> 28u); + p_mut.y ^= 3333777777u >> (p_mut.y >> 28u); + + let n = p_mut.x * p_mut.y; + let h = n ^ (n >> 15u); + return f32(h) * (1.0/4294967296.0); +} + +fn get_rgb_noise(grid_x: i32, grid_y: i32, frame_offset: u32) -> vec3 { + let coord = vec2(vec2(grid_x, grid_y)) + frame_offset; + let hash_val = hash(coord); + let r = hash_val; + // Derive uncorrelated random values for G and B channels from the + // single base hash. This avoids the computational cost of calling + // hash() multiple times. + let g = fract(hash_val * 12.9898 + 78.233); + let b = fract(hash_val * 63.346 + 45.543); + return vec3(r, g, b); +} + +fn get_procedural_grain(uv: vec2, screen_dimensions: vec2, grain_size: f32) -> vec3 { + // Convert UV to pixel coordinates, then scale down by grain size. + // This creates a virtual grid where each cell represents a grain chunk. + let pixel_coord = uv * vec2(screen_dimensions); + let scaled_coord = pixel_coord / grain_size; + + // Calculate a frame offset based on screen resolution to animate the grain. + let frame_offset = film_grain_settings.frame * screen_dimensions.x * screen_dimensions.y; + + // Split coordinates into integer (grid cell) and fractional (intra-cell) parts. + let i = floor(scaled_coord); + let f = fract(scaled_coord); + + // Sample noise at the 4 corners of the current grid cell. + let v00 = get_rgb_noise(i32(i.x), i32(i.y), frame_offset); + let v10 = get_rgb_noise(i32(i.x) + 1, i32(i.y), frame_offset); + let v01 = get_rgb_noise(i32(i.x), i32(i.y) + 1, frame_offset); + let v11 = get_rgb_noise(i32(i.x) + 1, i32(i.y) + 1, frame_offset); + + let u = smoothstep(0.0, 1.0, f.x); + let v = smoothstep(0.0, 1.0, f.y); + + let mix_x1 = mix(v00, v10, u); + let mix_x2 = mix(v01, v11, v); + + let r = mix(mix_x1.r, mix_x2.r, f.y); + let g = mix(mix_x1.g, mix_x2.g, f.y); + let b = mix(mix_x1.b, mix_x2.b, f.y); + + return vec3(r, g, b); +} + +fn get_grain_sample(uv: vec2, grain_size: f32) -> vec3 { + let screen_dimensions = textureDimensions(source_texture); + let grain_texture_size = vec2(textureDimensions(film_grain_texture)); + + if (grain_texture_size.x < 2 || grain_texture_size.y < 2) { + return get_procedural_grain(uv, screen_dimensions, grain_size); + } + + let tiling = vec2(screen_dimensions) / (grain_texture_size * grain_size); + + // Generate a random offset for each frame to prevent static patterns. + // We use 101u and 211u as distinct seeds for X and Y axes. + let rand_x = hash(vec2(film_grain_settings.frame, 101u)); + let rand_y = hash(vec2(film_grain_settings.frame, 211u)); + let random_offset = vec2(rand_x, rand_y); + + let centered_uv = (uv - 0.5) * tiling; + let final_uv = centered_uv + random_offset + 0.5; + + return textureSample(film_grain_texture, film_grain_sampler, final_uv).rgb; +} + +fn film_grain(uv: vec2, color: vec3) -> vec3 { + if (film_grain_settings.intensity < EPSILON) { + return color; + } + + let intensity = saturate(film_grain_settings.intensity); + let shadows_intensity = saturate(film_grain_settings.shadows_intensity); + let midtones_intensity = saturate(film_grain_settings.midtones_intensity); + let highlights_intensity = saturate(film_grain_settings.highlights_intensity); + let shadows_threshold = saturate(film_grain_settings.shadows_threshold); + let highlights_threshold = saturate(film_grain_settings.highlights_threshold); + let grain_size = max(film_grain_settings.grain_size, EPSILON); + + // Sample the grain texture (or generate procedural noise). + let grain_color = get_grain_sample(uv, grain_size); + + // Calculate perceptual luminance using the Rec.709 standard. + let luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); + // Calculate blending factors for shadows, midtones, and highlights. + let shadow_factor = 1.0 - smoothstep(shadows_threshold - 0.1, shadows_threshold + 0.1, luminance); + let highlight_factor = smoothstep(highlights_threshold - 0.1, highlights_threshold + 0.1, luminance); + let midtone_factor = 1.0 - shadow_factor - highlight_factor; + + let local_intensity = + (shadow_factor * shadows_intensity) + + (midtone_factor * midtones_intensity) + + (highlight_factor * highlights_intensity); + + let strength = local_intensity * intensity; + let overlay_grain = grain_color * strength; + return color.rgb + overlay_grain; +} diff --git a/crates/bevy_post_process/src/effect_stack/mod.rs b/crates/bevy_post_process/src/effect_stack/mod.rs index edb2a50e6731b..7e8d9fa9a6c92 100644 --- a/crates/bevy_post_process/src/effect_stack/mod.rs +++ b/crates/bevy_post_process/src/effect_stack/mod.rs @@ -3,23 +3,27 @@ //! Includes: //! //! - Chromatic Aberration +//! - Film Grain //! - Vignette mod chromatic_aberration; +mod film_grain; mod vignette; -use bevy_color::ColorToComponents; pub use chromatic_aberration::{ChromaticAberration, ChromaticAberrationUniform}; +pub use film_grain::{FilmGrain, FilmGrainUniform}; pub use vignette::{Vignette, VignetteUniform}; -use crate::effect_stack::chromatic_aberration::{ - DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA, +use crate::effect_stack::{ + chromatic_aberration::{DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA}, + film_grain::{DefaultFilmGrainTexture, DEFAULT_FILM_GRAIN_TEXTURE_DATA}, }; use bevy_app::{App, Plugin}; use bevy_asset::{ embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, }; +use bevy_color::ColorToComponents; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -27,7 +31,7 @@ use bevy_ecs::{ query::{AnyOf, Or, QueryItem, With}, resource::Resource, schedule::IntoScheduleConfigs as _, - system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut}, world::World, }; use bevy_image::{BevyDefault, Image}; @@ -40,7 +44,7 @@ use bevy_render::{ }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, - BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, + AddressMode, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, MipmapFilterMode, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, @@ -67,6 +71,7 @@ use bevy_core_pipeline::{ /// Includes: /// /// - Chromatic Aberration +/// - Film Grain /// - Vignette #[derive(Default)] pub struct EffectStackPlugin; @@ -82,6 +87,8 @@ pub struct PostProcessingPipeline { source_sampler: Sampler, /// Specifies how to sample the chromatic aberration gradient. chromatic_aberration_lut_sampler: Sampler, + /// Specifies how to sample the film grain texture. + film_grain_sampler: Sampler, /// The asset handle for the fullscreen vertex shader. fullscreen_shader: FullscreenShader, /// The fragment shader asset handle. @@ -109,6 +116,7 @@ pub struct PostProcessingPipelineId(CachedRenderPipelineId); pub struct PostProcessingUniformBuffers { chromatic_aberration: DynamicUniformBuffer, vignette: DynamicUniformBuffer, + film_grain: DynamicUniformBuffer, } /// A component, part of the render world, that stores the appropriate byte @@ -118,6 +126,7 @@ pub struct PostProcessingUniformBuffers { pub struct PostProcessingUniformBufferOffsets { chromatic_aberration: u32, vignette: u32, + film_grain: u32, } /// The render node that runs the built-in postprocessing stack. @@ -126,7 +135,9 @@ pub struct PostProcessingNode; impl Plugin for EffectStackPlugin { fn build(&self, app: &mut App) { + load_shader_library!(app, "bindings.wgsl"); load_shader_library!(app, "chromatic_aberration.wgsl"); + load_shader_library!(app, "film_grain.wgsl"); load_shader_library!(app, "vignette.wgsl"); embedded_asset!(app, "post_process.wgsl"); @@ -145,8 +156,21 @@ impl Plugin for EffectStackPlugin { RenderAssetUsages::RENDER_WORLD, )); + let default_film_grain_texture = assets.add(Image::new( + Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + DEFAULT_FILM_GRAIN_TEXTURE_DATA.to_vec(), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + )); + app.add_plugins(ExtractComponentPlugin::::default()) - .add_plugins(ExtractComponentPlugin::::default()); + .add_plugins(ExtractComponentPlugin::::default()) + .add_plugins(ExtractComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -154,6 +178,7 @@ impl Plugin for EffectStackPlugin { render_app .insert_resource(DefaultChromaticAberrationLut(default_lut)) + .insert_resource(DefaultFilmGrainTexture(default_film_grain_texture)) .init_resource::>() .init_resource::() .add_systems(RenderStartup, init_post_processing_pipeline) @@ -212,6 +237,12 @@ pub fn init_post_processing_pipeline( uniform_buffer::(true), // Vignette settings: uniform_buffer::(true), + // Film grain texture. + texture_2d(TextureSampleType::Float { filterable: true }), + // Film grain texture sampler. + sampler(SamplerBindingType::Filtering), + // Film grain settings: + uniform_buffer::(true), ), ), ); @@ -233,10 +264,21 @@ pub fn init_post_processing_pipeline( ..default() }); + let film_grain_sampler = render_device.create_sampler(&SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + address_mode_w: AddressMode::Repeat, + mipmap_filter: MipmapFilterMode::Linear, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + commands.insert_resource(PostProcessingPipeline { bind_group_layout, source_sampler, chromatic_aberration_lut_sampler, + film_grain_sampler, fullscreen_shader: fullscreen_shader.clone(), fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"), }); @@ -268,7 +310,7 @@ impl ViewNode for PostProcessingNode { type ViewQuery = ( Read, Read, - AnyOf<(Read, Read)>, + AnyOf<(Read, Read, Read)>, Read, ); @@ -276,16 +318,18 @@ impl ViewNode for PostProcessingNode { &self, _: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (view_target, pipeline_id, post_effects, post_processing_uniform_buffer_offsets): QueryItem< - 'w, - '_, - Self::ViewQuery, - >, + ( + view_target, + pipeline_id, + (maybe_chromatic_aberration, maybe_vignette, maybe_film_grain), + post_processing_uniform_buffer_offsets, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { - let (maybe_chromatic_aberration, maybe_vignette) = post_effects; - - if maybe_chromatic_aberration.is_none() && maybe_vignette.is_none() { + if maybe_chromatic_aberration.is_none() + && maybe_vignette.is_none() + && maybe_film_grain.is_none() + { return Ok(()); } @@ -294,6 +338,7 @@ impl ViewNode for PostProcessingNode { let post_processing_uniform_buffers = world.resource::(); let gpu_image_assets = world.resource::>(); let default_lut = world.resource::(); + let default_film_grain_texture = world.resource::(); // We need a render pipeline to be prepared. let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else { @@ -308,6 +353,14 @@ impl ViewNode for PostProcessingNode { return Ok(()); }; + let Some(film_grain_texture) = gpu_image_assets.get( + maybe_film_grain + .and_then(|fg| fg.texture.as_ref()) + .unwrap_or(&default_film_grain_texture.0), + ) else { + return Ok(()); + }; + // We need the postprocessing settings to be uploaded to the GPU. let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers .chromatic_aberration @@ -322,6 +375,12 @@ impl ViewNode for PostProcessingNode { return Ok(()); }; + let Some(film_grain_uniform_buffer_binding) = + post_processing_uniform_buffers.film_grain.binding() + else { + return Ok(()); + }; + let diagnostics = render_context.diagnostic_recorder(); // Use the [`PostProcessWrite`] infrastructure, since this is a @@ -352,6 +411,9 @@ impl ViewNode for PostProcessingNode { &post_processing_pipeline.chromatic_aberration_lut_sampler, chromatic_aberration_uniform_buffer_binding, vignette_uniform_buffer_binding, + &film_grain_texture.texture_view, + &post_processing_pipeline.film_grain_sampler, + film_grain_uniform_buffer_binding, )), ); @@ -367,6 +429,7 @@ impl ViewNode for PostProcessingNode { &[ post_processing_uniform_buffer_offsets.chromatic_aberration, post_processing_uniform_buffer_offsets.vignette, + post_processing_uniform_buffer_offsets.film_grain, ], ); render_pass.draw(0..3, 0..1); @@ -383,7 +446,10 @@ pub fn prepare_post_processing_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, post_processing_pipeline: Res, - views: Query<(Entity, &ExtractedView), Or<(With, With)>>, + views: Query< + (Entity, &ExtractedView), + Or<(With, With, With)>, + >, ) { for (entity, view) in views.iter() { let pipeline_id = pipelines.specialize( @@ -411,16 +477,26 @@ pub fn prepare_post_processing_uniforms( mut post_processing_uniform_buffers: ResMut, render_device: Res, render_queue: Res, + mut frame_count: Local, mut views: Query< - (Entity, Option<&ChromaticAberration>, Option<&Vignette>), - Or<(With, With)>, + ( + Entity, + Option<&ChromaticAberration>, + Option<&Vignette>, + Option<&FilmGrain>, + ), + Or<(With, With, With)>, >, ) { + *frame_count += 1; post_processing_uniform_buffers.chromatic_aberration.clear(); post_processing_uniform_buffers.vignette.clear(); + post_processing_uniform_buffers.film_grain.clear(); // Gather up all the postprocessing settings. - for (view_entity, maybe_chromatic_aberration, maybe_vignette) in views.iter_mut() { + for (view_entity, maybe_chromatic_aberration, maybe_vignette, maybe_film_grain) in + views.iter_mut() + { let chromatic_aberration_uniform_buffer_offset = if let Some(chromatic_aberration) = maybe_chromatic_aberration { post_processing_uniform_buffers.chromatic_aberration.push( @@ -456,11 +532,31 @@ pub fn prepare_post_processing_uniforms( .push(&VignetteUniform::default()) }; + let film_grain_uniform_buffer_offset = if let Some(film_grain) = maybe_film_grain { + post_processing_uniform_buffers + .film_grain + .push(&FilmGrainUniform { + intensity: film_grain.intensity, + shadows_intensity: film_grain.shadows_intensity, + midtones_intensity: film_grain.midtones_intensity, + highlights_intensity: film_grain.highlights_intensity, + shadows_threshold: film_grain.shadows_threshold, + highlights_threshold: film_grain.highlights_threshold, + grain_size: film_grain.grain_size, + frame: *frame_count, + }) + } else { + post_processing_uniform_buffers + .film_grain + .push(&FilmGrainUniform::default()) + }; + commands .entity(view_entity) .insert(PostProcessingUniformBufferOffsets { chromatic_aberration: chromatic_aberration_uniform_buffer_offset, vignette: vignette_uniform_buffer_offset, + film_grain: film_grain_uniform_buffer_offset, }); } @@ -471,4 +567,7 @@ pub fn prepare_post_processing_uniforms( post_processing_uniform_buffers .vignette .write_buffer(&render_device, &render_queue); + post_processing_uniform_buffers + .film_grain + .write_buffer(&render_device, &render_queue); } diff --git a/crates/bevy_post_process/src/effect_stack/post_process.wgsl b/crates/bevy_post_process/src/effect_stack/post_process.wgsl index c81f25024459e..ac9117266cee4 100644 --- a/crates/bevy_post_process/src/effect_stack/post_process.wgsl +++ b/crates/bevy_post_process/src/effect_stack/post_process.wgsl @@ -1,11 +1,13 @@ -// Miscellaneous postprocessing effects, currently just chromatic aberration. +// Miscellaneous postprocessing effects. #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput #import bevy_post_process::effect_stack::chromatic_aberration::chromatic_aberration +#import bevy_post_process::effect_stack::film_grain::film_grain #import bevy_post_process::effect_stack::vignette::vignette @fragment fn fragment_main(in: FullscreenVertexOutput) -> @location(0) vec4 { - let color = chromatic_aberration(in.uv); - return vec4(vignette(in.uv, color), 1.0); + let ca_color = chromatic_aberration(in.uv); + let v_color = vignette(in.uv, ca_color); + return vec4(film_grain(in.uv, v_color), 1.0); } diff --git a/crates/bevy_post_process/src/effect_stack/vignette.wgsl b/crates/bevy_post_process/src/effect_stack/vignette.wgsl index 35301937ee201..033dc4eacfe26 100644 --- a/crates/bevy_post_process/src/effect_stack/vignette.wgsl +++ b/crates/bevy_post_process/src/effect_stack/vignette.wgsl @@ -2,25 +2,7 @@ #define_import_path bevy_post_process::effect_stack::vignette -#import bevy_post_process::effect_stack::chromatic_aberration::source_texture - -// See `bevy_post_process::effect_stack::Vignette` for more -// information on these fields. -struct VignetteSettings { - intensity: f32, - radius: f32, - smoothness: f32, - roundness: f32, - center: vec2, - edge_compensation: f32, - unused: u32, - color: vec4 -} - -const EPSILON: f32 = 1.19209290e-07; - -// The settings supplied by the developer. -@group(0) @binding(5) var vignette_settings: VignetteSettings; +#import bevy_post_process::effect_stack::bindings::{source_texture, vignette_settings, VignetteSettings, EPSILON} fn vignette(uv: vec2, color: vec3) -> vec3 { if (vignette_settings.intensity < EPSILON) { diff --git a/examples/3d/post_processing.rs b/examples/3d/post_processing.rs index 25873b6d1df96..30f5021116615 100644 --- a/examples/3d/post_processing.rs +++ b/examples/3d/post_processing.rs @@ -3,6 +3,7 @@ //! Includes: //! //! - Chromatic Aberration +//! - Film Grain //! - Vignette use std::f32::consts::PI; @@ -10,34 +11,43 @@ use std::f32::consts::PI; use bevy::{ camera::Hdr, light::CascadeShadowConfigBuilder, - post_process::effect_stack::{ChromaticAberration, Vignette}, + post_process::effect_stack::{ChromaticAberration, FilmGrain, Vignette}, prelude::*, }; /// The number of units per frame to add to or subtract from intensity when the /// arrow keys are held. -const ADJUSTMENT_SPEED: f32 = 0.005; +const ADJUSTMENT_SPEED: f32 = 0.002; /// The maximum supported chromatic aberration intensity level. const MAX_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.4; +/// The maximum supported film grain intensity level. +const MAX_FILM_GRAIN_INTENSITY: f32 = 0.2; + /// The settings that the user can control. #[derive(Resource)] struct AppSettings { + /// Control visibility of UI. + ui_visible: bool, /// The index of the currently selected UI item. selected: usize, /// The intensity of the chromatic aberration effect. chromatic_aberration_intensity: f32, /// The intensity of the vignette effect. vignette_intensity: f32, - /// The radius of the vignette. + /// The radius of the vignette effect. vignette_radius: f32, - /// The smoothness of the vignette. + /// The smoothness of the vignette effect. vignette_smoothness: f32, - /// The roundness of the vignette. + /// The roundness of the vignette effect. vignette_roundness: f32, - /// The edge compensation of the vignette. + /// The edge compensation of the vignette effect. vignette_edge_compensation: f32, + /// The intensity of the film grain effect. + film_grain_intensity: f32, + /// The grain size of the film grain effect. + film_grain_grain_size: f32, } /// The entry point. @@ -92,6 +102,8 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { ChromaticAberration::default(), // Include the `Vignette` component. Vignette::default(), + // Include the `FilmGrain` component. + FilmGrain::default(), )); } @@ -147,23 +159,31 @@ fn spawn_text(commands: &mut Commands) { impl Default for AppSettings { fn default() -> Self { let vignette_default = Vignette::default(); + let film_grain = FilmGrain::default(); Self { - selected: 0, + ui_visible: true, + selected: 6, chromatic_aberration_intensity: ChromaticAberration::default().intensity, vignette_intensity: vignette_default.intensity, vignette_radius: vignette_default.radius, vignette_smoothness: vignette_default.smoothness, vignette_roundness: vignette_default.roundness, vignette_edge_compensation: vignette_default.edge_compensation, + film_grain_intensity: film_grain.intensity, + film_grain_grain_size: film_grain.grain_size, } } } /// Handles requests from the user to change the chromatic aberration intensity. fn handle_keyboard_input(mut app_settings: ResMut, input: Res>) { + if input.just_pressed(KeyCode::KeyH) { + app_settings.ui_visible = !app_settings.ui_visible; + } + if input.just_pressed(KeyCode::ArrowUp) && app_settings.selected > 0 { app_settings.selected -= 1; - } else if input.just_pressed(KeyCode::ArrowDown) && app_settings.selected < 5 { + } else if input.just_pressed(KeyCode::ArrowDown) && app_settings.selected < 7 { app_settings.selected += 1; } @@ -198,6 +218,14 @@ fn handle_keyboard_input(mut app_settings: ResMut, input: Res { + app_settings.film_grain_intensity = + (app_settings.film_grain_intensity + delta).clamp(0.0, MAX_FILM_GRAIN_INTENSITY); + } + 7 => { + app_settings.film_grain_grain_size = + (app_settings.film_grain_grain_size + delta).max(0.01); + } _ => {} } } @@ -206,6 +234,7 @@ fn handle_keyboard_input(mut app_settings: ResMut, input: Res, mut vignette: Query<&mut Vignette>, + mut film_grain: Query<&mut FilmGrain>, app_settings: Res, ) { let intensity = app_settings.chromatic_aberration_intensity; @@ -231,41 +260,60 @@ fn update_chromatic_aberration_settings( vignette.roundness = app_settings.vignette_roundness; vignette.edge_compensation = app_settings.vignette_edge_compensation; } + + for mut film_grain in &mut film_grain { + film_grain.intensity = app_settings.film_grain_intensity; + film_grain.grain_size = app_settings.film_grain_grain_size; + } } /// Updates the help text at the bottom of the screen to reflect the current /// [`AppSettings`]. fn update_help_text(mut text: Single<&mut Text>, app_settings: Res) { text.clear(); - //let vignette_mode_list = ["Cosine Fourth Law", "Higher-order Powers", "Smoothstep"]; - let text_list = [ - format!( - "Chromatic aberration intensity: {:.2}\n", - app_settings.chromatic_aberration_intensity - ), - format!( - "Vignette intensity: {:.2}\n", - app_settings.vignette_intensity - ), - format!("Vignette radius: {:.2}\n", app_settings.vignette_radius), - format!( - "Vignette smoothness: {:.2}\n", - app_settings.vignette_smoothness - ), - format!( - "Vignette roundness: {:.2}\n", - app_settings.vignette_roundness - ), - format!( - "Vignette edge_compensation: {:.2}\n", - app_settings.vignette_edge_compensation - ), - ]; - for (i, val) in text_list.iter().enumerate() { - if i == app_settings.selected { - text.push_str("> "); + + if app_settings.ui_visible { + let text_list = [ + format!( + "Chromatic aberration intensity: {:.2}\n", + app_settings.chromatic_aberration_intensity + ), + format!( + "Vignette intensity: {:.2}\n", + app_settings.vignette_intensity + ), + format!("Vignette radius: {:.2}\n", app_settings.vignette_radius), + format!( + "Vignette smoothness: {:.2}\n", + app_settings.vignette_smoothness + ), + format!( + "Vignette roundness: {:.2}\n", + app_settings.vignette_roundness + ), + format!( + "Vignette edge_compensation: {:.2}\n", + app_settings.vignette_edge_compensation + ), + format!( + "Film grain intensity: {:.2}\n", + app_settings.film_grain_intensity + ), + format!( + "Film grain grain size: {:.2}\n", + app_settings.film_grain_grain_size + ), + ]; + + for (i, val) in text_list.iter().enumerate() { + if i == app_settings.selected { + text.push_str("> "); + } + text.push_str(val); } - text.push_str(val); + + text.push_str("\n(Press Up or Down to select)\n(Press Left or Right to change)\n"); } - text.push_str("\n(Press Up or Down to select)\n(Press Left or Right to change)"); + + text.push_str("(Press H to toggle UI)"); }