Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions crates/bevy_post_process/src/effect_stack/bindings.wgsl
Original file line number Diff line number Diff line change
@@ -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<f32>;
// 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<f32>;
// 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<uniform> chromatic_aberration_settings: ChromaticAberrationSettings;
// The settings were supplied by the developer.
@group(0) @binding(5) var<uniform> vignette_settings: VignetteSettings;
// The film grain texture.
@group(0) @binding(6) var film_grain_texture: texture_2d<f32>;
// 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<uniform> 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<f32>,
edge_compensation: f32,
unused: u32,
color: vec4<f32>
}

// 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32>;
// 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<f32>;
// 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<uniform> 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<f32>) -> vec3<f32> {
// Radial chromatic aberration implemented using the *Inside* technique:
Expand Down
148 changes: 148 additions & 0 deletions crates/bevy_post_process/src/effect_stack/film_grain.rs
Original file line number Diff line number Diff line change
@@ -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<Image>);

#[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<Handle<Image>>,
}

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<FilmGrain>;

type QueryFilter = With<Camera>;

type Out = FilmGrain;

fn extract_component(film_grain: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
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,
}
116 changes: 116 additions & 0 deletions crates/bevy_post_process/src/effect_stack/film_grain.wgsl
Original file line number Diff line number Diff line change
@@ -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<u32>) -> f32 {
var p_mut = p;
p_mut *= vec2<u32>(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<f32> {
let coord = vec2<u32>(vec2<i32>(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<f32>(r, g, b);
}

fn get_procedural_grain(uv: vec2<f32>, screen_dimensions: vec2<u32>, grain_size: f32) -> vec3<f32> {
// 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<f32>(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<f32>(r, g, b);
}

fn get_grain_sample(uv: vec2<f32>, grain_size: f32) -> vec3<f32> {
let screen_dimensions = textureDimensions(source_texture);
let grain_texture_size = vec2<f32>(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<f32>(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<u32>(film_grain_settings.frame, 101u));
let rand_y = hash(vec2<u32>(film_grain_settings.frame, 211u));
let random_offset = vec2<f32>(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<f32>, color: vec3<f32>) -> vec3<f32> {
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<f32>(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;
}
Loading