From 13deb3ed76070353acc7c12a85a96a82a785d854 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Mon, 6 Jan 2025 10:43:21 -0800 Subject: [PATCH] Anamorphic Bloom (#17096) https://github.com/user-attachments/assets/e2de3d20-4246-4eba-a0a7-8469a468dddb The _JJ Abrahams_ https://github.com/user-attachments/assets/2dce3df9-665b-46ff-b687-e7cb54364f30 The _Cyberfunk 2025_ image # Objective - Add the ability to scale bloom for artistic control, and to mimic anamorphic blurs. ## Solution - Add a scale factor in bloom settings, and plumb this to the shader. ## Testing - Added runtime-tweak-able setting to the `bloom_3d`/`bloom_2d ` example --- ## Showcase ![image](https://github.com/user-attachments/assets/bb44dae4-52bb-4981-a77f-aaa1ec83f5d6) - Added `scale` parameter to `Bloom` to improve artistic control and enable anamorphic bloom. --- .../bevy_core_pipeline/src/bloom/bloom.wgsl | 47 +++++++++++++++++-- .../src/bloom/downsampling_pipeline.rs | 11 ++++- .../bevy_core_pipeline/src/bloom/settings.rs | 26 ++++++---- examples/2d/bloom_2d.rs | 13 ++++- examples/3d/bloom_3d.rs | 39 +++++++++------ 5 files changed, 105 insertions(+), 31 deletions(-) diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl index 0b10333192..aa4a2f94c4 100644 --- a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -9,8 +9,8 @@ struct BloomUniforms { threshold_precomputations: vec4, viewport: vec4, + scale: vec2, aspect: f32, - uv_offset: f32 }; @group(0) @binding(0) var input_texture: texture_2d; @@ -51,6 +51,14 @@ fn karis_average(color: vec3) -> f32 { // [COD] slide 153 fn sample_input_13_tap(uv: vec2) -> vec3 { +#ifdef UNIFORM_SCALE + // This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be + // expressed with constant offsets. + // + // It's possible that this isn't meaningfully faster than the "slow" path. However, because it + // is hard to test performance on all platforms, and uniform bloom is the most common case, this + // path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero, + // cost to maintainability, but it does help me sleep at night. let a = textureSample(input_texture, s, uv, vec2(-2, 2)).rgb; let b = textureSample(input_texture, s, uv, vec2(0, 2)).rgb; let c = textureSample(input_texture, s, uv, vec2(2, 2)).rgb; @@ -64,6 +72,35 @@ fn sample_input_13_tap(uv: vec2) -> vec3 { let k = textureSample(input_texture, s, uv, vec2(1, 1)).rgb; let l = textureSample(input_texture, s, uv, vec2(-1, -1)).rgb; let m = textureSample(input_texture, s, uv, vec2(1, -1)).rgb; +#else + // This is the flexible, but potentially slower, path for non-uniform sampling. Because the + // sample is not a constant, and it can fall outside of the limits imposed on constant sample + // offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the + // texture. + // + // It isn't clear if this is meaningfully slower than using the offset syntax, the spec doesn't + // mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset + // syntax uses a const-expr implies that it allows some compiler optimizations - maybe more + // impactful on mobile? + let scale = uniforms.scale; + let ps = scale / vec2(textureDimensions(input_texture)); + let pl = 2.0 * ps; + let ns = -1.0 * ps; + let nl = -2.0 * ps; + let a = textureSample(input_texture, s, uv + vec2(nl.x, pl.y)).rgb; + let b = textureSample(input_texture, s, uv + vec2(0.00, pl.y)).rgb; + let c = textureSample(input_texture, s, uv + vec2(pl.x, pl.y)).rgb; + let d = textureSample(input_texture, s, uv + vec2(nl.x, 0.00)).rgb; + let e = textureSample(input_texture, s, uv).rgb; + let f = textureSample(input_texture, s, uv + vec2(pl.x, 0.00)).rgb; + let g = textureSample(input_texture, s, uv + vec2(nl.x, nl.y)).rgb; + let h = textureSample(input_texture, s, uv + vec2(0.00, nl.y)).rgb; + let i = textureSample(input_texture, s, uv + vec2(pl.x, nl.y)).rgb; + let j = textureSample(input_texture, s, uv + vec2(ns.x, ps.y)).rgb; + let k = textureSample(input_texture, s, uv + vec2(ps.x, ps.y)).rgb; + let l = textureSample(input_texture, s, uv + vec2(ns.x, ns.y)).rgb; + let m = textureSample(input_texture, s, uv + vec2(ps.x, ns.y)).rgb; +#endif #ifdef FIRST_DOWNSAMPLE // [COD] slide 168 @@ -95,9 +132,11 @@ fn sample_input_13_tap(uv: vec2) -> vec3 { // [COD] slide 162 fn sample_input_3x3_tent(uv: vec2) -> vec3 { - // UV offsets configured from uniforms. - let x = uniforms.uv_offset / uniforms.aspect; - let y = uniforms.uv_offset; + // While this is probably technically incorrect, it makes nonuniform bloom smoother, without + // having any impact on uniform bloom, which simply evaluates to 1.0 here. + let frag_size = uniforms.scale / vec2(textureDimensions(input_texture)); + let x = frag_size.x; + let y = frag_size.y; let a = textureSample(input_texture, s, vec2(uv.x - x, uv.y + y)).rgb; let b = textureSample(input_texture, s, vec2(uv.x, uv.y + y)).rgb; diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index e3efe5cad8..642791568a 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; -use bevy_math::Vec4; +use bevy_math::{Vec2, Vec4}; use bevy_render::{ render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -31,6 +31,7 @@ pub struct BloomDownsamplingPipeline { pub struct BloomDownsamplingPipelineKeys { prefilter: bool, first_downsample: bool, + uniform_scale: bool, } /// The uniform struct extracted from [`Bloom`] attached to a Camera. @@ -40,8 +41,8 @@ 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 uv_offset: f32, } impl FromWorld for BloomDownsamplingPipeline { @@ -102,6 +103,10 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { shader_defs.push("USE_THRESHOLD".into()); } + if key.uniform_scale { + shader_defs.push("UNIFORM_SCALE".into()); + } + RenderPipelineDescriptor { label: Some( if key.first_downsample { @@ -148,6 +153,7 @@ pub fn prepare_downsampling_pipeline( BloomDownsamplingPipelineKeys { prefilter, first_downsample: false, + uniform_scale: bloom.scale == Vec2::ONE, }, ); @@ -157,6 +163,7 @@ pub fn prepare_downsampling_pipeline( BloomDownsamplingPipelineKeys { prefilter, first_downsample: true, + uniform_scale: bloom.scale == Vec2::ONE, }, ); diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 7816eece51..2e22875a35 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -1,6 +1,6 @@ use super::downsampling_pipeline::BloomUniforms; use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent}; -use bevy_math::{AspectRatio, URect, UVec4, Vec4}; +use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; @@ -113,14 +113,14 @@ pub struct Bloom { /// Only tweak if you are seeing visual artifacts. pub max_mip_dimension: u32, - /// UV offset for bloom shader. Ideally close to 2.0 / `max_mip_dimension`. - /// Only tweak if you are seeing visual artifacts. - pub uv_offset: f32, + /// Amount to stretch the bloom on each axis. Artistic control, can be used to emulate + /// anamorphic blur by using a large x-value. For large values, you may need to increase + /// [`Bloom::max_mip_dimension`] to reduce sampling artifacts. + pub scale: Vec2, } impl Bloom { const DEFAULT_MAX_MIP_DIMENSION: u32 = 512; - const DEFAULT_UV_OFFSET: f32 = 0.004; /// The default bloom preset. /// @@ -136,7 +136,15 @@ impl Bloom { }, composite_mode: BloomCompositeMode::EnergyConserving, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, + }; + + /// Emulates the look of stylized anamorphic bloom, stretched horizontally. + pub const ANAMORPHIC: Self = Self { + // The larger scale necessitates a larger resolution to reduce artifacts: + max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION * 2, + scale: Vec2::new(4.0, 1.0), + ..Self::NATURAL }; /// A preset that's similar to how older games did bloom. @@ -151,7 +159,7 @@ impl Bloom { }, composite_mode: BloomCompositeMode::Additive, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, }; /// A preset that applies a very strong bloom, and blurs the whole screen. @@ -166,7 +174,7 @@ impl Bloom { }, composite_mode: BloomCompositeMode::EnergyConserving, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, }; } @@ -240,7 +248,7 @@ impl ExtractComponent for Bloom { aspect: AspectRatio::try_from_pixels(size.x, size.y) .expect("Valid screen size values for Bloom settings") .ratio(), - uv_offset: bloom.uv_offset, + scale: bloom.scale, }; Some((bloom.clone(), uniform)) diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index d4c2db3698..ebb20dceaf 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -3,7 +3,7 @@ use bevy::{ core_pipeline::{ bloom::{Bloom, BloomCompositeMode}, - tonemapping::Tonemapping, + tonemapping::{DebandDither, Tonemapping}, }, prelude::*, }; @@ -26,10 +26,12 @@ fn setup( Camera2d, Camera { hdr: true, // 1. HDR is required for bloom + clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended Bloom::default(), // 3. Enable bloom for the camera + DebandDither::Enabled, // Optional: bloom causes gradients which cause banding )); // Sprite @@ -107,6 +109,7 @@ fn update_bloom_settings( "(U/J) Threshold softness: {}\n", bloom.prefilter.threshold_softness )); + text.push_str(&format!("(I/K) Horizontal Scale: {}\n", bloom.scale.x)); if keycode.just_pressed(KeyCode::Space) { commands.entity(entity).remove::(); @@ -169,6 +172,14 @@ fn update_bloom_settings( bloom.prefilter.threshold_softness += dt / 10.0; } bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0); + + if keycode.pressed(KeyCode::KeyK) { + bloom.scale.x -= dt * 2.0; + } + if keycode.pressed(KeyCode::KeyI) { + bloom.scale.x += dt * 2.0; + } + bloom.scale.x = bloom.scale.x.clamp(0.0, 16.0); } (entity, None) => { diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index fa58400eda..fd87928283 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -1,7 +1,6 @@ //! Illustrates bloom post-processing using HDR and emissive materials. use bevy::{ - color::palettes::basic::GRAY, core_pipeline::{ bloom::{Bloom, BloomCompositeMode}, tonemapping::Tonemapping, @@ -31,32 +30,32 @@ fn setup_scene( Camera3d::default(), Camera { hdr: true, // 1. HDR is required for bloom + clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - // 3. Enable bloom for the camera - Bloom::NATURAL, + Bloom::NATURAL, // 3. Enable bloom for the camera )); let material_emissive1 = materials.add(StandardMaterial { - emissive: LinearRgba::rgb(13.99, 5.32, 2.0), // 4. Put something bright in a dark environment to see the effect + emissive: LinearRgba::rgb(0.0, 0.0, 150.0), // 4. Put something bright in a dark environment to see the effect ..default() }); let material_emissive2 = materials.add(StandardMaterial { - emissive: LinearRgba::rgb(2.0, 13.99, 5.32), + emissive: LinearRgba::rgb(1000.0, 1000.0, 1000.0), ..default() }); let material_emissive3 = materials.add(StandardMaterial { - emissive: LinearRgba::rgb(5.32, 2.0, 13.99), + emissive: LinearRgba::rgb(50.0, 0.0, 0.0), ..default() }); let material_non_emissive = materials.add(StandardMaterial { - base_color: GRAY.into(), + base_color: Color::BLACK, ..default() }); - let mesh = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap()); + let mesh = meshes.add(Sphere::new(0.4).mesh().ico(5).unwrap()); for x in -5..5 { for z in -5..5 { @@ -64,20 +63,21 @@ fn setup_scene( // the same spheres are always the same colors. let mut hasher = DefaultHasher::new(); (x, z).hash(&mut hasher); - let rand = (hasher.finish() - 2) % 6; + let rand = (hasher.finish() + 3) % 6; - let material = match rand { - 0 => material_emissive1.clone(), - 1 => material_emissive2.clone(), - 2 => material_emissive3.clone(), - 3..=5 => material_non_emissive.clone(), + let (material, scale) = match rand { + 0 => (material_emissive1.clone(), 0.5), + 1 => (material_emissive2.clone(), 0.1), + 2 => (material_emissive3.clone(), 1.0), + 3..=5 => (material_non_emissive.clone(), 1.5), _ => unreachable!(), }; commands.spawn(( Mesh3d(mesh.clone()), MeshMaterial3d(material), - Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0), + Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0) + .with_scale(Vec3::splat(scale)), Bouncing, )); } @@ -134,6 +134,7 @@ fn update_bloom_settings( "(U/J) Threshold softness: {}\n", bloom.prefilter.threshold_softness )); + text.push_str(&format!("(I/K) Horizontal Scale: {}\n", bloom.scale.x)); if keycode.just_pressed(KeyCode::Space) { commands.entity(entity).remove::(); @@ -196,6 +197,14 @@ fn update_bloom_settings( bloom.prefilter.threshold_softness += dt / 10.0; } bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0); + + if keycode.pressed(KeyCode::KeyK) { + bloom.scale.x -= dt * 2.0; + } + if keycode.pressed(KeyCode::KeyI) { + bloom.scale.x += dt * 2.0; + } + bloom.scale.x = bloom.scale.x.clamp(0.0, 8.0); } (entity, None) => {