diff --git a/Cargo.toml b/Cargo.toml index f3bab82571fdd1..5d3293ed1ee9c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,10 @@ path = "examples/3d/3d_scene.rs" name = "lighting" path = "examples/3d/lighting.rs" +[[example]] +name = "bloom" +path = "examples/3d/bloom.rs" + [[example]] name = "load_gltf" path = "examples/3d/load_gltf.rs" diff --git a/crates/bevy_pbr/src/bloom/bloom.wgsl b/crates/bevy_pbr/src/bloom/bloom.wgsl new file mode 100644 index 00000000000000..c2a0071f34d0e5 --- /dev/null +++ b/crates/bevy_pbr/src/bloom/bloom.wgsl @@ -0,0 +1,159 @@ +struct VertexOutput { + [[builtin(position)]] position: vec4; + [[location(0)]] uv: vec2; +}; + +var vertices: array, 3> = array, 3>( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0), +); + +// full screen triangle vertex shader +[[stage(vertex)]] +fn vertex([[builtin(vertex_index)]] idx: u32) -> VertexOutput { + var out: VertexOutput; + + out.position = vec4(vertices[idx], 0.0, 1.0); + out.uv = vertices[idx] * vec2(0.5, -0.5); + out.uv = out.uv + 0.5; + + return out; +} + +struct Uniforms { + threshold: f32; + knee: f32; + scale: f32; +}; + +[[group(0), binding(0)]] +var org: texture_2d; + +[[group(0), binding(1)]] +var org_sampler: sampler; + +[[group(0), binding(2)]] +var uniforms: Uniforms; + +[[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 org 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 +// outway 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(org, org_sampler, uv + vec2(-1.0, -1.0) * scale); + let b = textureSample(org, org_sampler, uv + vec2( 0.0, -1.0) * scale); + let c = textureSample(org, org_sampler, uv + vec2( 1.0, -1.0) * scale); + let d = textureSample(org, org_sampler, uv + vec2(-0.5, -0.5) * scale); + let e = textureSample(org, org_sampler, uv + vec2( 0.5, -0.5) * scale); + let f = textureSample(org, org_sampler, uv + vec2(-1.0, 0.0) * scale); + let g = textureSample(org, org_sampler, uv + vec2( 0.0, 0.0) * scale); + let h = textureSample(org, org_sampler, uv + vec2( 1.0, 0.0) * scale); + let i = textureSample(org, org_sampler, uv + vec2(-0.5, 0.5) * scale); + let j = textureSample(org, org_sampler, uv + vec2( 0.5, 0.5) * scale); + let k = textureSample(org, org_sampler, uv + vec2(-1.0, 1.0) * scale); + let l = textureSample(org, org_sampler, uv + vec2( 0.0, 1.0) * scale); + let m = textureSample(org, org_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 org using a 3x3 tent filter +// +// NOTE: use a 2x2 filter for better perf, but 3x3 looks better +fn sample_3x3_tent(uv: vec2, scale: vec2) -> vec4 { + let d = vec4(1.0, 1.0, -1.0, 0.0); + + var s: vec4 = textureSample(org, org_sampler, uv - d.xy * scale); + s = s + textureSample(org, org_sampler, uv - d.wy * scale) * 2.0; + s = s + textureSample(org, org_sampler, uv - d.zy * scale); + + s = s + textureSample(org, org_sampler, uv + d.zw * scale) * 2.0; + s = s + textureSample(org, org_sampler, uv ) * 4.0; + s = s + textureSample(org, org_sampler, uv + d.xw * scale) * 2.0; + + s = s + textureSample(org, org_sampler, uv + d.zy * scale); + s = s + textureSample(org, org_sampler, uv + d.wy * scale) * 2.0; + s = s + textureSample(org, org_sampler, uv + d.xy * scale); + + return s / 16.0; +} + +[[stage(fragment)]] +fn down_sample_pre_filter(in: VertexOutput) -> [[location(0)]] vec4 { + let texel_size = 1.0 / vec2(textureDimensions(org)); + + 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(in.uv, scale); + + o = quadratic_threshold(o, uniforms.threshold, curve); + o = max(o, vec4(0.00001)); + + return o; +} + +[[stage(fragment)]] +fn down_sample(in: VertexOutput) -> [[location(0)]] vec4 { + let texel_size = 1.0 / vec2(textureDimensions(org)); + + let scale = texel_size; + + return sample_13_tap(in.uv, scale); +} + +[[stage(fragment)]] +fn up_sample(in: VertexOutput) -> [[location(0)]] vec4 { + let texel_size = 1.0 / vec2(textureDimensions(org)); + + let up_sample = sample_3x3_tent(in.uv, texel_size * uniforms.scale); + var color: vec4 = textureSample(up, org_sampler, in.uv); + color = vec4(color.rgb + up_sample.rgb, up_sample.a); + + return color; +} + +[[stage(fragment)]] +fn up_sample_final(in: VertexOutput) -> [[location(0)]] vec4 { + let texel_size = 1.0 / vec2(textureDimensions(org)); + + let up_sample = sample_3x3_tent(in.uv, texel_size * uniforms.scale); + + return up_sample; +} diff --git a/crates/bevy_pbr/src/bloom/mod.rs b/crates/bevy_pbr/src/bloom/mod.rs new file mode 100644 index 00000000000000..d0b5ff0ae58bd1 --- /dev/null +++ b/crates/bevy_pbr/src/bloom/mod.rs @@ -0,0 +1,755 @@ +use std::{num::NonZeroU32, sync::Mutex}; + +use bevy_app::Plugin; +use bevy_core_pipeline::MainPass3dNode; +use bevy_ecs::prelude::*; +use bevy_render::{ + camera::{CameraPlugin, ExtractedCamera, ExtractedCameraNames}, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + render_resource::std140::{AsStd140, Std140}, + render_resource::*, + renderer::{RenderContext, RenderDevice, RenderQueue}, + view::{ExtractedWindows, ViewTarget}, + RenderApp, RenderStage, +}; + +pub struct BloomPlugin; + +impl Plugin for BloomPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + + let render_app = app.sub_app_mut(RenderApp); + render_app + .init_resource::() + .add_system_to_stage(RenderStage::Extract, extract_bloom_settings); + + let mut render_graph = render_app.world.get_resource_mut::().unwrap(); + + let bloom_node = BloomNode::default(); + + let draw_3d_graph = render_graph + .get_sub_graph_mut(bevy_core_pipeline::draw_3d_graph::NAME) + .unwrap(); + draw_3d_graph.add_node(BloomNode::NODE_NAME, bloom_node); + + draw_3d_graph + .add_node_edge( + bevy_core_pipeline::draw_3d_graph::node::MAIN_PASS, + BloomNode::NODE_NAME, + ) + .unwrap(); + draw_3d_graph + .add_node_edge( + BloomNode::NODE_NAME, + bevy_core_pipeline::draw_3d_graph::node::TONEMAPPING, + ) + .unwrap(); + draw_3d_graph + .add_slot_edge( + bevy_core_pipeline::draw_3d_graph::node::MAIN_PASS, + MainPass3dNode::OUT_HDR, + BloomNode::NODE_NAME, + BloomNode::IN_HDR, + ) + .unwrap(); + } +} + +fn extract_bloom_settings(mut commands: Commands, bloom_settings: Res) { + commands.insert_resource(bloom_settings.into_inner().clone()); +} + +/// Resources used by [`BloomNode`]. +pub struct BloomShaders { + pub down_sampling_pipeline: RenderPipeline, + pub down_sampling_pre_filter_pipeline: RenderPipeline, + pub up_sampling_pipeline: RenderPipeline, + pub up_sampling_final_pipeline: RenderPipeline, + pub shader_module: ShaderModule, + pub down_layout: BindGroupLayout, + pub up_layout: BindGroupLayout, + pub sampler: Sampler, +} + +impl FromWorld for BloomShaders { + fn from_world(world: &mut World) -> Self { + let render_device = world.get_resource::().unwrap(); + let shader = ShaderModuleDescriptor { + label: Some("bloom shader"), + source: ShaderSource::Wgsl(include_str!("bloom.wgsl").into()), + }; + let shader_module = render_device.create_shader_module(&shader); + + let down_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("bloom_down_sampling_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + ], + }); + + let up_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("bloom_up_sampling_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + BindGroupLayoutEntry { + binding: 3, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + visibility: ShaderStages::VERTEX_FRAGMENT, + count: None, + }, + ], + }); + + let down_pipeline_layout = + render_device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some("bloom_down_sampling_layout"), + bind_group_layouts: &[&down_layout], + push_constant_ranges: &[], + }); + + let up_pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some("bloom_up_sampling_layout"), + bind_group_layouts: &[&up_layout], + push_constant_ranges: &[], + }); + + let down_sampling_pre_filter_pipeline = + render_device.create_render_pipeline(&RawRenderPipelineDescriptor { + label: Some("bloom_down_sampling_pre_filter_pipeline"), + layout: Some(&down_pipeline_layout), + vertex: RawVertexState { + module: &shader_module, + entry_point: "vertex", + buffers: &[], + }, + fragment: Some(RawFragmentState { + module: &shader_module, + entry_point: "down_sample_pre_filter", + targets: &[ColorTargetState { + format: ViewTarget::TEXTURE_FORMAT_HDR, + blend: None, + write_mask: ColorWrites::ALL, + }], + }), + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..Default::default() + }, + multisample: Default::default(), + depth_stencil: None, + multiview: None, + }); + + let down_sampling_pipeline = + render_device.create_render_pipeline(&RawRenderPipelineDescriptor { + label: Some("bloom_down_sampling_pipeline"), + layout: Some(&down_pipeline_layout), + vertex: RawVertexState { + module: &shader_module, + entry_point: "vertex", + buffers: &[], + }, + fragment: Some(RawFragmentState { + module: &shader_module, + entry_point: "down_sample", + targets: &[ColorTargetState { + format: ViewTarget::TEXTURE_FORMAT_HDR, + blend: None, + write_mask: ColorWrites::ALL, + }], + }), + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..Default::default() + }, + multisample: Default::default(), + depth_stencil: None, + multiview: None, + }); + + let up_sampling_pipeline = + render_device.create_render_pipeline(&RawRenderPipelineDescriptor { + label: Some("bloom_up_sampling_pipeline"), + layout: Some(&up_pipeline_layout), + vertex: RawVertexState { + module: &shader_module, + entry_point: "vertex", + buffers: &[], + }, + fragment: Some(RawFragmentState { + module: &shader_module, + entry_point: "up_sample", + targets: &[ColorTargetState { + format: ViewTarget::TEXTURE_FORMAT_HDR, + blend: None, + write_mask: ColorWrites::ALL, + }], + }), + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..Default::default() + }, + multisample: Default::default(), + depth_stencil: None, + multiview: None, + }); + + let up_sampling_final_pipeline = + render_device.create_render_pipeline(&RawRenderPipelineDescriptor { + label: Some("bloom_up_sampling_final_pipeline"), + layout: Some(&down_pipeline_layout), + vertex: RawVertexState { + module: &shader_module, + entry_point: "vertex", + buffers: &[], + }, + fragment: Some(RawFragmentState { + module: &shader_module, + entry_point: "up_sample_final", + targets: &[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 { + cull_mode: Some(Face::Back), + ..Default::default() + }, + multisample: Default::default(), + depth_stencil: None, + multiview: None, + }); + + 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() + }); + + BloomShaders { + down_sampling_pre_filter_pipeline, + down_sampling_pipeline, + up_sampling_pipeline, + up_sampling_final_pipeline, + shader_module, + down_layout, + up_layout, + sampler, + } + } +} + +struct MipChain { + mips: u32, + scale: f32, + width: u32, + height: u32, + target_id: TextureViewId, + tex_a: Texture, + tex_b: Texture, + pre_filter_bind_group: BindGroup, + down_sampling_bind_groups: Vec, + up_sampling_bind_groups: Vec, + up_sampling_final_bind_group: BindGroup, +} + +impl MipChain { + fn new( + render_device: &RenderDevice, + bloom_shaders: &BloomShaders, + uniforms_buffer: &Buffer, + hdr_target: &TextureView, + width: u32, + height: u32, + ) -> Self { + let min_element = width.min(height) / 2; + let mut mips = 1; + + while min_element / 2u32.pow(mips) > 4 { + mips += 1; + } + + let size = Extent3d { + width: (width / 2).max(1), + height: (height / 2).max(1), + depth_or_array_layers: 1, + }; + + let tex_a = render_device.create_texture(&TextureDescriptor { + label: Some("bloom_tex_a"), + size, + mip_level_count: mips, + sample_count: 1, + dimension: TextureDimension::D2, + format: ViewTarget::TEXTURE_FORMAT_HDR, + usage: TextureUsages::COPY_DST + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }); + + let tex_b = render_device.create_texture(&TextureDescriptor { + label: Some("bloom_tex_b"), + size, + mip_level_count: mips, + sample_count: 1, + dimension: TextureDimension::D2, + format: ViewTarget::TEXTURE_FORMAT_HDR, + usage: TextureUsages::COPY_DST + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }); + + let pre_filter_bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_pre_filter_bind_group"), + layout: &bloom_shaders.down_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(hdr_target), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&bloom_shaders.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms_buffer.as_entire_binding(), + }, + ], + }); + + let mut down_sampling_bind_groups = Vec::new(); + + for mip in 1..mips { + let view = tex_a.create_view(&TextureViewDescriptor { + label: None, + base_mip_level: mip - 1, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_down_sampling_bind_group"), + layout: &bloom_shaders.down_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&bloom_shaders.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms_buffer.as_entire_binding(), + }, + ], + }); + + down_sampling_bind_groups.push(bind_group); + } + + let mut up_sampling_bind_groups = Vec::new(); + + for mip in 1..mips { + let up = tex_a.create_view(&TextureViewDescriptor { + label: None, + base_mip_level: mip - 1, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + let org_tex = if mip == mips - 1 { &tex_a } else { &tex_b }; + + let org = org_tex.create_view(&TextureViewDescriptor { + label: None, + base_mip_level: mip, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_up_sampling_bind_group"), + layout: &bloom_shaders.up_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&org), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&bloom_shaders.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms_buffer.as_entire_binding(), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::TextureView(&up), + }, + ], + }); + + up_sampling_bind_groups.push(bind_group); + } + + let org = tex_b.create_view(&TextureViewDescriptor { + label: None, + base_mip_level: 0, + ..Default::default() + }); + + let up_sampling_final_bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("bloom_up_sampling_final_bind_group"), + layout: &bloom_shaders.down_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&org), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&bloom_shaders.sampler), + }, + BindGroupEntry { + binding: 2, + resource: uniforms_buffer.as_entire_binding(), + }, + ], + }); + + Self { + mips, + scale: (min_element / 2u32.pow(mips)) as f32 / 8.0, + width, + height, + target_id: hdr_target.id(), + tex_a, + tex_b, + pre_filter_bind_group, + down_sampling_bind_groups, + up_sampling_bind_groups, + up_sampling_final_bind_group, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, AsStd140, Default, Debug)] +struct Uniforms { + threshold: f32, + knee: f32, + scale: f32, +} + +/// Settings for bloom. +#[derive(Clone, Debug)] +pub struct BloomSettings { + /// Enables bloom. + pub enabled: bool, + /// Threshold for for bloom to apply. + pub threshold: f32, + /// Adjusts the threshold curve. + pub knee: f32, + /// Scale used when up sampling. + pub up_sample_scale: f32, +} + +impl Default for BloomSettings { + #[inline] + fn default() -> Self { + Self { + enabled: true, + threshold: 1.0, + knee: 0.1, + up_sample_scale: 1.0, + } + } +} + +/// Applies bloom effect to the input texture. +/// +/// Use [`BloomSettings`] to configure the effect at runtime. +#[derive(Default)] +pub struct BloomNode { + uniforms_buffer: Option, + mip_chain: Mutex>, +} + +impl BloomNode { + pub const NODE_NAME: &'static str = "bloom"; + pub const IN_HDR: &'static str = "hdr_target"; +} + +impl Node for BloomNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_HDR, SlotType::TextureView)] + } + + fn update(&mut self, world: &mut World) { + if self.uniforms_buffer.is_none() { + let render_device = world.get_resource::().unwrap(); + + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("bloom_uniforms_buffer"), + size: Uniforms::std140_size_static() as u64, + mapped_at_creation: false, + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + }); + + self.uniforms_buffer = Some(buffer); + } + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let bloom_shaders = world.get_resource::().unwrap(); + let extracted_cameras = world.get_resource::().unwrap(); + let extracted_windows = world.get_resource::().unwrap(); + + let camera_3d = match extracted_cameras.entities.get(CameraPlugin::CAMERA_3D) { + Some(entity) => entity, + None => return Ok(()), + }; + + let extracted_camera = world.entity(*camera_3d).get::().unwrap(); + let extracted_window = extracted_windows.get(&extracted_camera.window_id).unwrap(); + + let render_queue = world.get_resource::().unwrap(); + let settings = world.get_resource::().unwrap(); + + if !settings.enabled { + return Ok(()); + } + + let hdr_target = graph.get_input_texture(Self::IN_HDR)?; + + let uniforms_buffer = self.uniforms_buffer.as_ref().unwrap(); + let mut mip_chain = self.mip_chain.lock().unwrap(); + + let mip_chain = if let Some(ref mut mip_chain) = *mip_chain { + // if the window changes but the size of the new window doesn't, using ExtractedWindow.size_changed + // wouldn't trigger a resize, comparing the size ensures that it does + if mip_chain.width != extracted_window.physical_width + || mip_chain.height != extracted_window.physical_height + || mip_chain.target_id != hdr_target.id() + { + *mip_chain = MipChain::new( + &render_context.render_device, + bloom_shaders, + uniforms_buffer, + hdr_target, + extracted_window.physical_width, + extracted_window.physical_height, + ); + } + + mip_chain + } else { + *mip_chain = Some(MipChain::new( + &render_context.render_device, + bloom_shaders, + uniforms_buffer, + hdr_target, + extracted_window.physical_width, + extracted_window.physical_height, + )); + + mip_chain.as_ref().unwrap() + }; + + let uniforms = Uniforms { + threshold: settings.threshold, + knee: settings.knee, + scale: settings.up_sample_scale * mip_chain.scale, + }; + + render_queue.write_buffer(uniforms_buffer, 0, uniforms.as_std140().as_bytes()); + + let view = mip_chain.tex_a.create_view(&TextureViewDescriptor { + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + { + let mut pre_filter_pass = + render_context + .command_encoder + .begin_render_pass(&RenderPassDescriptor { + label: Some("bloom_pre_filter_pass"), + color_attachments: &[RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(RawColor::BLACK), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + pre_filter_pass.set_pipeline(&bloom_shaders.down_sampling_pre_filter_pipeline); + pre_filter_pass.set_bind_group(0, &mip_chain.pre_filter_bind_group, &[]); + pre_filter_pass.draw(0..3, 0..1); + } + + for mip in 1..mip_chain.mips { + let view = mip_chain.tex_a.create_view(&TextureViewDescriptor { + base_mip_level: mip, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + let mut down_sampling_pass = + render_context + .command_encoder + .begin_render_pass(&RenderPassDescriptor { + label: Some("bloom_down_sampling_pass"), + color_attachments: &[RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(RawColor::BLACK), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + down_sampling_pass.set_pipeline(&bloom_shaders.down_sampling_pipeline); + down_sampling_pass.set_bind_group( + 0, + &mip_chain.down_sampling_bind_groups[mip as usize - 1], + &[], + ); + down_sampling_pass.draw(0..3, 0..1); + } + + for mip in (1..mip_chain.mips).rev() { + let view = mip_chain.tex_b.create_view(&TextureViewDescriptor { + base_mip_level: mip - 1, + mip_level_count: Some(unsafe { NonZeroU32::new_unchecked(1) }), + ..Default::default() + }); + + let mut up_sampling_pass = + render_context + .command_encoder + .begin_render_pass(&RenderPassDescriptor { + label: Some("bloom_up_sampling_pass"), + color_attachments: &[RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(RawColor::BLACK), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + up_sampling_pass.set_pipeline(&bloom_shaders.up_sampling_pipeline); + up_sampling_pass.set_bind_group( + 0, + &mip_chain.up_sampling_bind_groups[mip as usize - 1], + &[], + ); + up_sampling_pass.draw(0..3, 0..1); + } + + let mut up_sampling_final_pass = + render_context + .command_encoder + .begin_render_pass(&RenderPassDescriptor { + label: Some("bloom_up_sampling_final_pass"), + color_attachments: &[RenderPassColorAttachment { + view: hdr_target, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + up_sampling_final_pass.set_pipeline(&bloom_shaders.up_sampling_final_pipeline); + up_sampling_final_pass.set_bind_group(0, &mip_chain.up_sampling_final_bind_group, &[]); + up_sampling_final_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index f2dc9e1ef868c3..02302214038d21 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -1,12 +1,14 @@ pub mod wireframe; mod alpha; +pub mod bloom; mod bundle; mod light; mod material; mod render; pub use alpha::*; +use bloom::BloomPlugin; pub use bundle::*; pub use light::*; pub use material::*; @@ -56,6 +58,8 @@ pub struct PbrPlugin; impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { + app.add_plugin(BloomPlugin); + let mut shaders = app.world.get_resource_mut::>().unwrap(); shaders.set_untracked( PBR_SHADER_HANDLE, diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 000f2ec2d981bc..d668912770a4ea 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -24,10 +24,10 @@ pub use uniform_vec::*; pub use wgpu::{ util::BufferInitDescriptor, AddressMode, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, - BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, - BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, - CompareFunction, ComputePassDescriptor, ComputePipelineDescriptor, DepthBiasState, - DepthStencilState, Extent3d, Face, Features as WgpuFeatures, FilterMode, + BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferDescriptor, + BufferSize, BufferUsages, Color as RawColor, ColorTargetState, ColorWrites, CommandEncoder, + CommandEncoderDescriptor, CompareFunction, ComputePassDescriptor, ComputePipelineDescriptor, + DepthBiasState, DepthStencilState, Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, FrontFace, ImageCopyBuffer, ImageCopyBufferBase, ImageCopyTexture, ImageCopyTextureBase, ImageDataLayout, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MultisampleState, Operations, Origin3d, PipelineLayout, diff --git a/examples/3d/bloom.rs b/examples/3d/bloom.rs new file mode 100644 index 00000000000000..654809f36fb5c0 --- /dev/null +++ b/examples/3d/bloom.rs @@ -0,0 +1,66 @@ +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert(BloomSettings { + enabled: true, + threshold: 1.0, + knee: 0.1, + up_sample_scale: 1.0, + }) + .add_startup_system(setup) + .add_system(bounce) + .run(); +} + +#[derive(Component)] +struct Bouncing; + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let mesh = meshes.add( + shape::Icosphere { + radius: 0.5, + subdivisions: 5, + } + .into(), + ); + + let material = materials.add(StandardMaterial { + emissive: Color::rgb_linear(1.0, 0.3, 0.2) * 4.0, + ..Default::default() + }); + + for x in -10..10 { + for z in -10..10 { + commands + .spawn_bundle(PbrBundle { + mesh: mesh.clone(), + material: material.clone(), + transform: Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0), + ..Default::default() + }) + .insert(Bouncing); + } + } + + // camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..Default::default() + }); +} + +fn bounce(time: Res