Skip to content

Commit 13deb3e

Browse files
authored
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.
1 parent 7f74e3c commit 13deb3e

File tree

5 files changed

+105
-31
lines changed

5 files changed

+105
-31
lines changed

crates/bevy_core_pipeline/src/bloom/bloom.wgsl

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
struct BloomUniforms {
1010
threshold_precomputations: vec4<f32>,
1111
viewport: vec4<f32>,
12+
scale: vec2<f32>,
1213
aspect: f32,
13-
uv_offset: f32
1414
};
1515

1616
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@@ -51,6 +51,14 @@ fn karis_average(color: vec3<f32>) -> f32 {
5151

5252
// [COD] slide 153
5353
fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
54+
#ifdef UNIFORM_SCALE
55+
// This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be
56+
// expressed with constant offsets.
57+
//
58+
// It's possible that this isn't meaningfully faster than the "slow" path. However, because it
59+
// is hard to test performance on all platforms, and uniform bloom is the most common case, this
60+
// path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero,
61+
// cost to maintainability, but it does help me sleep at night.
5462
let a = textureSample(input_texture, s, uv, vec2<i32>(-2, 2)).rgb;
5563
let b = textureSample(input_texture, s, uv, vec2<i32>(0, 2)).rgb;
5664
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> {
6472
let k = textureSample(input_texture, s, uv, vec2<i32>(1, 1)).rgb;
6573
let l = textureSample(input_texture, s, uv, vec2<i32>(-1, -1)).rgb;
6674
let m = textureSample(input_texture, s, uv, vec2<i32>(1, -1)).rgb;
75+
#else
76+
// This is the flexible, but potentially slower, path for non-uniform sampling. Because the
77+
// sample is not a constant, and it can fall outside of the limits imposed on constant sample
78+
// offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the
79+
// texture.
80+
//
81+
// It isn't clear if this is meaningfully slower than using the offset syntax, the spec doesn't
82+
// mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset
83+
// syntax uses a const-expr implies that it allows some compiler optimizations - maybe more
84+
// impactful on mobile?
85+
let scale = uniforms.scale;
86+
let ps = scale / vec2<f32>(textureDimensions(input_texture));
87+
let pl = 2.0 * ps;
88+
let ns = -1.0 * ps;
89+
let nl = -2.0 * ps;
90+
let a = textureSample(input_texture, s, uv + vec2<f32>(nl.x, pl.y)).rgb;
91+
let b = textureSample(input_texture, s, uv + vec2<f32>(0.00, pl.y)).rgb;
92+
let c = textureSample(input_texture, s, uv + vec2<f32>(pl.x, pl.y)).rgb;
93+
let d = textureSample(input_texture, s, uv + vec2<f32>(nl.x, 0.00)).rgb;
94+
let e = textureSample(input_texture, s, uv).rgb;
95+
let f = textureSample(input_texture, s, uv + vec2<f32>(pl.x, 0.00)).rgb;
96+
let g = textureSample(input_texture, s, uv + vec2<f32>(nl.x, nl.y)).rgb;
97+
let h = textureSample(input_texture, s, uv + vec2<f32>(0.00, nl.y)).rgb;
98+
let i = textureSample(input_texture, s, uv + vec2<f32>(pl.x, nl.y)).rgb;
99+
let j = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ps.y)).rgb;
100+
let k = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ps.y)).rgb;
101+
let l = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ns.y)).rgb;
102+
let m = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ns.y)).rgb;
103+
#endif
67104

68105
#ifdef FIRST_DOWNSAMPLE
69106
// [COD] slide 168
@@ -95,9 +132,11 @@ fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
95132

96133
// [COD] slide 162
97134
fn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {
98-
// UV offsets configured from uniforms.
99-
let x = uniforms.uv_offset / uniforms.aspect;
100-
let y = uniforms.uv_offset;
135+
// While this is probably technically incorrect, it makes nonuniform bloom smoother, without
136+
// having any impact on uniform bloom, which simply evaluates to 1.0 here.
137+
let frag_size = uniforms.scale / vec2<f32>(textureDimensions(input_texture));
138+
let x = frag_size.x;
139+
let y = frag_size.y;
101140

102141
let a = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y + y)).rgb;
103142
let b = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y + y)).rgb;

crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bevy_ecs::{
55
system::{Commands, Query, Res, ResMut, Resource},
66
world::{FromWorld, World},
77
};
8-
use bevy_math::Vec4;
8+
use bevy_math::{Vec2, Vec4};
99
use bevy_render::{
1010
render_resource::{
1111
binding_types::{sampler, texture_2d, uniform_buffer},
@@ -31,6 +31,7 @@ pub struct BloomDownsamplingPipeline {
3131
pub struct BloomDownsamplingPipelineKeys {
3232
prefilter: bool,
3333
first_downsample: bool,
34+
uniform_scale: bool,
3435
}
3536

3637
/// The uniform struct extracted from [`Bloom`] attached to a Camera.
@@ -40,8 +41,8 @@ pub struct BloomUniforms {
4041
// Precomputed values used when thresholding, see https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4
4142
pub threshold_precomputations: Vec4,
4243
pub viewport: Vec4,
44+
pub scale: Vec2,
4345
pub aspect: f32,
44-
pub uv_offset: f32,
4546
}
4647

4748
impl FromWorld for BloomDownsamplingPipeline {
@@ -102,6 +103,10 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline {
102103
shader_defs.push("USE_THRESHOLD".into());
103104
}
104105

106+
if key.uniform_scale {
107+
shader_defs.push("UNIFORM_SCALE".into());
108+
}
109+
105110
RenderPipelineDescriptor {
106111
label: Some(
107112
if key.first_downsample {
@@ -148,6 +153,7 @@ pub fn prepare_downsampling_pipeline(
148153
BloomDownsamplingPipelineKeys {
149154
prefilter,
150155
first_downsample: false,
156+
uniform_scale: bloom.scale == Vec2::ONE,
151157
},
152158
);
153159

@@ -157,6 +163,7 @@ pub fn prepare_downsampling_pipeline(
157163
BloomDownsamplingPipelineKeys {
158164
prefilter,
159165
first_downsample: true,
166+
uniform_scale: bloom.scale == Vec2::ONE,
160167
},
161168
);
162169

crates/bevy_core_pipeline/src/bloom/settings.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::downsampling_pipeline::BloomUniforms;
22
use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent};
3-
use bevy_math::{AspectRatio, URect, UVec4, Vec4};
3+
use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4};
44
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
55
use bevy_render::{extract_component::ExtractComponent, prelude::Camera};
66

@@ -113,14 +113,14 @@ pub struct Bloom {
113113
/// Only tweak if you are seeing visual artifacts.
114114
pub max_mip_dimension: u32,
115115

116-
/// UV offset for bloom shader. Ideally close to 2.0 / `max_mip_dimension`.
117-
/// Only tweak if you are seeing visual artifacts.
118-
pub uv_offset: f32,
116+
/// Amount to stretch the bloom on each axis. Artistic control, can be used to emulate
117+
/// anamorphic blur by using a large x-value. For large values, you may need to increase
118+
/// [`Bloom::max_mip_dimension`] to reduce sampling artifacts.
119+
pub scale: Vec2,
119120
}
120121

121122
impl Bloom {
122123
const DEFAULT_MAX_MIP_DIMENSION: u32 = 512;
123-
const DEFAULT_UV_OFFSET: f32 = 0.004;
124124

125125
/// The default bloom preset.
126126
///
@@ -136,7 +136,15 @@ impl Bloom {
136136
},
137137
composite_mode: BloomCompositeMode::EnergyConserving,
138138
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
139-
uv_offset: Self::DEFAULT_UV_OFFSET,
139+
scale: Vec2::ONE,
140+
};
141+
142+
/// Emulates the look of stylized anamorphic bloom, stretched horizontally.
143+
pub const ANAMORPHIC: Self = Self {
144+
// The larger scale necessitates a larger resolution to reduce artifacts:
145+
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION * 2,
146+
scale: Vec2::new(4.0, 1.0),
147+
..Self::NATURAL
140148
};
141149

142150
/// A preset that's similar to how older games did bloom.
@@ -151,7 +159,7 @@ impl Bloom {
151159
},
152160
composite_mode: BloomCompositeMode::Additive,
153161
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
154-
uv_offset: Self::DEFAULT_UV_OFFSET,
162+
scale: Vec2::ONE,
155163
};
156164

157165
/// A preset that applies a very strong bloom, and blurs the whole screen.
@@ -166,7 +174,7 @@ impl Bloom {
166174
},
167175
composite_mode: BloomCompositeMode::EnergyConserving,
168176
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
169-
uv_offset: Self::DEFAULT_UV_OFFSET,
177+
scale: Vec2::ONE,
170178
};
171179
}
172180

@@ -240,7 +248,7 @@ impl ExtractComponent for Bloom {
240248
aspect: AspectRatio::try_from_pixels(size.x, size.y)
241249
.expect("Valid screen size values for Bloom settings")
242250
.ratio(),
243-
uv_offset: bloom.uv_offset,
251+
scale: bloom.scale,
244252
};
245253

246254
Some((bloom.clone(), uniform))

examples/2d/bloom_2d.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use bevy::{
44
core_pipeline::{
55
bloom::{Bloom, BloomCompositeMode},
6-
tonemapping::Tonemapping,
6+
tonemapping::{DebandDither, Tonemapping},
77
},
88
prelude::*,
99
};
@@ -26,10 +26,12 @@ fn setup(
2626
Camera2d,
2727
Camera {
2828
hdr: true, // 1. HDR is required for bloom
29+
clear_color: ClearColorConfig::Custom(Color::BLACK),
2930
..default()
3031
},
3132
Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended
3233
Bloom::default(), // 3. Enable bloom for the camera
34+
DebandDither::Enabled, // Optional: bloom causes gradients which cause banding
3335
));
3436

3537
// Sprite
@@ -107,6 +109,7 @@ fn update_bloom_settings(
107109
"(U/J) Threshold softness: {}\n",
108110
bloom.prefilter.threshold_softness
109111
));
112+
text.push_str(&format!("(I/K) Horizontal Scale: {}\n", bloom.scale.x));
110113

111114
if keycode.just_pressed(KeyCode::Space) {
112115
commands.entity(entity).remove::<Bloom>();
@@ -169,6 +172,14 @@ fn update_bloom_settings(
169172
bloom.prefilter.threshold_softness += dt / 10.0;
170173
}
171174
bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0);
175+
176+
if keycode.pressed(KeyCode::KeyK) {
177+
bloom.scale.x -= dt * 2.0;
178+
}
179+
if keycode.pressed(KeyCode::KeyI) {
180+
bloom.scale.x += dt * 2.0;
181+
}
182+
bloom.scale.x = bloom.scale.x.clamp(0.0, 16.0);
172183
}
173184

174185
(entity, None) => {

examples/3d/bloom_3d.rs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Illustrates bloom post-processing using HDR and emissive materials.
22
33
use bevy::{
4-
color::palettes::basic::GRAY,
54
core_pipeline::{
65
bloom::{Bloom, BloomCompositeMode},
76
tonemapping::Tonemapping,
@@ -31,53 +30,54 @@ fn setup_scene(
3130
Camera3d::default(),
3231
Camera {
3332
hdr: true, // 1. HDR is required for bloom
33+
clear_color: ClearColorConfig::Custom(Color::BLACK),
3434
..default()
3535
},
3636
Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended
3737
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
38-
// 3. Enable bloom for the camera
39-
Bloom::NATURAL,
38+
Bloom::NATURAL, // 3. Enable bloom for the camera
4039
));
4140

4241
let material_emissive1 = materials.add(StandardMaterial {
43-
emissive: LinearRgba::rgb(13.99, 5.32, 2.0), // 4. Put something bright in a dark environment to see the effect
42+
emissive: LinearRgba::rgb(0.0, 0.0, 150.0), // 4. Put something bright in a dark environment to see the effect
4443
..default()
4544
});
4645
let material_emissive2 = materials.add(StandardMaterial {
47-
emissive: LinearRgba::rgb(2.0, 13.99, 5.32),
46+
emissive: LinearRgba::rgb(1000.0, 1000.0, 1000.0),
4847
..default()
4948
});
5049
let material_emissive3 = materials.add(StandardMaterial {
51-
emissive: LinearRgba::rgb(5.32, 2.0, 13.99),
50+
emissive: LinearRgba::rgb(50.0, 0.0, 0.0),
5251
..default()
5352
});
5453
let material_non_emissive = materials.add(StandardMaterial {
55-
base_color: GRAY.into(),
54+
base_color: Color::BLACK,
5655
..default()
5756
});
5857

59-
let mesh = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap());
58+
let mesh = meshes.add(Sphere::new(0.4).mesh().ico(5).unwrap());
6059

6160
for x in -5..5 {
6261
for z in -5..5 {
6362
// This generates a pseudo-random integer between `[0, 6)`, but deterministically so
6463
// the same spheres are always the same colors.
6564
let mut hasher = DefaultHasher::new();
6665
(x, z).hash(&mut hasher);
67-
let rand = (hasher.finish() - 2) % 6;
66+
let rand = (hasher.finish() + 3) % 6;
6867

69-
let material = match rand {
70-
0 => material_emissive1.clone(),
71-
1 => material_emissive2.clone(),
72-
2 => material_emissive3.clone(),
73-
3..=5 => material_non_emissive.clone(),
68+
let (material, scale) = match rand {
69+
0 => (material_emissive1.clone(), 0.5),
70+
1 => (material_emissive2.clone(), 0.1),
71+
2 => (material_emissive3.clone(), 1.0),
72+
3..=5 => (material_non_emissive.clone(), 1.5),
7473
_ => unreachable!(),
7574
};
7675

7776
commands.spawn((
7877
Mesh3d(mesh.clone()),
7978
MeshMaterial3d(material),
80-
Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0),
79+
Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0)
80+
.with_scale(Vec3::splat(scale)),
8181
Bouncing,
8282
));
8383
}
@@ -134,6 +134,7 @@ fn update_bloom_settings(
134134
"(U/J) Threshold softness: {}\n",
135135
bloom.prefilter.threshold_softness
136136
));
137+
text.push_str(&format!("(I/K) Horizontal Scale: {}\n", bloom.scale.x));
137138

138139
if keycode.just_pressed(KeyCode::Space) {
139140
commands.entity(entity).remove::<Bloom>();
@@ -196,6 +197,14 @@ fn update_bloom_settings(
196197
bloom.prefilter.threshold_softness += dt / 10.0;
197198
}
198199
bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0);
200+
201+
if keycode.pressed(KeyCode::KeyK) {
202+
bloom.scale.x -= dt * 2.0;
203+
}
204+
if keycode.pressed(KeyCode::KeyI) {
205+
bloom.scale.x += dt * 2.0;
206+
}
207+
bloom.scale.x = bloom.scale.x.clamp(0.0, 8.0);
199208
}
200209

201210
(entity, None) => {

0 commit comments

Comments
 (0)