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  - Added `scale` parameter to `Bloom` to improve artistic control and enable anamorphic bloom.
This commit is contained in:
parent
7f74e3c2f9
commit
13deb3ed76
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user