diff --git a/Cargo.toml b/Cargo.toml index 3226a40152643..f8f26109910da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -313,6 +313,16 @@ description = "Illustrates spot lights" category = "3D Rendering" wasm = true +[[example]] +name = "bloom" +path = "examples/3d/bloom.rs" + +[package.metadata.example.bloom] +name = "Bloom" +description = "Illustrates bloom configuration using HDR and emissive materials" +category = "3D Rendering" +wasm = false + [[example]] name = "load_gltf" path = "examples/3d/load_gltf.rs" diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 2f984c0404575..6f16b9439ee75 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -25,6 +25,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.9.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.9.0-dev" } bevy_render = { path = "../bevy_render", version = "0.9.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.9.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.9.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.9.0-dev" } serde = { version = "1", features = ["derive"] } diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl new file mode 100644 index 0000000000000..0e6addeef3ee3 --- /dev/null +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -0,0 +1,136 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader + +struct BloomUniforms { + threshold: f32, + knee: f32, + scale: f32, + intensity: f32, +}; + +@group(0) @binding(0) +var original: texture_2d; +@group(0) @binding(1) +var original_sampler: sampler; +@group(0) @binding(2) +var uniforms: BloomUniforms; +@group(0) @binding(3) +var up: texture_2d; + +fn quadratic_threshold(color: vec4, threshold: f32, curve: vec3) -> vec4 { + let br = max(max(color.r, color.g), color.b); + + var rq: f32 = clamp(br - curve.x, 0.0, curve.y); + rq = curve.z * rq * rq; + + return color * max(rq, br - threshold) / max(br, 0.0001); +} + +// Samples original around the supplied uv using a filter. +// +// 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, scale: vec2) -> vec4 { + let a = textureSample(original, original_sampler, uv + vec2(-1.0, -1.0) * scale); + let b = textureSample(original, original_sampler, uv + vec2(0.0, -1.0) * scale); + let c = textureSample(original, original_sampler, uv + vec2(1.0, -1.0) * scale); + let d = textureSample(original, original_sampler, uv + vec2(-0.5, -0.5) * scale); + let e = textureSample(original, original_sampler, uv + vec2(0.5, -0.5) * scale); + let f = textureSample(original, original_sampler, uv + vec2(-1.0, 0.0) * scale); + let g = textureSample(original, original_sampler, uv + vec2(0.0, 0.0) * scale); + let h = textureSample(original, original_sampler, uv + vec2(1.0, 0.0) * scale); + let i = textureSample(original, original_sampler, uv + vec2(-0.5, 0.5) * scale); + let j = textureSample(original, original_sampler, uv + vec2(0.5, 0.5) * scale); + let k = textureSample(original, original_sampler, uv + vec2(-1.0, 1.0) * scale); + let l = textureSample(original, original_sampler, uv + vec2(0.0, 1.0) * scale); + let m = textureSample(original, original_sampler, uv + vec2(1.0, 1.0) * scale); + + let div = (1.0 / 4.0) * vec2(0.5, 0.125); + + var o: vec4 = (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. +// +// NOTE: Use a 2x2 filter for better perf, but 3x3 looks better. +fn sample_original_3x3_tent(uv: vec2, scale: vec2) -> vec4 { + let d = vec4(1.0, 1.0, -1.0, 0.0); + + var s: vec4 = 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; +} + +@fragment +fn downsample_prefilter(@location(0) uv: vec2) -> @location(0) vec4 { + let texel_size = 1.0 / vec2(textureDimensions(original)); + + let scale = texel_size; + + let curve = vec3( + uniforms.threshold - uniforms.knee, + uniforms.knee * 2.0, + 0.25 / uniforms.knee, + ); + + var o: vec4 = sample_13_tap(uv, scale); + + o = quadratic_threshold(o, uniforms.threshold, curve); + o = max(o, vec4(0.00001)); + + return o; +} + +@fragment +fn downsample(@location(0) uv: vec2) -> @location(0) vec4 { + let texel_size = 1.0 / vec2(textureDimensions(original)); + + let scale = texel_size; + + return sample_13_tap(uv, scale); +} + +@fragment +fn upsample(@location(0) uv: vec2) -> @location(0) vec4 { + let texel_size = 1.0 / vec2(textureDimensions(original)); + + let upsample = sample_original_3x3_tent(uv, texel_size * uniforms.scale); + var color: vec4 = textureSample(up, original_sampler, uv); + color = vec4(color.rgb + upsample.rgb, upsample.a); + + return color; +} + +@fragment +fn upsample_final(@location(0) uv: vec2) -> @location(0) vec4 { + let texel_size = 1.0 / vec2(textureDimensions(original)); + + let upsample = sample_original_3x3_tent(uv, texel_size * uniforms.scale); + + return vec4(upsample.rgb * uniforms.intensity, upsample.a); +} diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs new file mode 100644 index 0000000000000..d61a0f3298294 --- /dev/null +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -0,0 +1,800 @@ +use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::{QueryState, With}, + system::{Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::UVec2; +use bevy_reflect::{Reflect, TypeUuid}; +use bevy_render::{ + camera::ExtractedCamera, + prelude::Camera, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + render_phase::TrackedRenderPass, + render_resource::*, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::ViewTarget, + Extract, RenderApp, RenderStage, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use bevy_utils::HashMap; +use std::num::NonZeroU32; + +pub mod draw_3d_graph { + pub mod node { + /// Label for the bloom render node. + pub const BLOOM: &str = "bloom_3d"; + } +} +pub mod draw_2d_graph { + pub mod node { + /// Label for the bloom render node. + pub const BLOOM: &str = "bloom_2d"; + } +} + +const BLOOM_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 929599476923908); + +pub struct BloomPlugin; + +impl Plugin for BloomPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl); + + app.register_type::(); + + let render_app = match app.get_sub_app_mut(RenderApp) { + Ok(render_app) => render_app, + Err(_) => return, + }; + + render_app + .init_resource::() + .init_resource::() + .add_system_to_stage(RenderStage::Extract, extract_bloom_settings) + .add_system_to_stage(RenderStage::Prepare, prepare_bloom_textures) + .add_system_to_stage(RenderStage::Prepare, prepare_bloom_uniforms) + .add_system_to_stage(RenderStage::Queue, queue_bloom_bind_groups); + + { + let bloom_node = BloomNode::new(&mut render_app.world); + let mut graph = render_app.world.resource_mut::(); + let draw_3d_graph = graph + .get_sub_graph_mut(crate::core_3d::graph::NAME) + .unwrap(); + draw_3d_graph.add_node(draw_3d_graph::node::BLOOM, bloom_node); + draw_3d_graph + .add_slot_edge( + draw_3d_graph.input_node().unwrap().id, + crate::core_3d::graph::input::VIEW_ENTITY, + draw_3d_graph::node::BLOOM, + BloomNode::IN_VIEW, + ) + .unwrap(); + // MAIN_PASS -> BLOOM -> TONEMAPPING + draw_3d_graph + .add_node_edge( + crate::core_3d::graph::node::MAIN_PASS, + draw_3d_graph::node::BLOOM, + ) + .unwrap(); + draw_3d_graph + .add_node_edge( + draw_3d_graph::node::BLOOM, + crate::core_3d::graph::node::TONEMAPPING, + ) + .unwrap(); + } + + { + let bloom_node = BloomNode::new(&mut render_app.world); + let mut graph = render_app.world.resource_mut::(); + let draw_2d_graph = graph + .get_sub_graph_mut(crate::core_2d::graph::NAME) + .unwrap(); + draw_2d_graph.add_node(draw_2d_graph::node::BLOOM, bloom_node); + draw_2d_graph + .add_slot_edge( + draw_2d_graph.input_node().unwrap().id, + crate::core_2d::graph::input::VIEW_ENTITY, + draw_2d_graph::node::BLOOM, + BloomNode::IN_VIEW, + ) + .unwrap(); + // MAIN_PASS -> BLOOM -> TONEMAPPING + draw_2d_graph + .add_node_edge( + crate::core_2d::graph::node::MAIN_PASS, + draw_2d_graph::node::BLOOM, + ) + .unwrap(); + draw_2d_graph + .add_node_edge( + draw_2d_graph::node::BLOOM, + crate::core_2d::graph::node::TONEMAPPING, + ) + .unwrap(); + } + } +} + +/// Applies a bloom effect to a HDR-enabled 2d or 3d camera. +/// +/// See also . +#[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: 1.0). + pub intensity: f32, +} + +impl Default for BloomSettings { + fn default() -> Self { + Self { + threshold: 1.0, + knee: 0.1, + scale: 1.0, + intensity: 1.0, + } + } +} + +struct BloomNode { + view_query: QueryState<( + &'static ExtractedCamera, + &'static ViewTarget, + &'static BloomTextures, + &'static BloomBindGroups, + &'static BloomUniformIndex, + )>, +} + +impl BloomNode { + const IN_VIEW: &'static str = "view"; + + fn new(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for BloomNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + #[cfg(feature = "trace")] + let _bloom_span = info_span!("bloom").entered(); + + let pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let (camera, view_target, textures, bind_groups, uniform_index) = + match self.view_query.get_manual(world, view_entity) { + Ok(result) => result, + _ => return Ok(()), + }; + let ( + downsampling_prefilter_pipeline, + downsampling_pipeline, + upsampling_pipeline, + upsampling_final_pipeline, + ) = match ( + 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 view = &BloomTextures::texture_view(&textures.texture_a, 0); + let mut prefilter_pass = + TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( + &RenderPassDescriptor { + label: Some("bloom_prefilter_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }, + )); + prefilter_pass.set_render_pipeline(downsampling_prefilter_pipeline); + prefilter_pass.set_bind_group(0, &bind_groups.prefilter_bind_group, &[uniform_index.0]); + if let Some(viewport) = camera.viewport.as_ref() { + prefilter_pass.set_camera_viewport(viewport); + } + prefilter_pass.draw(0..3, 0..1); + } + + for mip in 1..textures.mip_count { + let view = &BloomTextures::texture_view(&textures.texture_a, mip); + let mut downsampling_pass = + TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( + &RenderPassDescriptor { + label: Some("bloom_downsampling_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }, + )); + downsampling_pass.set_render_pipeline(downsampling_pipeline); + downsampling_pass.set_bind_group( + 0, + &bind_groups.downsampling_bind_groups[mip as usize - 1], + &[uniform_index.0], + ); + if let Some(viewport) = camera.viewport.as_ref() { + downsampling_pass.set_camera_viewport(viewport); + } + downsampling_pass.draw(0..3, 0..1); + } + + for mip in (1..textures.mip_count).rev() { + let view = &BloomTextures::texture_view(&textures.texture_b, mip - 1); + let mut upsampling_pass = + TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( + &RenderPassDescriptor { + label: Some("bloom_upsampling_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }, + )); + upsampling_pass.set_render_pipeline(upsampling_pipeline); + upsampling_pass.set_bind_group( + 0, + &bind_groups.upsampling_bind_groups[mip as usize - 1], + &[uniform_index.0], + ); + if let Some(viewport) = camera.viewport.as_ref() { + upsampling_pass.set_camera_viewport(viewport); + } + upsampling_pass.draw(0..3, 0..1); + } + + { + let mut upsampling_final_pass = + TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( + &RenderPassDescriptor { + label: Some("bloom_upsampling_final_pass"), + color_attachments: &[Some(view_target.get_unsampled_color_attachment( + Operations { + load: LoadOp::Load, + store: true, + }, + ))], + depth_stencil_attachment: None, + }, + )); + upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline); + upsampling_final_pass.set_bind_group( + 0, + &bind_groups.upsampling_final_bind_group, + &[uniform_index.0], + ); + if let Some(viewport) = camera.viewport.as_ref() { + upsampling_final_pass.set_camera_viewport(viewport); + } + upsampling_final_pass.draw(0..3, 0..1); + } + + 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::(); + + 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 mut pipeline_cache = world.resource_mut::(); + + let downsampling_prefilter_pipeline = + pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("bloom_downsampling_prefilter_pipeline".into()), + layout: Some(vec![downsampling_bind_group_layout.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: BLOOM_SHADER_HANDLE.typed::(), + 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(), + }); + + let downsampling_pipeline = + pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("bloom_downsampling_pipeline".into()), + layout: Some(vec![downsampling_bind_group_layout.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: BLOOM_SHADER_HANDLE.typed::(), + 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(), + }); + + let upsampling_pipeline = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("bloom_upsampling_pipeline".into()), + layout: Some(vec![upsampling_bind_group_layout.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: BLOOM_SHADER_HANDLE.typed::(), + 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(), + }); + + let upsampling_final_pipeline = + pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("bloom_upsampling_final_pipeline".into()), + layout: Some(vec![downsampling_bind_group_layout.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: BLOOM_SHADER_HANDLE.typed::(), + 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::REPLACE, + }), + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + }); + + BloomPipelines { + downsampling_prefilter_pipeline, + downsampling_pipeline, + upsampling_pipeline, + upsampling_final_pipeline, + sampler, + downsampling_bind_group_layout, + upsampling_bind_group_layout, + } + } +} + +fn extract_bloom_settings( + mut commands: Commands, + cameras: Extract>>, +) { + for (entity, camera, bloom_settings) in &cameras { + if camera.is_active && camera.hdr { + commands.get_or_spawn(entity).insert(bloom_settings.clone()); + } + } +} + +#[derive(Component)] +struct BloomTextures { + texture_a: CachedTexture, + texture_b: CachedTexture, + mip_count: u32, +} + +impl BloomTextures { + fn texture_view(texture: &CachedTexture, base_mip_level: u32) -> TextureView { + texture.texture.create_view(&TextureViewDescriptor { + base_mip_level, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }) + } +} + +fn prepare_bloom_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + views: Query<(Entity, &ExtractedCamera), With>, +) { + let mut texture_as = HashMap::default(); + let mut texture_bs = HashMap::default(); + for (entity, camera) in &views { + if let Some(UVec2 { + x: width, + y: height, + }) = camera.physical_viewport_size + { + let min_view = width.min(height) / 2; + let mip_count = calculate_mip_count(min_view); + + let mut texture_descriptor = TextureDescriptor { + label: None, + size: Extent3d { + width: (width / 2).max(1), + height: (height / 2).max(1), + depth_or_array_layers: 1, + }, + mip_level_count: mip_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: ViewTarget::TEXTURE_FORMAT_HDR, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + }; + + texture_descriptor.label = Some("bloom_texture_a"); + let texture_a = texture_as + .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, + }); + } + } +} + +#[derive(ShaderType)] +struct BloomUniform { + threshold: f32, + knee: f32, + scale: f32, + intensity: f32, +} + +#[derive(Resource, Default)] +struct BloomUniforms { + uniforms: DynamicUniformBuffer, +} + +#[derive(Component)] +struct BloomUniformIndex(u32); + +fn prepare_bloom_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut bloom_uniforms: ResMut, + bloom_query: Query<(Entity, &ExtractedCamera, &BloomSettings)>, +) { + bloom_uniforms.uniforms.clear(); + + let entities = bloom_query + .iter() + .filter_map(|(entity, camera, settings)| { + let size = match camera.physical_viewport_size { + Some(size) => size, + None => return None, + }; + 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; + + let uniform = BloomUniform { + threshold: settings.threshold, + knee: settings.knee, + scale: settings.scale * scale, + intensity: settings.intensity, + }; + let index = bloom_uniforms.uniforms.push(uniform); + Some((entity, (BloomUniformIndex(index)))) + }) + .collect::>(); + commands.insert_or_spawn_batch(entities); + + bloom_uniforms + .uniforms + .write_buffer(&render_device, &render_queue); +} + +#[derive(Component)] +struct BloomBindGroups { + prefilter_bind_group: BindGroup, + downsampling_bind_groups: Box<[BindGroup]>, + upsampling_bind_groups: Box<[BindGroup]>, + upsampling_final_bind_group: BindGroup, +} + +fn queue_bloom_bind_groups( + mut commands: Commands, + render_device: Res, + pipelines: Res, + uniforms: Res, + views: Query<(Entity, &ViewTarget, &BloomTextures)>, +) { + if let Some(uniforms) = uniforms.uniforms.binding() { + 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; + + let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count); + for mip in 1..textures.mip_count { + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_downsampling_bind_group"), + layout: &pipelines.downsampling_bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&BloomTextures::texture_view( + &textures.texture_a, + mip - 1, + )), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&pipelines.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms.clone(), + }, + ], + }); + + downsampling_bind_groups.push(bind_group); + } + + let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count); + for mip in 1..textures.mip_count { + let up = BloomTextures::texture_view(&textures.texture_a, mip - 1); + let org = BloomTextures::texture_view( + if mip == textures.mip_count - 1 { + &textures.texture_a + } else { + &textures.texture_b + }, + mip, + ); + + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_upsampling_bind_group"), + layout: &pipelines.upsampling_bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&org), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&pipelines.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms.clone(), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::TextureView(&up), + }, + ], + }); + + upsampling_bind_groups.push(bind_group); + } + + let upsampling_final_bind_group = + render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_upsampling_final_bind_group"), + layout: &pipelines.downsampling_bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&BloomTextures::texture_view( + &textures.texture_b, + 0, + )), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&pipelines.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms.clone(), + }, + ], + }); + + commands.entity(entity).insert(BloomBindGroups { + prefilter_bind_group, + downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(), + upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(), + upsampling_final_bind_group, + }); + } + } +} + +fn calculate_mip_count(min_view: u32) -> u32 { + ((min_view as f32).log2().round() as u32 - 1).max(1) +} diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 85f2c213ae344..adfe9d500f038 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -1,3 +1,4 @@ +pub mod bloom; pub mod clear_color; pub mod core_2d; pub mod core_3d; @@ -16,6 +17,7 @@ pub mod prelude { } use crate::{ + bloom::BloomPlugin, clear_color::{ClearColor, ClearColorConfig}, core_2d::Core2dPlugin, core_3d::Core3dPlugin, @@ -44,10 +46,11 @@ impl Plugin for CorePipelinePlugin { .register_type::() .init_resource::() .add_plugin(ExtractResourcePlugin::::default()) - .add_plugin(TonemappingPlugin) - .add_plugin(UpscalingPlugin) .add_plugin(Core2dPlugin) .add_plugin(Core3dPlugin) + .add_plugin(TonemappingPlugin) + .add_plugin(UpscalingPlugin) + .add_plugin(BloomPlugin) .add_plugin(FxaaPlugin); } } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index c133442e8dd3a..c9d7e7c3f77bf 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -127,12 +127,12 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .register_type::() - .add_plugin(MeshRenderPlugin) - .add_plugin(MaterialPlugin::::default()) .register_asset_reflect::() .register_type::() .register_type::() .register_type::() + .add_plugin(MeshRenderPlugin) + .add_plugin(MaterialPlugin::::default()) .init_resource::() .init_resource::() .init_resource::() diff --git a/examples/3d/bloom.rs b/examples/3d/bloom.rs new file mode 100644 index 0000000000000..a68ca8cbb33ef --- /dev/null +++ b/examples/3d/bloom.rs @@ -0,0 +1,174 @@ +//! 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>, + mut materials: ResMut>, + asset_server: Res, +) { + 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::default() + }); + let material_non_emissive = materials.add(StandardMaterial { + base_color: Color::GRAY, + ..Default::default() + }); + + let mesh = meshes.add( + shape::Icosphere { + radius: 0.5, + subdivisions: 5, + } + .into(), + ); + + 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::default() + }, + Bouncing, + )); + } + } + + // UI camera + commands.spawn(Camera2dBundle { + camera: Camera { + priority: -1, + ..default() + }, + ..default() + }); + + 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>, + time: Res