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_

<img width="1392" alt="image"
src="https://github.com/user-attachments/assets/0179df38-ea2e-4f34-bbd3-d3240f0d0a4f"
/>

# 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.
This commit is contained in:
Aevyrie 2025-01-06 10:43:21 -08:00 committed by GitHub
parent 7f74e3c2f9
commit 13deb3ed76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 105 additions and 31 deletions

View File

@ -9,8 +9,8 @@
struct BloomUniforms {
threshold_precomputations: vec4<f32>,
viewport: vec4<f32>,
scale: vec2<f32>,
aspect: f32,
uv_offset: f32
};
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@ -51,6 +51,14 @@ fn karis_average(color: vec3<f32>) -> f32 {
// [COD] slide 153
fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
#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<i32>(-2, 2)).rgb;
let b = textureSample(input_texture, s, uv, vec2<i32>(0, 2)).rgb;
let c = textureSample(input_texture, s, uv, vec2<i32>(2, 2)).rgb;
@ -64,6 +72,35 @@ fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
let k = textureSample(input_texture, s, uv, vec2<i32>(1, 1)).rgb;
let l = textureSample(input_texture, s, uv, vec2<i32>(-1, -1)).rgb;
let m = textureSample(input_texture, s, uv, vec2<i32>(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<f32>(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<f32>(nl.x, pl.y)).rgb;
let b = textureSample(input_texture, s, uv + vec2<f32>(0.00, pl.y)).rgb;
let c = textureSample(input_texture, s, uv + vec2<f32>(pl.x, pl.y)).rgb;
let d = textureSample(input_texture, s, uv + vec2<f32>(nl.x, 0.00)).rgb;
let e = textureSample(input_texture, s, uv).rgb;
let f = textureSample(input_texture, s, uv + vec2<f32>(pl.x, 0.00)).rgb;
let g = textureSample(input_texture, s, uv + vec2<f32>(nl.x, nl.y)).rgb;
let h = textureSample(input_texture, s, uv + vec2<f32>(0.00, nl.y)).rgb;
let i = textureSample(input_texture, s, uv + vec2<f32>(pl.x, nl.y)).rgb;
let j = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ps.y)).rgb;
let k = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ps.y)).rgb;
let l = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ns.y)).rgb;
let m = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ns.y)).rgb;
#endif
#ifdef FIRST_DOWNSAMPLE
// [COD] slide 168
@ -95,9 +132,11 @@ fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
// [COD] slide 162
fn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {
// 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<f32>(textureDimensions(input_texture));
let x = frag_size.x;
let y = frag_size.y;
let a = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y + y)).rgb;
let b = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y + y)).rgb;

View File

@ -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,
},
);

View File

@ -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))

View File

@ -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::<Bloom>();
@ -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) => {

View File

@ -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::<Bloom>();
@ -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) => {