mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
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:
parent
cbbf8ac575
commit
2a7000a738
12 changed files with 1409 additions and 772 deletions
20
Cargo.toml
20
Cargo.toml
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
185
crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs
Normal file
185
crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
|
||||||
label: Some("bloom_prefilter_bind_group"),
|
|
||||||
layout: &pipelines.downsampling_bind_group_layout,
|
|
||||||
entries: &[
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: BindingResource::TextureView(view_target.main_texture()),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: BindingResource::Sampler(&pipelines.sampler),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: uniforms.clone(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
let bind_group_count = textures.mip_count as usize - 1;
|
for (entity, bloom_texture) in &views {
|
||||||
|
let bind_group_count = bloom_texture.mip_count as usize - 1;
|
||||||
|
|
||||||
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
|
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
|
||||||
for mip in 1..textures.mip_count {
|
for mip in 1..bloom_texture.mip_count {
|
||||||
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
|
downsampling_bind_groups.push(render_device.create_bind_group(&BindGroupDescriptor {
|
||||||
label: Some("bloom_downsampling_bind_group"),
|
label: Some("bloom_downsampling_bind_group"),
|
||||||
layout: &pipelines.downsampling_bind_group_layout,
|
layout: &downsampling_pipeline.bind_group_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: BindingResource::TextureView(&BloomTextures::texture_view(
|
resource: BindingResource::TextureView(&bloom_texture.view(mip - 1)),
|
||||||
&textures.texture_a,
|
|
||||||
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(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
}));
|
||||||
|
|
||||||
downsampling_bind_groups.push(bind_group);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
|
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
|
||||||
for mip in 1..textures.mip_count {
|
for mip in (0..bloom_texture.mip_count).rev() {
|
||||||
let up = BloomTextures::texture_view(&textures.texture_a, mip - 1);
|
upsampling_bind_groups.push(render_device.create_bind_group(&BindGroupDescriptor {
|
||||||
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"),
|
label: Some("bloom_upsampling_bind_group"),
|
||||||
layout: &pipelines.upsampling_bind_group_layout,
|
layout: &upsampling_pipeline.bind_group_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: BindingResource::TextureView(&org),
|
resource: BindingResource::TextureView(&bloom_texture.view(mip)),
|
||||||
},
|
},
|
||||||
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(),
|
||||||
},
|
|
||||||
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 {
|
commands.entity(entity).insert(BloomBindGroups {
|
||||||
prefilter_bind_group,
|
|
||||||
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
|
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
|
||||||
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
|
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
|
||||||
upsampling_final_bind_group,
|
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
|
||||||
}
|
}
|
||||||
|
|
215
crates/bevy_core_pipeline/src/bloom/settings.rs
Normal file
215
crates/bevy_core_pipeline/src/bloom/settings.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs
Normal file
176
crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
212
examples/2d/bloom_2d.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
247
examples/3d/bloom_3d.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue