Revamp Bloom (#6677)

![image](https://user-images.githubusercontent.com/47158642/220197588-25e11022-02e4-45f3-b2e5-392c4ce7a025.png)

Huge credit to @StarLederer, who did almost all of the work on this. We're just reusing this PR to keep everything in one place.

# Objective

1. Make bloom more physically based.
1. Improve artistic control.
1. Allow to use bloom as screen blur.
1. Fix #6634.
1. Address #6655 (although the author makes incorrect conclusions).

## Solution

1. Set the default threshold to 0.
2. Lerp between bloom textures when `composite_mode: BloomCompositeMode::EnergyConserving`.
1. Use [a parametric function](https://starlederer.github.io/bloom) to control blend levels for each bloom texture. In the future this can be controlled per-pixel for things like lens dirt.
3. Implement BloomCompositeMode::Additive` for situations where the old school look is desired.

## Changelog

* Bloom now looks different.
* Added `BloomSettings:lf_boost`, `BloomSettings:lf_boost_curvature`, `BloomSettings::high_pass_frequency` and `BloomSettings::composite_mode`.
* `BloomSettings::scale` removed.
* `BloomSettings::knee` renamed to `BloomPrefilterSettings::softness`.
* `BloomSettings::threshold` renamed to `BloomPrefilterSettings::threshold`.
* The bloom example has been renamed to bloom_3d and improved. A bloom_2d example was added.

## Migration Guide

* Refactor mentions of `BloomSettings::knee` and `BloomSettings::threshold` as `BloomSettings::prefilter_settings` where knee is now `softness`.
* If defined without `..default()` add `..default()` to definitions of `BloomSettings` instances or manually define missing fields.
* Adapt to Bloom looking visually different (if needed).

Co-authored-by: Herman Lederer <germans.lederers@gmail.com>
This commit is contained in:
JMS55 2023-03-04 12:05:26 +00:00
parent cbbf8ac575
commit 2a7000a738
12 changed files with 1409 additions and 772 deletions

View file

@ -50,7 +50,7 @@ default = [
"x11", "x11",
"filesystem_watcher", "filesystem_watcher",
"android_shared_stdcxx", "android_shared_stdcxx",
"tonemapping_luts" "tonemapping_luts",
] ]
# Force dynamic linking, which improves iterative compile times # Force dynamic linking, which improves iterative compile times
@ -239,6 +239,16 @@ path = "examples/hello_world.rs"
hidden = true hidden = true
# 2D Rendering # 2D Rendering
[[example]]
name = "bloom_2d"
path = "examples/2d/bloom_2d.rs"
[package.metadata.example.bloom_2d]
name = "2D Bloom"
description = "Illustrates bloom post-processing in 2d"
category = "2D Rendering"
wasm = false
[[example]] [[example]]
name = "move_sprite" name = "move_sprite"
path = "examples/2d/move_sprite.rs" path = "examples/2d/move_sprite.rs"
@ -451,11 +461,11 @@ category = "3D Rendering"
wasm = true wasm = true
[[example]] [[example]]
name = "bloom" name = "bloom_3d"
path = "examples/3d/bloom.rs" path = "examples/3d/bloom_3d.rs"
[package.metadata.example.bloom] [package.metadata.example.bloom_3d]
name = "Bloom" name = "3D Bloom"
description = "Illustrates bloom configuration using HDR and emissive materials" description = "Illustrates bloom configuration using HDR and emissive materials"
category = "3D Rendering" category = "3D Rendering"
wasm = false wasm = false

View file

@ -1,138 +1,151 @@
// Bloom works by creating an intermediate texture with a bunch of mip levels, each half the size of the previous.
// You then downsample each mip (starting with the original texture) to the lower resolution mip under it, going in order.
// You then upsample each mip (starting from the smallest mip) and blend with the higher resolution mip above it (ending on the original texture).
//
// References:
// * [COD] - Next Generation Post Processing in Call of Duty - http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
// * [PBB] - Physically Based Bloom - https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
#import bevy_core_pipeline::fullscreen_vertex_shader #import bevy_core_pipeline::fullscreen_vertex_shader
struct BloomUniforms { struct BloomUniforms {
threshold: f32, threshold_precomputations: vec4<f32>,
knee: f32,
scale: f32,
intensity: f32,
viewport: vec4<f32>, viewport: vec4<f32>,
aspect: f32,
}; };
@group(0) @binding(0) @group(0) @binding(0)
var original: texture_2d<f32>; var input_texture: texture_2d<f32>;
@group(0) @binding(1) @group(0) @binding(1)
var original_sampler: sampler; var s: sampler;
@group(0) @binding(2) @group(0) @binding(2)
var<uniform> uniforms: BloomUniforms; var<uniform> uniforms: BloomUniforms;
@group(0) @binding(3)
var up: texture_2d<f32>;
fn quadratic_threshold(color: vec4<f32>, threshold: f32, curve: vec3<f32>) -> vec4<f32> { #ifdef FIRST_DOWNSAMPLE
let br = max(max(color.r, color.g), color.b); // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4
fn soft_threshold(color: vec3<f32>) -> vec3<f32> {
let brightness = max(color.r, max(color.g, color.b));
var softness = brightness - uniforms.threshold_precomputations.y;
softness = clamp(softness, 0.0, uniforms.threshold_precomputations.z);
softness = softness * softness * uniforms.threshold_precomputations.w;
var contribution = max(brightness - uniforms.threshold_precomputations.x, softness);
contribution /= max(brightness, 0.00001); // Prevent division by 0
return color * contribution;
}
#endif
var rq: f32 = clamp(br - curve.x, 0.0, curve.y); // luminance coefficients from Rec. 709.
rq = curve.z * rq * rq; // https://en.wikipedia.org/wiki/Rec._709
fn tonemapping_luminance(v: vec3<f32>) -> f32 {
return color * max(rq, br - threshold) / max(br, 0.0001); return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
} }
// Samples original around the supplied uv using a filter. fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
// return pow(color, vec3<f32>(1.0 / 2.2));
// o o o
// o o
// o o o
// o o
// o o o
//
// This is used because it has a number of advantages that
// outweigh the cost of 13 samples that basically boil down
// to it looking better.
//
// These advantages are outlined in a youtube video by the Cherno:
// https://www.youtube.com/watch?v=tI70-HIc5ro
fn sample_13_tap(uv: vec2<f32>, scale: vec2<f32>) -> vec4<f32> {
let a = textureSample(original, original_sampler, uv + vec2<f32>(-1.0, -1.0) * scale);
let b = textureSample(original, original_sampler, uv + vec2<f32>(0.0, -1.0) * scale);
let c = textureSample(original, original_sampler, uv + vec2<f32>(1.0, -1.0) * scale);
let d = textureSample(original, original_sampler, uv + vec2<f32>(-0.5, -0.5) * scale);
let e = textureSample(original, original_sampler, uv + vec2<f32>(0.5, -0.5) * scale);
let f = textureSample(original, original_sampler, uv + vec2<f32>(-1.0, 0.0) * scale);
let g = textureSample(original, original_sampler, uv + vec2<f32>(0.0, 0.0) * scale);
let h = textureSample(original, original_sampler, uv + vec2<f32>(1.0, 0.0) * scale);
let i = textureSample(original, original_sampler, uv + vec2<f32>(-0.5, 0.5) * scale);
let j = textureSample(original, original_sampler, uv + vec2<f32>(0.5, 0.5) * scale);
let k = textureSample(original, original_sampler, uv + vec2<f32>(-1.0, 1.0) * scale);
let l = textureSample(original, original_sampler, uv + vec2<f32>(0.0, 1.0) * scale);
let m = textureSample(original, original_sampler, uv + vec2<f32>(1.0, 1.0) * scale);
let div = (1.0 / 4.0) * vec2<f32>(0.5, 0.125);
var o: vec4<f32> = (d + e + i + j) * div.x;
o = o + (a + b + g + f) * div.y;
o = o + (b + c + h + g) * div.y;
o = o + (f + g + l + k) * div.y;
o = o + (g + h + m + l) * div.y;
return o;
} }
// Samples original using a 3x3 tent filter. // http://graphicrants.blogspot.com/2013/12/tone-mapping.html
// fn karis_average(color: vec3<f32>) -> f32 {
// NOTE: Use a 2x2 filter for better perf, but 3x3 looks better. // Luminance calculated by gamma-correcting linear RGB to non-linear sRGB using pow(color, 1.0 / 2.2)
fn sample_original_3x3_tent(uv: vec2<f32>, scale: vec2<f32>) -> vec4<f32> { // and then calculating luminance based on Rec. 709 color primaries.
let d = vec4<f32>(1.0, 1.0, -1.0, 0.0); let luma = tonemapping_luminance(rgb_to_srgb_simple(color)) / 4.0;
return 1.0 / (1.0 + luma);
var s: vec4<f32> = textureSample(original, original_sampler, uv - d.xy * scale);
s = s + textureSample(original, original_sampler, uv - d.wy * scale) * 2.0;
s = s + textureSample(original, original_sampler, uv - d.zy * scale);
s = s + textureSample(original, original_sampler, uv + d.zw * scale) * 2.0;
s = s + textureSample(original, original_sampler, uv) * 4.0;
s = s + textureSample(original, original_sampler, uv + d.xw * scale) * 2.0;
s = s + textureSample(original, original_sampler, uv + d.zy * scale);
s = s + textureSample(original, original_sampler, uv + d.wy * scale) * 2.0;
s = s + textureSample(original, original_sampler, uv + d.xy * scale);
return s / 16.0;
} }
// [COD] slide 153
fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
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;
let d = textureSample(input_texture, s, uv, vec2<i32>(-2, 0)).rgb;
let e = textureSample(input_texture, s, uv).rgb;
let f = textureSample(input_texture, s, uv, vec2<i32>(2, 0)).rgb;
let g = textureSample(input_texture, s, uv, vec2<i32>(-2, -2)).rgb;
let h = textureSample(input_texture, s, uv, vec2<i32>(0, -2)).rgb;
let i = textureSample(input_texture, s, uv, vec2<i32>(2, -2)).rgb;
let j = textureSample(input_texture, s, uv, vec2<i32>(-1, 1)).rgb;
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;
#ifdef FIRST_DOWNSAMPLE
// [COD] slide 168
//
// The first downsample pass reads from the rendered frame which may exhibit
// 'fireflies' (individual very bright pixels) that should not cause the bloom effect.
//
// The first downsample uses a firefly-reduction method proposed by Brian Karis
// which takes a weighted-average of the samples to limit their luma range to [0, 1].
// This implementation matches the LearnOpenGL article [PBB].
var group0 = (a + b + d + e) * (0.125f / 4.0f);
var group1 = (b + c + e + f) * (0.125f / 4.0f);
var group2 = (d + e + g + h) * (0.125f / 4.0f);
var group3 = (e + f + h + i) * (0.125f / 4.0f);
var group4 = (j + k + l + m) * (0.5f / 4.0f);
group0 *= karis_average(group0);
group1 *= karis_average(group1);
group2 *= karis_average(group2);
group3 *= karis_average(group3);
group4 *= karis_average(group4);
return group0 + group1 + group2 + group3 + group4;
#else
var sample = (a + c + g + i) * 0.03125;
sample += (b + d + f + h) * 0.0625;
sample += (e + j + k + l + m) * 0.125;
return sample;
#endif
}
// [COD] slide 162
fn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {
// Radius. Empirically chosen by and tweaked from the LearnOpenGL article.
let x = 0.004 / uniforms.aspect;
let y = 0.004;
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;
let c = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y + y)).rgb;
let d = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y)).rgb;
let e = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y)).rgb;
let f = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y)).rgb;
let g = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y - y)).rgb;
let h = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y - y)).rgb;
let i = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y - y)).rgb;
var sample = e * 0.25;
sample += (b + d + f + h) * 0.125;
sample += (a + c + g + i) * 0.0625;
return sample;
}
#ifdef FIRST_DOWNSAMPLE
@fragment @fragment
fn downsample_prefilter(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32> { fn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32> {
let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw; let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw;
let texel_size = 1.0 / vec2<f32>(textureDimensions(original)); var sample = sample_input_13_tap(sample_uv);
// Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
// downscaling and upscaling which would result in black boxes.
// The upper bound is to prevent NaNs.
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+38));
let scale = texel_size; #ifdef USE_THRESHOLD
sample = soft_threshold(sample);
#endif
let curve = vec3<f32>( return vec4<f32>(sample, 1.0);
uniforms.threshold - uniforms.knee,
uniforms.knee * 2.0,
0.25 / uniforms.knee,
);
var o: vec4<f32> = sample_13_tap(sample_uv, scale);
o = quadratic_threshold(o, uniforms.threshold, curve);
o = max(o, vec4<f32>(0.00001));
return o;
} }
#endif
@fragment @fragment
fn downsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { fn downsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let texel_size = 1.0 / vec2<f32>(textureDimensions(original)); return vec4<f32>(sample_input_13_tap(uv), 1.0);
let scale = texel_size;
return sample_13_tap(uv, scale);
} }
@fragment @fragment
fn upsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { fn upsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let texel_size = 1.0 / vec2<f32>(textureDimensions(original)); return vec4<f32>(sample_input_3x3_tent(uv), 1.0);
let upsample = sample_original_3x3_tent(uv, texel_size * uniforms.scale);
var color: vec4<f32> = textureSample(up, original_sampler, uv);
color = vec4<f32>(color.rgb + upsample.rgb, upsample.a);
return color;
}
@fragment
fn upsample_final(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let texel_size = 1.0 / vec2<f32>(textureDimensions(original));
let upsample = sample_original_3x3_tent(uv, texel_size * uniforms.scale);
return vec4<f32>(upsample.rgb * uniforms.intensity, upsample.a);
} }

View file

@ -0,0 +1,185 @@
use super::{BloomSettings, BLOOM_SHADER_HANDLE, BLOOM_TEXTURE_FORMAT};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use bevy_ecs::{
prelude::{Component, Entity},
system::{Commands, Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_math::Vec4;
use bevy_render::{render_resource::*, renderer::RenderDevice};
#[derive(Component)]
pub struct BloomDownsamplingPipelineIds {
pub main: CachedRenderPipelineId,
pub first: CachedRenderPipelineId,
}
#[derive(Resource)]
pub struct BloomDownsamplingPipeline {
/// Layout with a texture, a sampler, and uniforms
pub bind_group_layout: BindGroupLayout,
pub sampler: Sampler,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct BloomDownsamplingPipelineKeys {
prefilter: bool,
first_downsample: bool,
}
/// The uniform struct extracted from [`BloomSettings`] attached to a Camera.
/// Will be available for use in the Bloom shader.
#[derive(Component, ShaderType, Clone)]
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 aspect: f32,
}
impl FromWorld for BloomDownsamplingPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// Input texture binding
let texture = BindGroupLayoutEntry {
binding: 0,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
visibility: ShaderStages::FRAGMENT,
count: None,
};
// Sampler binding
let sampler = BindGroupLayoutEntry {
binding: 1,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
visibility: ShaderStages::FRAGMENT,
count: None,
};
// Downsampling settings binding
let settings = BindGroupLayoutEntry {
binding: 2,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(BloomUniforms::min_size()),
},
visibility: ShaderStages::FRAGMENT,
count: None,
};
// Bind group layout
let bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("bloom_downsampling_bind_group_layout_with_settings"),
entries: &[texture, sampler, settings],
});
// Sampler
let sampler = render_device.create_sampler(&SamplerDescriptor {
min_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
..Default::default()
});
BloomDownsamplingPipeline {
bind_group_layout,
sampler,
}
}
}
impl SpecializedRenderPipeline for BloomDownsamplingPipeline {
type Key = BloomDownsamplingPipelineKeys;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let layout = vec![self.bind_group_layout.clone()];
let entry_point = if key.first_downsample {
"downsample_first".into()
} else {
"downsample".into()
};
let mut shader_defs = vec![];
if key.first_downsample {
shader_defs.push("FIRST_DOWNSAMPLE".into());
}
if key.prefilter {
shader_defs.push("USE_THRESHOLD".into());
}
RenderPipelineDescriptor {
label: Some(
if key.first_downsample {
"bloom_downsampling_pipeline_first"
} else {
"bloom_downsampling_pipeline"
}
.into(),
),
layout,
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs,
entry_point,
targets: vec![Some(ColorTargetState {
format: BLOOM_TEXTURE_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
}
}
}
pub fn prepare_downsampling_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BloomDownsamplingPipeline>>,
pipeline: Res<BloomDownsamplingPipeline>,
views: Query<(Entity, &BloomSettings)>,
) {
for (entity, settings) in &views {
let prefilter = settings.prefilter_settings.threshold > 0.0;
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomDownsamplingPipelineKeys {
prefilter,
first_downsample: false,
},
);
let pipeline_first_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomDownsamplingPipelineKeys {
prefilter,
first_downsample: true,
},
);
commands
.entity(entity)
.insert(BloomDownsamplingPipelineIds {
first: pipeline_first_id,
main: pipeline_id,
});
}
}

View file

@ -1,19 +1,27 @@
use crate::{core_2d, core_3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state}; mod downsampling_pipeline;
mod settings;
mod upsampling_pipeline;
pub use settings::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings};
use crate::{core_2d, core_3d};
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, HandleUntyped}; use bevy_asset::{load_internal_asset, HandleUntyped};
use bevy_ecs::{ use bevy_ecs::{
prelude::*, prelude::{Component, Entity},
query::{QueryItem, QueryState}, query::{QueryState, With},
schedule::IntoSystemConfig,
system::{Commands, Query, Res, ResMut},
world::World,
}; };
use bevy_math::{UVec2, UVec4, Vec4}; use bevy_math::UVec2;
use bevy_reflect::{Reflect, TypeUuid}; use bevy_reflect::TypeUuid;
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::ExtractedCamera,
extract_component::{ extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,
UniformComponentPlugin,
}, },
prelude::Camera, prelude::Color,
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType},
render_resource::*, render_resource::*,
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice},
@ -23,12 +31,24 @@ use bevy_render::{
}; };
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use bevy_utils::tracing::info_span; use bevy_utils::tracing::info_span;
use bevy_utils::HashMap; use downsampling_pipeline::{
prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
BloomUniforms,
};
use std::num::NonZeroU32; use std::num::NonZeroU32;
use upsampling_pipeline::{
prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
};
const BLOOM_SHADER_HANDLE: HandleUntyped = const BLOOM_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 929599476923908); HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 929599476923908);
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Float;
// Maximum size of each dimension for the largest mipchain texture used in downscaling/upscaling.
// 512 behaves well with the UV offset of 0.004 used in bloom.wgsl
const MAX_MIP_DIMENSION: u32 = 512;
pub struct BloomPlugin; pub struct BloomPlugin;
impl Plugin for BloomPlugin { impl Plugin for BloomPlugin {
@ -36,8 +56,10 @@ impl Plugin for BloomPlugin {
load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl); load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl);
app.register_type::<BloomSettings>(); app.register_type::<BloomSettings>();
app.register_type::<BloomPrefilterSettings>();
app.register_type::<BloomCompositeMode>();
app.add_plugin(ExtractComponentPlugin::<BloomSettings>::default()); app.add_plugin(ExtractComponentPlugin::<BloomSettings>::default());
app.add_plugin(UniformComponentPlugin::<BloomUniform>::default()); app.add_plugin(UniformComponentPlugin::<BloomUniforms>::default());
let render_app = match app.get_sub_app_mut(RenderApp) { let render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app, Ok(render_app) => render_app,
@ -45,10 +67,16 @@ impl Plugin for BloomPlugin {
}; };
render_app render_app
.init_resource::<BloomPipelines>() .init_resource::<BloomDownsamplingPipeline>()
.init_resource::<BloomUpsamplingPipeline>()
.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
.add_system(prepare_bloom_textures.in_set(RenderSet::Prepare)) .add_system(prepare_bloom_textures.in_set(RenderSet::Prepare))
.add_system(prepare_downsampling_pipeline.in_set(RenderSet::Prepare))
.add_system(prepare_upsampling_pipeline.in_set(RenderSet::Prepare))
.add_system(queue_bloom_bind_groups.in_set(RenderSet::Queue)); .add_system(queue_bloom_bind_groups.in_set(RenderSet::Queue));
// Add bloom to the 3d render graph
{ {
let bloom_node = BloomNode::new(&mut render_app.world); let bloom_node = BloomNode::new(&mut render_app.world);
let mut graph = render_app.world.resource_mut::<RenderGraph>(); let mut graph = render_app.world.resource_mut::<RenderGraph>();
@ -73,6 +101,7 @@ impl Plugin for BloomPlugin {
); );
} }
// Add bloom to the 2d render graph
{ {
let bloom_node = BloomNode::new(&mut render_app.world); let bloom_node = BloomNode::new(&mut render_app.world);
let mut graph = render_app.world.resource_mut::<RenderGraph>(); let mut graph = render_app.world.resource_mut::<RenderGraph>();
@ -99,88 +128,16 @@ impl Plugin for BloomPlugin {
} }
} }
/// Applies a bloom effect to a HDR-enabled 2d or 3d camera.
///
/// Bloom causes bright objects to "glow", emitting a halo of light around them.
///
/// Often used in conjunction with `bevy_pbr::StandardMaterial::emissive`.
///
/// Note: This light is not "real" in the way directional or point lights are.
///
/// Bloom will not cast shadows or bend around other objects - it is purely a post-processing
/// effect overlaid on top of the already-rendered scene.
///
/// See also <https://en.wikipedia.org/wiki/Bloom_(shader_effect)>.
#[derive(Component, Reflect, Clone)]
pub struct BloomSettings {
/// Baseline of the threshold curve (default: 1.0).
///
/// RGB values under the threshold curve will not have bloom applied.
pub threshold: f32,
/// Knee of the threshold curve (default: 0.1).
pub knee: f32,
/// Scale used when upsampling (default: 1.0).
pub scale: f32,
/// Intensity of the bloom effect (default: 0.3).
pub intensity: f32,
}
impl Default for BloomSettings {
fn default() -> Self {
Self {
threshold: 1.0,
knee: 0.1,
scale: 1.0,
intensity: 0.3,
}
}
}
impl ExtractComponent for BloomSettings {
type Query = (&'static Self, &'static Camera);
type Filter = ();
type Out = BloomUniform;
fn extract_component((settings, camera): QueryItem<'_, Self::Query>) -> Option<Self::Out> {
if !(camera.is_active && camera.hdr) {
return None;
}
if let (Some((origin, _)), Some(size), Some(target_size)) = (
camera.physical_viewport_rect(),
camera.physical_viewport_size(),
camera.physical_target_size(),
) {
let min_view = size.x.min(size.y) / 2;
let mip_count = calculate_mip_count(min_view);
let scale = (min_view / 2u32.pow(mip_count)) as f32 / 8.0;
Some(BloomUniform {
threshold: settings.threshold,
knee: settings.knee,
scale: settings.scale * scale,
intensity: settings.intensity,
viewport: UVec4::new(origin.x, origin.y, size.x, size.y).as_vec4()
/ UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y)
.as_vec4(),
})
} else {
None
}
}
}
pub struct BloomNode { pub struct BloomNode {
view_query: QueryState<( view_query: QueryState<(
&'static ExtractedCamera, &'static ExtractedCamera,
&'static ViewTarget, &'static ViewTarget,
&'static BloomTextures, &'static BloomTexture,
&'static BloomBindGroups, &'static BloomBindGroups,
&'static DynamicUniformIndex<BloomUniform>, &'static DynamicUniformIndex<BloomUniforms>,
&'static BloomSettings,
&'static UpsamplingPipelineIds,
&'static BloomDownsamplingPipelineIds,
)>, )>,
} }
@ -203,6 +160,9 @@ impl Node for BloomNode {
self.view_query.update_archetypes(world); self.view_query.update_archetypes(world);
} }
// Atypically for a post-processing effect, we do not need to
// use a secondary texture normally provided by view_target.post_process_write(),
// instead we write into our own bloom texture and then directly back onto main.
fn run( fn run(
&self, &self,
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
@ -212,34 +172,67 @@ impl Node for BloomNode {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _bloom_span = info_span!("bloom").entered(); let _bloom_span = info_span!("bloom").entered();
let pipelines = world.resource::<BloomPipelines>(); let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();
let pipeline_cache = world.resource::<PipelineCache>(); let pipeline_cache = world.resource::<PipelineCache>();
let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();
let view_entity = graph.get_input_entity(Self::IN_VIEW)?; let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (camera, view_target, textures, bind_groups, uniform_index) = let Ok((
match self.view_query.get_manual(world, view_entity) { camera,
Ok(result) => result, view_target,
_ => return Ok(()), bloom_texture,
}; bind_groups,
let ( uniform_index,
downsampling_prefilter_pipeline, bloom_settings,
downsampling_pipeline, upsampling_pipeline_ids,
upsampling_pipeline, downsampling_pipeline_ids,
upsampling_final_pipeline, )) = self.view_query.get_manual(world, view_entity)
) = match ( else { return Ok(()) };
pipeline_cache.get_render_pipeline(pipelines.downsampling_prefilter_pipeline),
pipeline_cache.get_render_pipeline(pipelines.downsampling_pipeline),
pipeline_cache.get_render_pipeline(pipelines.upsampling_pipeline),
pipeline_cache.get_render_pipeline(pipelines.upsampling_final_pipeline),
) {
(Some(p1), Some(p2), Some(p3), Some(p4)) => (p1, p2, p3, p4),
_ => return Ok(()),
};
let (
Some(uniforms),
Some(downsampling_first_pipeline),
Some(downsampling_pipeline),
Some(upsampling_pipeline),
Some(upsampling_final_pipeline),
) = (
uniforms.binding(),
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),
) else { return Ok(()) };
render_context.command_encoder().push_debug_group("bloom");
// First downsample pass
{ {
let view = &BloomTextures::texture_view(&textures.texture_a, 0); let downsampling_first_bind_group =
let mut prefilter_pass = render_context
.render_device()
.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_downsampling_first_bind_group"),
layout: &downsampling_pipeline_res.bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
// Read from main texture directly
resource: BindingResource::TextureView(view_target.main_texture()),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&bind_groups.sampler),
},
BindGroupEntry {
binding: 2,
resource: uniforms.clone(),
},
],
});
let view = &bloom_texture.view(0);
let mut downsampling_first_pass =
render_context.begin_tracked_render_pass(RenderPassDescriptor { render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("bloom_prefilter_pass"), label: Some("bloom_downsampling_first_pass"),
color_attachments: &[Some(RenderPassColorAttachment { color_attachments: &[Some(RenderPassColorAttachment {
view, view,
resolve_target: None, resolve_target: None,
@ -247,17 +240,18 @@ impl Node for BloomNode {
})], })],
depth_stencil_attachment: None, depth_stencil_attachment: None,
}); });
prefilter_pass.set_render_pipeline(downsampling_prefilter_pipeline); downsampling_first_pass.set_render_pipeline(downsampling_first_pipeline);
prefilter_pass.set_bind_group( downsampling_first_pass.set_bind_group(
0, 0,
&bind_groups.prefilter_bind_group, &downsampling_first_bind_group,
&[uniform_index.index()], &[uniform_index.index()],
); );
prefilter_pass.draw(0..3, 0..1); downsampling_first_pass.draw(0..3, 0..1);
} }
for mip in 1..textures.mip_count { // Other downsample passes
let view = &BloomTextures::texture_view(&textures.texture_a, mip); for mip in 1..bloom_texture.mip_count {
let view = &bloom_texture.view(mip);
let mut downsampling_pass = let mut downsampling_pass =
render_context.begin_tracked_render_pass(RenderPassDescriptor { render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("bloom_downsampling_pass"), label: Some("bloom_downsampling_pass"),
@ -277,27 +271,40 @@ impl Node for BloomNode {
downsampling_pass.draw(0..3, 0..1); downsampling_pass.draw(0..3, 0..1);
} }
for mip in (1..textures.mip_count).rev() { // Upsample passes except the final one
let view = &BloomTextures::texture_view(&textures.texture_b, mip - 1); for mip in (1..bloom_texture.mip_count).rev() {
let view = &bloom_texture.view(mip - 1);
let mut upsampling_pass = let mut upsampling_pass =
render_context.begin_tracked_render_pass(RenderPassDescriptor { render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("bloom_upsampling_pass"), label: Some("bloom_upsampling_pass"),
color_attachments: &[Some(RenderPassColorAttachment { color_attachments: &[Some(RenderPassColorAttachment {
view, view,
resolve_target: None, resolve_target: None,
ops: Operations::default(), ops: Operations {
load: LoadOp::Load,
store: true,
},
})], })],
depth_stencil_attachment: None, depth_stencil_attachment: None,
}); });
upsampling_pass.set_render_pipeline(upsampling_pipeline); upsampling_pass.set_render_pipeline(upsampling_pipeline);
upsampling_pass.set_bind_group( upsampling_pass.set_bind_group(
0, 0,
&bind_groups.upsampling_bind_groups[mip as usize - 1], &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - mip - 1) as usize],
&[uniform_index.index()], &[uniform_index.index()],
); );
let blend = compute_blend_factor(
bloom_settings,
mip as f32,
(bloom_texture.mip_count - 1) as f32,
);
upsampling_pass.set_blend_constant(Color::rgb_linear(blend, blend, blend));
upsampling_pass.draw(0..3, 0..1); upsampling_pass.draw(0..3, 0..1);
} }
// Final upsample pass
// This is very similar to the above upsampling passes with the only difference
// being the pipeline (which itself is barely different) and the color attachment
{ {
let mut upsampling_final_pass = let mut upsampling_final_pass =
render_context.begin_tracked_render_pass(RenderPassDescriptor { render_context.begin_tracked_render_pass(RenderPassDescriptor {
@ -313,245 +320,36 @@ impl Node for BloomNode {
upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline); upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline);
upsampling_final_pass.set_bind_group( upsampling_final_pass.set_bind_group(
0, 0,
&bind_groups.upsampling_final_bind_group, &bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],
&[uniform_index.index()], &[uniform_index.index()],
); );
if let Some(viewport) = camera.viewport.as_ref() { if let Some(viewport) = camera.viewport.as_ref() {
upsampling_final_pass.set_camera_viewport(viewport); upsampling_final_pass.set_camera_viewport(viewport);
} }
let blend =
compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);
upsampling_final_pass.set_blend_constant(Color::rgb_linear(blend, blend, blend));
upsampling_final_pass.draw(0..3, 0..1); upsampling_final_pass.draw(0..3, 0..1);
} }
render_context.command_encoder().pop_debug_group();
Ok(()) Ok(())
} }
} }
#[derive(Resource)]
struct BloomPipelines {
downsampling_prefilter_pipeline: CachedRenderPipelineId,
downsampling_pipeline: CachedRenderPipelineId,
upsampling_pipeline: CachedRenderPipelineId,
upsampling_final_pipeline: CachedRenderPipelineId,
sampler: Sampler,
downsampling_bind_group_layout: BindGroupLayout,
upsampling_bind_group_layout: BindGroupLayout,
}
impl FromWorld for BloomPipelines {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let sampler = render_device.create_sampler(&SamplerDescriptor {
min_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
..Default::default()
});
let downsampling_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("bloom_downsampling_bind_group_layout"),
entries: &[
// Upsampled input texture (downsampled for final upsample)
BindGroupLayoutEntry {
binding: 0,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Sampler
BindGroupLayoutEntry {
binding: 1,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Bloom settings
BindGroupLayoutEntry {
binding: 2,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(BloomUniform::min_size()),
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
],
});
let upsampling_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("bloom_upsampling_bind_group_layout"),
entries: &[
// Downsampled input texture
BindGroupLayoutEntry {
binding: 0,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Sampler
BindGroupLayoutEntry {
binding: 1,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Bloom settings
BindGroupLayoutEntry {
binding: 2,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(BloomUniform::min_size()),
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Upsampled input texture
BindGroupLayoutEntry {
binding: 3,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
],
});
let pipeline_cache = world.resource::<PipelineCache>();
let downsampling_prefilter_pipeline =
pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("bloom_downsampling_prefilter_pipeline".into()),
layout: vec![downsampling_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "downsample_prefilter".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
});
let downsampling_pipeline =
pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("bloom_downsampling_pipeline".into()),
layout: vec![downsampling_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "downsample".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
});
let upsampling_pipeline = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("bloom_upsampling_pipeline".into()),
layout: vec![upsampling_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "upsample".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
});
let upsampling_final_pipeline =
pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("bloom_upsampling_final_pipeline".into()),
layout: vec![downsampling_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "upsample_final".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: Some(BlendState {
color: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
alpha: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::One,
operation: BlendOperation::Max,
},
}),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
});
BloomPipelines {
downsampling_prefilter_pipeline,
downsampling_pipeline,
upsampling_pipeline,
upsampling_final_pipeline,
sampler,
downsampling_bind_group_layout,
upsampling_bind_group_layout,
}
}
}
#[derive(Component)] #[derive(Component)]
struct BloomTextures { struct BloomTexture {
texture_a: CachedTexture, // First mip is half the screen resolution, successive mips are half the previous
texture_b: CachedTexture, texture: CachedTexture,
mip_count: u32, mip_count: u32,
} }
impl BloomTextures { impl BloomTexture {
fn texture_view(texture: &CachedTexture, base_mip_level: u32) -> TextureView { fn view(&self, base_mip_level: u32) -> TextureView {
texture.texture.create_view(&TextureViewDescriptor { self.texture.texture.create_view(&TextureViewDescriptor {
base_mip_level, base_mip_level,
mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), mip_level_count: NonZeroU32::new(1),
..Default::default() ..Default::default()
}) })
} }
@ -561,203 +359,144 @@ fn prepare_bloom_textures(
mut commands: Commands, mut commands: Commands,
mut texture_cache: ResMut<TextureCache>, mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<BloomUniform>>, views: Query<(Entity, &ExtractedCamera), With<BloomSettings>>,
) { ) {
let mut texture_as = HashMap::default();
let mut texture_bs = HashMap::default();
for (entity, camera) in &views { for (entity, camera) in &views {
if let Some(UVec2 { if let Some(UVec2 {
x: width, x: width,
y: height, y: height,
}) = camera.physical_viewport_size }) = camera.physical_viewport_size
{ {
let min_view = width.min(height) / 2; // How many times we can halve the resolution minus one so we don't go unnecessarily low
let mip_count = calculate_mip_count(min_view); let mip_count = MAX_MIP_DIMENSION.ilog2().max(2) - 1;
let mip_height_ratio = MAX_MIP_DIMENSION as f32 / height as f32;
let mut texture_descriptor = TextureDescriptor { let texture_descriptor = TextureDescriptor {
label: None, label: Some("bloom_texture"),
size: Extent3d { size: Extent3d {
width: (width / 2).max(1), width: ((width as f32 * mip_height_ratio).round() as u32).max(1),
height: (height / 2).max(1), height: ((height as f32 * mip_height_ratio).round() as u32).max(1),
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
mip_level_count: mip_count, mip_level_count: mip_count,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: ViewTarget::TEXTURE_FORMAT_HDR, format: BLOOM_TEXTURE_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
}; };
texture_descriptor.label = Some("bloom_texture_a"); commands.entity(entity).insert(BloomTexture {
let texture_a = texture_as texture: texture_cache.get(&render_device, texture_descriptor),
.entry(camera.target.clone())
.or_insert_with(|| texture_cache.get(&render_device, texture_descriptor.clone()))
.clone();
texture_descriptor.label = Some("bloom_texture_b");
let texture_b = texture_bs
.entry(camera.target.clone())
.or_insert_with(|| texture_cache.get(&render_device, texture_descriptor))
.clone();
commands.entity(entity).insert(BloomTextures {
texture_a,
texture_b,
mip_count, mip_count,
}); });
} }
} }
} }
/// The uniform struct extracted from [`BloomSettings`] attached to a [`Camera`].
/// Will be available for use in the Bloom shader.
#[doc(hidden)]
#[derive(Component, ShaderType, Clone)]
pub struct BloomUniform {
threshold: f32,
knee: f32,
scale: f32,
intensity: f32,
viewport: Vec4,
}
#[derive(Component)] #[derive(Component)]
struct BloomBindGroups { struct BloomBindGroups {
prefilter_bind_group: BindGroup,
downsampling_bind_groups: Box<[BindGroup]>, downsampling_bind_groups: Box<[BindGroup]>,
upsampling_bind_groups: Box<[BindGroup]>, upsampling_bind_groups: Box<[BindGroup]>,
upsampling_final_bind_group: BindGroup, sampler: Sampler,
} }
fn queue_bloom_bind_groups( fn queue_bloom_bind_groups(
mut commands: Commands, mut commands: Commands,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
pipelines: Res<BloomPipelines>, downsampling_pipeline: Res<BloomDownsamplingPipeline>,
uniforms: Res<ComponentUniforms<BloomUniform>>, upsampling_pipeline: Res<BloomUpsamplingPipeline>,
views: Query<(Entity, &ViewTarget, &BloomTextures)>, views: Query<(Entity, &BloomTexture)>,
uniforms: Res<ComponentUniforms<BloomUniforms>>,
) { ) {
if let Some(uniforms) = uniforms.binding() { let sampler = &downsampling_pipeline.sampler;
for (entity, view_target, textures) in &views {
let prefilter_bind_group = render_device.create_bind_group(&BindGroupDescriptor { for (entity, bloom_texture) in &views {
label: Some("bloom_prefilter_bind_group"), let bind_group_count = bloom_texture.mip_count as usize - 1;
layout: &pipelines.downsampling_bind_group_layout,
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in 1..bloom_texture.mip_count {
downsampling_bind_groups.push(render_device.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_downsampling_bind_group"),
layout: &downsampling_pipeline.bind_group_layout,
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: BindingResource::TextureView(view_target.main_texture()), resource: BindingResource::TextureView(&bloom_texture.view(mip - 1)),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, binding: 1,
resource: BindingResource::Sampler(&pipelines.sampler), resource: BindingResource::Sampler(sampler),
}, },
BindGroupEntry { BindGroupEntry {
binding: 2, binding: 2,
resource: uniforms.clone(), resource: uniforms.binding().unwrap(),
}, },
], ],
}); }));
let bind_group_count = textures.mip_count as usize - 1;
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in 1..textures.mip_count {
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_downsampling_bind_group"),
layout: &pipelines.downsampling_bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&BloomTextures::texture_view(
&textures.texture_a,
mip - 1,
)),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&pipelines.sampler),
},
BindGroupEntry {
binding: 2,
resource: uniforms.clone(),
},
],
});
downsampling_bind_groups.push(bind_group);
}
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in 1..textures.mip_count {
let up = BloomTextures::texture_view(&textures.texture_a, mip - 1);
let org = BloomTextures::texture_view(
if mip == textures.mip_count - 1 {
&textures.texture_a
} else {
&textures.texture_b
},
mip,
);
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_upsampling_bind_group"),
layout: &pipelines.upsampling_bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&org),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&pipelines.sampler),
},
BindGroupEntry {
binding: 2,
resource: uniforms.clone(),
},
BindGroupEntry {
binding: 3,
resource: BindingResource::TextureView(&up),
},
],
});
upsampling_bind_groups.push(bind_group);
}
let upsampling_final_bind_group =
render_device.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_upsampling_final_bind_group"),
layout: &pipelines.downsampling_bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&BloomTextures::texture_view(
&textures.texture_b,
0,
)),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&pipelines.sampler),
},
BindGroupEntry {
binding: 2,
resource: uniforms.clone(),
},
],
});
commands.entity(entity).insert(BloomBindGroups {
prefilter_bind_group,
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
upsampling_final_bind_group,
});
} }
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in (0..bloom_texture.mip_count).rev() {
upsampling_bind_groups.push(render_device.create_bind_group(&BindGroupDescriptor {
label: Some("bloom_upsampling_bind_group"),
layout: &upsampling_pipeline.bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&bloom_texture.view(mip)),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
BindGroupEntry {
binding: 2,
resource: uniforms.binding().unwrap(),
},
],
}));
}
commands.entity(entity).insert(BloomBindGroups {
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
sampler: sampler.clone(),
});
} }
} }
fn calculate_mip_count(min_view: u32) -> u32 { /// Calculates blend intensities of blur pyramid levels
((min_view as f32).log2().round() as i32 - 3).max(1) as u32 /// during the upsampling + compositing stage.
///
/// The function assumes all pyramid levels are upsampled and
/// blended into higher frequency ones using this function to
/// calculate blend levels every time. The final (highest frequency)
/// pyramid level in not blended into anything therefore this function
/// is not applied to it. As a result, the *mip* parameter of 0 indicates
/// the second-highest frequency pyramid level (in our case that is the
/// 0th mip of the bloom texture with the original image being the
/// actual highest frequency level).
///
/// Parameters:
/// * *mip* - the index of the lower frequency pyramid level (0 - max_mip, where 0 indicates highest frequency mip but not the highest frequency image).
/// * *max_mip* - the index of the lowest frequency pyramid level.
///
/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
/// [`BloomSettings`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
#[allow(clippy::doc_markdown)]
fn compute_blend_factor(bloom_settings: &BloomSettings, mip: f32, max_mip: f32) -> f32 {
let mut lf_boost = (1.0
- (1.0 - (mip / max_mip)).powf(1.0 / (1.0 - bloom_settings.low_frequency_boost_curvature)))
* bloom_settings.low_frequency_boost;
let high_pass_lq = 1.0
- (((mip / max_mip) - bloom_settings.high_pass_frequency)
/ bloom_settings.high_pass_frequency)
.clamp(0.0, 1.0);
lf_boost *= match bloom_settings.composite_mode {
BloomCompositeMode::EnergyConserving => 1.0 - bloom_settings.intensity,
BloomCompositeMode::Additive => 1.0,
};
(bloom_settings.intensity + lf_boost) * high_pass_lq
} }

View file

@ -0,0 +1,215 @@
use super::downsampling_pipeline::BloomUniforms;
use bevy_ecs::{prelude::Component, query::QueryItem};
use bevy_math::{UVec4, Vec4};
use bevy_reflect::Reflect;
use bevy_render::{extract_component::ExtractComponent, prelude::Camera};
/// Applies a bloom effect to an HDR-enabled 2d or 3d camera.
///
/// Bloom emulates an effect found in real cameras and the human eye,
/// causing halos to appear around very bright parts of the scene.
///
/// See also <https://en.wikipedia.org/wiki/Bloom_(shader_effect)>.
///
/// # Usage Notes
///
/// **Bloom is currently not compatible with WebGL2.**
///
/// Often used in conjunction with `bevy_pbr::StandardMaterial::emissive`
/// for 3d meshes or [`bevy_render::color::Color::Hsla`]`::lightness` for 2d sprites.
///
/// Bloom is best used alongside a tonemapping function that desaturates bright colors,
/// such as [`crate::tonemapping::Tonemapping::TonyMcMapface`].
///
/// Bevy's implementation uses a parametric curve to blend between a set of
/// blurred (lower frequency) images generated from the camera's view.
/// See <https://starlederer.github.io/bloom/> for a visualization of the parametric curve
/// used in Bevy as well as a visualization of the curve's respective scattering profile.
#[allow(clippy::doc_markdown)]
#[derive(Component, Reflect, Clone)]
pub struct BloomSettings {
/// Controls the baseline of how much the image is scattered (default: 0.15).
///
/// # In energy-conserving mode
/// The value represents how likely the light is to scatter.
///
/// The value should be between 0.0 and 1.0 where:
/// * 0.0 means no bloom
/// * 1.0 means the light is scattered as much as possible
///
/// # In additive mode
/// The value represents how much scattered light is added to
/// the image to create the glow effect.
///
/// In this configuration:
/// * 0.0 means no bloom
/// * > 0.0 means a proportionate amount of scattered light is added
pub intensity: f32,
/// Low frequency contribution boost.
/// Controls how much more likely the light
/// is to scatter completely sideways (low frequency image).
///
/// Comparable to a low shelf boost on an equalizer.
///
/// # In energy-conserving mode
/// The value should be between 0.0 and 1.0 where:
/// * 0.0 means low frequency light uses base intensity for blend factor calculation
/// * 1.0 means low frequency light contributes at full power
///
/// # In additive mode
/// The value represents how much scattered light is added to
/// the image to create the glow effect.
///
/// In this configuration:
/// * 0.0 means no bloom
/// * > 0.0 means a proportionate amount of scattered light is added
pub low_frequency_boost: f32,
/// Low frequency contribution boost curve.
/// Controls the curvature of the blend factor function
/// making frequencies next to the lowest ones contribute more.
///
/// Somewhat comparable to the Q factor of an equalizer node.
///
/// Valid range:
/// * 0.0 - base base intensity and boosted intensity are linearly interpolated
/// * 1.0 - all frequencies below maximum are at boosted intensity level
pub low_frequency_boost_curvature: f32,
/// Tightens how much the light scatters (default: 1.0).
///
/// Valid range:
/// * 0.0 - maximum scattering angle is 0 degrees (no scattering)
/// * 1.0 - maximum scattering angle is 90 degrees
pub high_pass_frequency: f32,
pub prefilter_settings: BloomPrefilterSettings,
/// Controls whether bloom textures
/// are blended between or added to each other. Useful
/// if image brightening is desired and a must-change
/// if `prefilter_settings` are used.
///
/// # Recommendation
/// Set to [`BloomCompositeMode::Additive`] if `prefilter_settings` are
/// configured in a non-energy-conserving way,
/// otherwise set to [`BloomCompositeMode::EnergyConserving`].
pub composite_mode: BloomCompositeMode,
}
impl BloomSettings {
/// The default bloom preset.
pub const NATURAL: Self = Self {
intensity: 0.15,
low_frequency_boost: 0.7,
low_frequency_boost_curvature: 0.95,
high_pass_frequency: 1.0,
prefilter_settings: BloomPrefilterSettings {
threshold: 0.0,
threshold_softness: 0.0,
},
composite_mode: BloomCompositeMode::EnergyConserving,
};
/// A preset that's similiar to how older games did bloom.
pub const OLD_SCHOOL: Self = Self {
intensity: 0.05,
low_frequency_boost: 0.7,
low_frequency_boost_curvature: 0.95,
high_pass_frequency: 1.0,
prefilter_settings: BloomPrefilterSettings {
threshold: 0.6,
threshold_softness: 0.2,
},
composite_mode: BloomCompositeMode::Additive,
};
/// A preset that applies a very strong bloom, and blurs the whole screen.
pub const SCREEN_BLUR: Self = Self {
intensity: 1.0,
low_frequency_boost: 0.0,
low_frequency_boost_curvature: 0.0,
high_pass_frequency: 1.0 / 3.0,
prefilter_settings: BloomPrefilterSettings {
threshold: 0.0,
threshold_softness: 0.0,
},
composite_mode: BloomCompositeMode::EnergyConserving,
};
}
impl Default for BloomSettings {
fn default() -> Self {
Self::NATURAL
}
}
/// Applies a threshold filter to the input image to extract the brightest
/// regions before blurring them and compositing back onto the original image.
/// These settings are useful when emulating the 1990s-2000s game look.
///
/// # Considerations
/// * Changing these settings creates a physically inaccurate image
/// * Changing these settings makes it easy to make the final result look worse
/// * Non-default prefilter settings should be used in conjuction with [`BloomCompositeMode::Additive`]
#[derive(Default, Clone, Reflect)]
pub struct BloomPrefilterSettings {
/// Baseline of the quadratic threshold curve (default: 0.0).
///
/// RGB values under the threshold curve will not contribute to the effect.
pub threshold: f32,
/// Controls how much to blend between the thresholded and non-thresholded colors (default: 0.0).
///
/// 0.0 = Abrupt threshold, no blending
/// 1.0 = Fully soft threshold
///
/// Values outside of the range [0.0, 1.0] will be clamped.
pub threshold_softness: f32,
}
#[derive(Clone, Reflect, PartialEq, Eq, Hash, Copy)]
pub enum BloomCompositeMode {
EnergyConserving,
Additive,
}
impl ExtractComponent for BloomSettings {
type Query = (&'static Self, &'static Camera);
type Filter = ();
type Out = (Self, BloomUniforms);
fn extract_component((settings, camera): QueryItem<'_, Self::Query>) -> Option<Self::Out> {
match (
camera.physical_viewport_rect(),
camera.physical_viewport_size(),
camera.physical_target_size(),
camera.is_active,
camera.hdr,
) {
(Some((origin, _)), Some(size), Some(target_size), true, true) => {
let threshold = settings.prefilter_settings.threshold;
let threshold_softness = settings.prefilter_settings.threshold_softness;
let knee = threshold * threshold_softness.clamp(0.0, 1.0);
let uniform = BloomUniforms {
threshold_precomputations: Vec4::new(
threshold,
threshold - knee,
2.0 * knee,
0.25 / (knee + 0.00001),
),
viewport: UVec4::new(origin.x, origin.y, size.x, size.y).as_vec4()
/ UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y)
.as_vec4(),
aspect: size.x as f32 / size.y as f32,
};
Some((settings.clone(), uniform))
}
_ => None,
}
}
}

View file

@ -0,0 +1,176 @@
use super::{
downsampling_pipeline::BloomUniforms, BloomCompositeMode, BloomSettings, BLOOM_SHADER_HANDLE,
BLOOM_TEXTURE_FORMAT,
};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use bevy_ecs::{
prelude::{Component, Entity},
system::{Commands, Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_render::{render_resource::*, renderer::RenderDevice, view::ViewTarget};
#[derive(Component)]
pub struct UpsamplingPipelineIds {
pub id_main: CachedRenderPipelineId,
pub id_final: CachedRenderPipelineId,
}
#[derive(Resource)]
pub struct BloomUpsamplingPipeline {
pub bind_group_layout: BindGroupLayout,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct BloomUpsamplingPipelineKeys {
composite_mode: BloomCompositeMode,
final_pipeline: bool,
}
impl FromWorld for BloomUpsamplingPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("bloom_upsampling_bind_group_layout"),
entries: &[
// Input texture
BindGroupLayoutEntry {
binding: 0,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
// Sampler
BindGroupLayoutEntry {
binding: 1,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
visibility: ShaderStages::FRAGMENT,
count: None,
},
// BloomUniforms
BindGroupLayoutEntry {
binding: 2,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(BloomUniforms::min_size()),
},
visibility: ShaderStages::FRAGMENT,
count: None,
},
],
});
BloomUpsamplingPipeline { bind_group_layout }
}
}
impl SpecializedRenderPipeline for BloomUpsamplingPipeline {
type Key = BloomUpsamplingPipelineKeys;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let texture_format = if key.final_pipeline {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
BLOOM_TEXTURE_FORMAT
};
let color_blend = match key.composite_mode {
BloomCompositeMode::EnergyConserving => {
// At the time of developing this we decided to blend our
// blur pyramid levels using native WGPU render pass blend
// constants. They are set in the bloom node's run function.
// This seemed like a good approach at the time which allowed
// us to perform complex calculations for blend levels on the CPU,
// however, we missed the fact that this prevented us from using
// textures to customize bloom appearance on individual parts
// of the screen and create effects such as lens dirt or
// screen blur behind certain UI elements.
//
// TODO: Use alpha instead of blend constants and move
// compute_blend_factor to the shader. The shader
// will likely need to know current mip number or
// mip "angle" (original texture is 0deg, max mip is 90deg)
// so make sure you give it that as a uniform.
// That does have to be provided per each pass unlike other
// uniforms that are set once.
BlendComponent {
src_factor: BlendFactor::Constant,
dst_factor: BlendFactor::OneMinusConstant,
operation: BlendOperation::Add,
}
}
BloomCompositeMode::Additive => BlendComponent {
src_factor: BlendFactor::Constant,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
};
RenderPipelineDescriptor {
label: Some("bloom_upsampling_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "upsample".into(),
targets: vec![Some(ColorTargetState {
format: texture_format,
blend: Some(BlendState {
color: color_blend,
alpha: BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
}
}
}
pub fn prepare_upsampling_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BloomUpsamplingPipeline>>,
pipeline: Res<BloomUpsamplingPipeline>,
views: Query<(Entity, &BloomSettings)>,
) {
for (entity, settings) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomUpsamplingPipelineKeys {
composite_mode: settings.composite_mode,
final_pipeline: false,
},
);
let pipeline_final_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomUpsamplingPipelineKeys {
composite_mode: settings.composite_mode,
final_pipeline: true,
},
);
commands.entity(entity).insert(UpsamplingPipelineIds {
id_main: pipeline_id,
id_final: pipeline_final_id,
});
}
}

View file

@ -171,6 +171,7 @@ impl Tonemapping {
*self != Tonemapping::None *self != Tonemapping::None
} }
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TonemappingPipelineKey { pub struct TonemappingPipelineKey {
deband_dither: DebandDither, deband_dither: DebandDither,

View file

@ -258,6 +258,10 @@ fn tonemapping_reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
return tonemapping_change_luminance(color, l_new); return tonemapping_change_luminance(color, l_new);
} }
fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
return pow(color, vec3<f32>(1.0 / 2.2));
}
// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49 // Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49
// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf // https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> { fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> {

212
examples/2d/bloom_2d.rs Normal file
View file

@ -0,0 +1,212 @@
//! Illustrates bloom post-processing in 2d.
use bevy::{
core_pipeline::{
bloom::{BloomCompositeMode, BloomSettings},
tonemapping::Tonemapping,
},
prelude::*,
sprite::MaterialMesh2dBundle,
};
fn main() {
App::new()
.insert_resource(ClearColor(Color::DARK_GRAY))
.add_plugins(DefaultPlugins)
.add_startup_system(setup)
.add_system(update_bloom_settings)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Camera2dBundle {
camera: Camera {
hdr: true, // 1. HDR is required for bloom
..default()
},
tonemapping: Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended
..default()
},
BloomSettings::default(), // 3. Enable bloom for the camera
));
// Sprite
commands.spawn(SpriteBundle {
texture: asset_server.load("branding/icon.png"),
sprite: Sprite {
color: Color::rgb(5.0, 5.0, 5.0), // 4. Put something bright in a dark environment to see the effect
custom_size: Some(Vec2::splat(160.0)),
..default()
},
..default()
});
// Circle mesh
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(100.).into()).into(),
// 4. Put something bright in a dark environment to see the effect
material: materials.add(ColorMaterial::from(Color::rgb(7.5, 0.0, 7.5))),
transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
..default()
});
// Hexagon mesh
commands.spawn(MaterialMesh2dBundle {
mesh: meshes
.add(shape::RegularPolygon::new(100., 6).into())
.into(),
// 4. Put something bright in a dark environment to see the effect
material: materials.add(ColorMaterial::from(Color::rgb(6.25, 9.4, 9.1))),
transform: Transform::from_translation(Vec3::new(200., 0., 0.)),
..default()
});
// UI
commands.spawn(
TextBundle::from_section(
"",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
},
)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(10.0),
left: Val::Px(10.0),
..default()
},
..default()
}),
);
}
// ------------------------------------------------------------------------------------------------
fn update_bloom_settings(
mut camera: Query<(Entity, Option<&mut BloomSettings>), With<Camera>>,
mut text: Query<&mut Text>,
mut commands: Commands,
keycode: Res<Input<KeyCode>>,
time: Res<Time>,
) {
let bloom_settings = camera.single_mut();
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
match bloom_settings {
(entity, Some(mut bloom_settings)) => {
*text = "BloomSettings (Toggle: Space)\n".to_string();
text.push_str(&format!("(Q/A) Intensity: {}\n", bloom_settings.intensity));
text.push_str(&format!(
"(W/S) Low-frequency boost: {}\n",
bloom_settings.low_frequency_boost
));
text.push_str(&format!(
"(E/D) Low-frequency boost curvature: {}\n",
bloom_settings.low_frequency_boost_curvature
));
text.push_str(&format!(
"(R/F) High-pass frequency: {}\n",
bloom_settings.high_pass_frequency
));
text.push_str(&format!(
"(T/G) Mode: {}\n",
match bloom_settings.composite_mode {
BloomCompositeMode::EnergyConserving => "Energy-conserving",
BloomCompositeMode::Additive => "Additive",
}
));
text.push_str(&format!(
"(Y/H) Threshold: {}\n",
bloom_settings.prefilter_settings.threshold
));
text.push_str(&format!(
"(U/J) Threshold softness: {}\n",
bloom_settings.prefilter_settings.threshold_softness
));
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).remove::<BloomSettings>();
}
let dt = time.delta_seconds();
if keycode.pressed(KeyCode::A) {
bloom_settings.intensity -= dt / 10.0;
}
if keycode.pressed(KeyCode::Q) {
bloom_settings.intensity += dt / 10.0;
}
bloom_settings.intensity = bloom_settings.intensity.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::S) {
bloom_settings.low_frequency_boost -= dt / 10.0;
}
if keycode.pressed(KeyCode::W) {
bloom_settings.low_frequency_boost += dt / 10.0;
}
bloom_settings.low_frequency_boost = bloom_settings.low_frequency_boost.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::D) {
bloom_settings.low_frequency_boost_curvature -= dt / 10.0;
}
if keycode.pressed(KeyCode::E) {
bloom_settings.low_frequency_boost_curvature += dt / 10.0;
}
bloom_settings.low_frequency_boost_curvature =
bloom_settings.low_frequency_boost_curvature.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::F) {
bloom_settings.high_pass_frequency -= dt / 10.0;
}
if keycode.pressed(KeyCode::R) {
bloom_settings.high_pass_frequency += dt / 10.0;
}
bloom_settings.high_pass_frequency = bloom_settings.high_pass_frequency.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::G) {
bloom_settings.composite_mode = BloomCompositeMode::Additive;
}
if keycode.pressed(KeyCode::T) {
bloom_settings.composite_mode = BloomCompositeMode::EnergyConserving;
}
if keycode.pressed(KeyCode::H) {
bloom_settings.prefilter_settings.threshold -= dt;
}
if keycode.pressed(KeyCode::Y) {
bloom_settings.prefilter_settings.threshold += dt;
}
bloom_settings.prefilter_settings.threshold =
bloom_settings.prefilter_settings.threshold.max(0.0);
if keycode.pressed(KeyCode::J) {
bloom_settings.prefilter_settings.threshold_softness -= dt / 10.0;
}
if keycode.pressed(KeyCode::U) {
bloom_settings.prefilter_settings.threshold_softness += dt / 10.0;
}
bloom_settings.prefilter_settings.threshold_softness = bloom_settings
.prefilter_settings
.threshold_softness
.clamp(0.0, 1.0);
}
(entity, None) => {
*text = "Bloom: Off (Toggle: Space)".to_string();
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).insert(BloomSettings::default());
}
}
}
}

View file

@ -1,166 +0,0 @@
//! Illustrates bloom configuration using HDR and emissive materials.
use bevy::{core_pipeline::bloom::BloomSettings, prelude::*};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_startup_system(setup_scene)
.add_system(update_bloom_settings)
.add_system(bounce_spheres)
.run();
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Camera3dBundle {
camera: Camera {
hdr: true, // 1. HDR must be enabled on the camera
..default()
},
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
BloomSettings::default(), // 2. Enable bloom for the camera
));
let material_emissive = materials.add(StandardMaterial {
emissive: Color::rgb_linear(5.2, 1.2, 0.8), // 3. Set StandardMaterial::emissive using Color::rgb_linear, for entities we want to apply bloom to
..default()
});
let material_non_emissive = materials.add(StandardMaterial {
base_color: Color::GRAY,
..default()
});
let mesh = meshes.add(
shape::Icosphere {
radius: 0.5,
subdivisions: 5,
}
.try_into()
.unwrap(),
);
for x in -10..10 {
for z in -10..10 {
let mut hasher = DefaultHasher::new();
(x, z).hash(&mut hasher);
let rand = hasher.finish() % 2 == 0;
let material = if rand {
material_emissive.clone()
} else {
material_non_emissive.clone()
};
commands.spawn((
PbrBundle {
mesh: mesh.clone(),
material,
transform: Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0),
..default()
},
Bouncing,
));
}
}
commands.spawn(
TextBundle::from_section(
"",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::BLACK,
},
)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: Val::Px(10.0),
left: Val::Px(10.0),
..default()
},
..default()
}),
);
}
// ------------------------------------------------------------------------------------------------
fn update_bloom_settings(
mut camera: Query<&mut BloomSettings>,
mut text: Query<&mut Text>,
keycode: Res<Input<KeyCode>>,
time: Res<Time>,
) {
let mut bloom_settings = camera.single_mut();
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
*text = "BloomSettings\n".to_string();
text.push_str("-------------\n");
text.push_str(&format!("Threshold: {}\n", bloom_settings.threshold));
text.push_str(&format!("Knee: {}\n", bloom_settings.knee));
text.push_str(&format!("Scale: {}\n", bloom_settings.scale));
text.push_str(&format!("Intensity: {}\n", bloom_settings.intensity));
text.push_str("\n\n");
text.push_str("Controls (-/+)\n");
text.push_str("---------------\n");
text.push_str("Q/W - Threshold\n");
text.push_str("E/R - Knee\n");
text.push_str("A/S - Scale\n");
text.push_str("D/F - Intensity\n");
let dt = time.delta_seconds();
if keycode.pressed(KeyCode::Q) {
bloom_settings.threshold -= dt;
}
if keycode.pressed(KeyCode::W) {
bloom_settings.threshold += dt;
}
if keycode.pressed(KeyCode::E) {
bloom_settings.knee -= dt;
}
if keycode.pressed(KeyCode::R) {
bloom_settings.knee += dt;
}
if keycode.pressed(KeyCode::A) {
bloom_settings.scale -= dt;
}
if keycode.pressed(KeyCode::S) {
bloom_settings.scale += dt;
}
if keycode.pressed(KeyCode::D) {
bloom_settings.intensity -= dt;
}
if keycode.pressed(KeyCode::F) {
bloom_settings.intensity += dt;
}
}
#[derive(Component)]
struct Bouncing;
fn bounce_spheres(time: Res<Time>, mut query: Query<&mut Transform, With<Bouncing>>) {
for mut transform in query.iter_mut() {
transform.translation.y =
(transform.translation.x + transform.translation.z + time.elapsed_seconds()).sin();
}
}

247
examples/3d/bloom_3d.rs Normal file
View file

@ -0,0 +1,247 @@
//! Illustrates bloom post-processing using HDR and emissive materials.
use bevy::{
core_pipeline::{
bloom::{BloomCompositeMode, BloomSettings},
tonemapping::Tonemapping,
},
prelude::*,
};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
fn main() {
App::new()
.insert_resource(ClearColor(Color::DARK_GRAY))
.add_plugins(DefaultPlugins)
.add_startup_system(setup_scene)
.add_system(update_bloom_settings)
.add_system(bounce_spheres)
.run();
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Camera3dBundle {
camera: Camera {
hdr: true, // 1. HDR is required for bloom
..default()
},
tonemapping: Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
BloomSettings::default(), // 3. Enable bloom for the camera
));
let material_emissive1 = materials.add(StandardMaterial {
emissive: Color::rgb_linear(5000.0, 500.0, 50.0), // 4. Put something bright in a dark environment to see the effect
..default()
});
let material_emissive2 = materials.add(StandardMaterial {
emissive: Color::rgb_linear(50.0, 5000.0, 500.0),
..default()
});
let material_emissive3 = materials.add(StandardMaterial {
emissive: Color::rgb_linear(500.0, 50.0, 5000.0),
..default()
});
let material_non_emissive = materials.add(StandardMaterial {
base_color: Color::GRAY,
..default()
});
let mesh = meshes.add(
shape::Icosphere {
radius: 0.5,
subdivisions: 5,
}
.try_into()
.unwrap(),
);
for x in -10..10 {
for z in -10..10 {
let mut hasher = DefaultHasher::new();
(x, z).hash(&mut hasher);
let rand = (hasher.finish() - 2) % 6;
let material = match rand {
0 => material_emissive1.clone(),
1 => material_emissive2.clone(),
2 => material_emissive3.clone(),
3 | 4 | 5 => material_non_emissive.clone(),
_ => unreachable!(),
};
commands.spawn((
PbrBundle {
mesh: mesh.clone(),
material,
transform: Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0),
..default()
},
Bouncing,
));
}
}
commands.spawn(
TextBundle::from_section(
"",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::BLACK,
},
)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(10.0),
left: Val::Px(10.0),
..default()
},
..default()
}),
);
}
// ------------------------------------------------------------------------------------------------
fn update_bloom_settings(
mut camera: Query<(Entity, Option<&mut BloomSettings>), With<Camera>>,
mut text: Query<&mut Text>,
mut commands: Commands,
keycode: Res<Input<KeyCode>>,
time: Res<Time>,
) {
let bloom_settings = camera.single_mut();
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
match bloom_settings {
(entity, Some(mut bloom_settings)) => {
*text = "BloomSettings (Toggle: Space)\n".to_string();
text.push_str(&format!("(Q/A) Intensity: {}\n", bloom_settings.intensity));
text.push_str(&format!(
"(W/S) Low-frequency boost: {}\n",
bloom_settings.low_frequency_boost
));
text.push_str(&format!(
"(E/D) Low-frequency boost curvature: {}\n",
bloom_settings.low_frequency_boost_curvature
));
text.push_str(&format!(
"(R/F) High-pass frequency: {}\n",
bloom_settings.high_pass_frequency
));
text.push_str(&format!(
"(T/G) Mode: {}\n",
match bloom_settings.composite_mode {
BloomCompositeMode::EnergyConserving => "Energy-conserving",
BloomCompositeMode::Additive => "Additive",
}
));
text.push_str(&format!(
"(Y/H) Threshold: {}\n",
bloom_settings.prefilter_settings.threshold
));
text.push_str(&format!(
"(U/J) Threshold softness: {}\n",
bloom_settings.prefilter_settings.threshold_softness
));
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).remove::<BloomSettings>();
}
let dt = time.delta_seconds();
if keycode.pressed(KeyCode::A) {
bloom_settings.intensity -= dt / 10.0;
}
if keycode.pressed(KeyCode::Q) {
bloom_settings.intensity += dt / 10.0;
}
bloom_settings.intensity = bloom_settings.intensity.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::S) {
bloom_settings.low_frequency_boost -= dt / 10.0;
}
if keycode.pressed(KeyCode::W) {
bloom_settings.low_frequency_boost += dt / 10.0;
}
bloom_settings.low_frequency_boost = bloom_settings.low_frequency_boost.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::D) {
bloom_settings.low_frequency_boost_curvature -= dt / 10.0;
}
if keycode.pressed(KeyCode::E) {
bloom_settings.low_frequency_boost_curvature += dt / 10.0;
}
bloom_settings.low_frequency_boost_curvature =
bloom_settings.low_frequency_boost_curvature.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::F) {
bloom_settings.high_pass_frequency -= dt / 10.0;
}
if keycode.pressed(KeyCode::R) {
bloom_settings.high_pass_frequency += dt / 10.0;
}
bloom_settings.high_pass_frequency = bloom_settings.high_pass_frequency.clamp(0.0, 1.0);
if keycode.pressed(KeyCode::G) {
bloom_settings.composite_mode = BloomCompositeMode::Additive;
}
if keycode.pressed(KeyCode::T) {
bloom_settings.composite_mode = BloomCompositeMode::EnergyConserving;
}
if keycode.pressed(KeyCode::H) {
bloom_settings.prefilter_settings.threshold -= dt;
}
if keycode.pressed(KeyCode::Y) {
bloom_settings.prefilter_settings.threshold += dt;
}
bloom_settings.prefilter_settings.threshold =
bloom_settings.prefilter_settings.threshold.max(0.0);
if keycode.pressed(KeyCode::J) {
bloom_settings.prefilter_settings.threshold_softness -= dt / 10.0;
}
if keycode.pressed(KeyCode::U) {
bloom_settings.prefilter_settings.threshold_softness += dt / 10.0;
}
bloom_settings.prefilter_settings.threshold_softness = bloom_settings
.prefilter_settings
.threshold_softness
.clamp(0.0, 1.0);
}
(entity, None) => {
*text = "Bloom: Off (Toggle: Space)".to_string();
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).insert(BloomSettings::default());
}
}
}
}
#[derive(Component)]
struct Bouncing;
fn bounce_spheres(time: Res<Time>, mut query: Query<&mut Transform, With<Bouncing>>) {
for mut transform in query.iter_mut() {
transform.translation.y =
(transform.translation.x + transform.translation.z + time.elapsed_seconds()).sin();
}
}

View file

@ -87,6 +87,7 @@ Example | Description
Example | Description Example | Description
--- | --- --- | ---
[2D Bloom](../examples/2d/bloom_2d.rs) | Illustrates bloom post-processing in 2d
[2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions
[2D Shapes](../examples/2d/2d_shapes.rs) | Renders a rectangle, circle, and hexagon [2D Shapes](../examples/2d/2d_shapes.rs) | Renders a rectangle, circle, and hexagon
[Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis
@ -105,11 +106,11 @@ Example | Description
Example | Description Example | Description
--- | --- --- | ---
[3D Bloom](../examples/3d/bloom_3d.rs) | Illustrates bloom configuration using HDR and emissive materials
[3D Scene](../examples/3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting [3D Scene](../examples/3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
[3D Shapes](../examples/3d/3d_shapes.rs) | A scene showcasing the built-in 3D shapes [3D Shapes](../examples/3d/3d_shapes.rs) | A scene showcasing the built-in 3D shapes
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect [Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
[Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes [Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes
[Bloom](../examples/3d/bloom.rs) | Illustrates bloom configuration using HDR and emissive materials
[FXAA](../examples/3d/fxaa.rs) | Compares MSAA (Multi-Sample Anti-Aliasing) and FXAA (Fast Approximate Anti-Aliasing) [FXAA](../examples/3d/fxaa.rs) | Compares MSAA (Multi-Sample Anti-Aliasing) and FXAA (Fast Approximate Anti-Aliasing)
[Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect [Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect
[Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene [Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene