diff --git a/Cargo.toml b/Cargo.toml index 3e29eff47f2d46..53db32d150de6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1220,6 +1220,17 @@ description = "A shader and a material that uses it" category = "Shaders" wasm = true +[[example]] +name = "shader_prepass" +path = "examples/shader/shader_prepass.rs" + +[package.metadata.example.shader_prepass] +name = "Material Prepass" +description = "A shader that uses the depth texture generated in a prepass" +category = "Shaders" +wasm = false + + [[example]] name = "shader_material_screenspace_texture" path = "examples/shader/shader_material_screenspace_texture.rs" diff --git a/assets/shaders/show_prepass.wgsl b/assets/shaders/show_prepass.wgsl new file mode 100644 index 00000000000000..592143aa6ee19a --- /dev/null +++ b/assets/shaders/show_prepass.wgsl @@ -0,0 +1,26 @@ +#import bevy_pbr::mesh_types +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::utils + +@group(1) @binding(0) +var show_depth: f32; +@group(1) @binding(1) +var show_normal: f32; + +@fragment +fn fragment( + @builtin(position) frag_coord: vec4, + @builtin(sample_index) sample_index: u32, + #import bevy_pbr::mesh_vertex_output +) -> @location(0) vec4 { + if show_depth == 1.0 { + let depth = prepass_depth(frag_coord, sample_index); + return vec4(depth, depth, depth, 1.0); + } else if show_normal == 1.0 { + let normal = prepass_normal(frag_coord, sample_index); + return vec4(normal, 1.0); + } else { + // transparent + return vec4(0.0); + } +} diff --git a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs index a836e0bcbe4502..353425e0dcfb68 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs @@ -1,6 +1,7 @@ use crate::{ clear_color::{ClearColor, ClearColorConfig}, core_3d::{AlphaMask3d, Camera3d, Opaque3d, Transparent3d}, + prepass::{DepthPrepass, NormalPrepass}, }; use bevy_ecs::prelude::*; use bevy_render::{ @@ -14,6 +15,8 @@ use bevy_render::{ #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; +use super::Camera3dDepthLoadOp; + pub struct MainPass3dNode { query: QueryState< ( @@ -24,6 +27,8 @@ pub struct MainPass3dNode { &'static Camera3d, &'static ViewTarget, &'static ViewDepthTexture, + Option<&'static DepthPrepass>, + Option<&'static NormalPrepass>, ), With, >, @@ -55,13 +60,20 @@ impl Node for MainPass3dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (camera, opaque_phase, alpha_mask_phase, transparent_phase, camera_3d, target, depth) = - match self.query.get_manual(world, view_entity) { - Ok(query) => query, - Err(_) => { - return Ok(()); - } // No window - }; + let Ok(( + camera, + opaque_phase, + alpha_mask_phase, + transparent_phase, + camera_3d, + target, + depth, + depth_prepass, + normal_prepass, + )) = self.query.get_manual(world, view_entity) else { + // No window + return Ok(()); + }; // Always run opaque pass to ensure screen is cleared { @@ -88,8 +100,15 @@ impl Node for MainPass3dNode { view: &depth.view, // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it depth_ops: Some(Operations { - // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. - load: camera_3d.depth_load_op.clone().into(), + load: if depth_prepass.is_some() || normal_prepass.is_some() { + // if any prepass runs, it will generate a depth buffer so we should use it, + // even if only the normal_prepass is used. + Camera3dDepthLoadOp::Load + } else { + // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. + camera_3d.depth_load_op.clone() + } + .into(), store: true, }), stencil_ops: None, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 34a430a3453980..d829f2b929d4dc 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -7,6 +7,7 @@ pub mod graph { pub const VIEW_ENTITY: &str = "view_entity"; } pub mod node { + pub const PREPASS: &str = "prepass"; pub const MAIN_PASS: &str = "main_pass"; pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; @@ -43,7 +44,11 @@ use bevy_render::{ }; use bevy_utils::{FloatOrd, HashMap}; -use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; +use crate::{ + prepass::{node::PrepassNode, DepthPrepass}, + tonemapping::TonemappingNode, + upscaling::UpscalingNode, +}; pub struct Core3dPlugin; @@ -68,20 +73,29 @@ impl Plugin for Core3dPlugin { .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::); + let prepass_node = PrepassNode::new(&mut render_app.world); let pass_node_3d = MainPass3dNode::new(&mut render_app.world); let tonemapping = TonemappingNode::new(&mut render_app.world); let upscaling = UpscalingNode::new(&mut render_app.world); let mut graph = render_app.world.resource_mut::(); let mut draw_3d_graph = RenderGraph::default(); + draw_3d_graph.add_node(graph::node::PREPASS, prepass_node); draw_3d_graph.add_node(graph::node::MAIN_PASS, pass_node_3d); draw_3d_graph.add_node(graph::node::TONEMAPPING, tonemapping); draw_3d_graph.add_node(graph::node::END_MAIN_PASS_POST_PROCESSING, EmptyNode); draw_3d_graph.add_node(graph::node::UPSCALING, upscaling); + let input_node_id = draw_3d_graph.set_input(vec![SlotInfo::new( graph::input::VIEW_ENTITY, SlotType::Entity, )]); + draw_3d_graph.add_slot_edge( + input_node_id, + graph::input::VIEW_ENTITY, + graph::node::PREPASS, + PrepassNode::IN_VIEW, + ); draw_3d_graph.add_slot_edge( input_node_id, graph::input::VIEW_ENTITY, @@ -100,6 +114,7 @@ impl Plugin for Core3dPlugin { graph::node::UPSCALING, UpscalingNode::IN_VIEW, ); + draw_3d_graph.add_node_edge(graph::node::PREPASS, graph::node::MAIN_PASS); draw_3d_graph.add_node_edge(graph::node::MAIN_PASS, graph::node::TONEMAPPING); draw_3d_graph.add_node_edge( graph::node::TONEMAPPING, @@ -253,7 +268,7 @@ pub fn prepare_core_3d_depth_textures( msaa: Res, render_device: Res, views_3d: Query< - (Entity, &ExtractedCamera), + (Entity, &ExtractedCamera, Option<&DepthPrepass>), ( With>, With>, @@ -262,34 +277,46 @@ pub fn prepare_core_3d_depth_textures( >, ) { let mut textures = HashMap::default(); - for (entity, camera) in &views_3d { - if let Some(physical_target_size) = camera.physical_target_size { - let cached_texture = textures - .entry(camera.target.clone()) - .or_insert_with(|| { - texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("view_depth_texture"), - size: Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24 - * bit depth for better performance */ - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ) - }) - .clone(); - commands.entity(entity).insert(ViewDepthTexture { - texture: cached_texture.texture, - view: cached_texture.default_view, - }); - } + for (entity, camera, depth_prepass) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + // Default usage required to write to the depth texture + let mut usage = TextureUsages::RENDER_ATTACHMENT; + if depth_prepass.is_some() { + // Required to read the output of the prepass + usage |= TextureUsages::COPY_SRC; + } + + // The size of the depth texture + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + // PERF: vulkan docs recommend using 24 bit depth for better performance + format: TextureFormat::Depth32Float, + usage, + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands.entity(entity).insert(ViewDepthTexture { + texture: cached_texture.texture, + view: cached_texture.default_view, + }); } } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index adfe9d500f0380..5b0fe9eaea21cc 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -4,6 +4,7 @@ pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; pub mod fxaa; +pub mod prepass; pub mod tonemapping; pub mod upscaling; @@ -23,6 +24,7 @@ use crate::{ core_3d::Core3dPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, + prepass::{DepthPrepass, NormalPrepass}, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, }; @@ -44,6 +46,8 @@ impl Plugin for CorePipelinePlugin { app.register_type::() .register_type::() + .register_type::() + .register_type::() .init_resource::() .add_plugin(ExtractResourcePlugin::::default()) .add_plugin(Core2dPlugin) diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs new file mode 100644 index 00000000000000..a3d05259d59cdb --- /dev/null +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -0,0 +1,147 @@ +//! Run a prepass before the main pass to generate depth and/or normals texture, sometimes called a thin g-buffer. +//! These textures are useful for various screen-space effects and reducing overdraw in the main pass. +//! +//! The prepass only runs for opaque meshes or meshes with an alpha mask. Transparent meshes are ignored. +//! +//! To enable the prepass, you need to add a prepass component to a [`crate::prelude::Camera3d`]. +//! +//! [`DepthPrepass`] +//! [`NormalPrepass`] +//! +//! The textures are automatically added to the default mesh view bindings. You can also get the raw textures +//! by querying the [`ViewPrepassTextures`] component on any camera with a prepass component. +//! +//! The depth prepass will always run and generate the depth buffer as a side effect, but it won't copy it +//! to a separate texture unless the [`DepthPrepass`] is activated. This means that if any prepass component is present +//! it will always create a depth buffer that will be used by the main pass. +//! +//! When using the default mesh view bindings you should be able to use `prepass_depth()` +//! and `prepass_normal()` to load the related textures. These functions are defined in `bevy_pbr::utils`. +//! See the `shader_prepass` example that shows how to use it. +//! +//! The prepass runs for each `Material`. You can control if the prepass should run per-material by setting the `prepass_enabled` +//! flag on the `MaterialPlugin`. +//! +//! Currently only works for 3D. + +pub mod node; + +use bevy_ecs::prelude::*; +use bevy_reflect::Reflect; +use bevy_render::{ + render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, + render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat}, + texture::CachedTexture, +}; +use bevy_utils::FloatOrd; + +pub const DEPTH_PREPASS_FORMAT: TextureFormat = TextureFormat::Depth32Float; +pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; + +/// If added to a [`crate::prelude::Camera3d`] then depth values will be copied to a separate texture available to the main pass. +#[derive(Component, Default, Reflect)] +pub struct DepthPrepass; + +/// If added to a [`crate::prelude::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass. +/// Normals will have normal map textures already applied. +#[derive(Component, Default, Reflect)] +pub struct NormalPrepass; + +/// Textures that are written to by the prepass. +/// +/// This component will only be present if any of the relevant prepass components are also present. +#[derive(Component)] +pub struct ViewPrepassTextures { + /// The depth texture generated by the prepass. + /// Exists only if [`DepthPrepass`] is added to the `ViewTarget` + pub depth: Option, + /// The normals texture generated by the prepass. + /// Exists only if [`NormalPrepass`] is added to the `ViewTarget` + pub normal: Option, + /// The size of the textures. + pub size: Extent3d, +} + +/// Opaque phase of the 3D prepass. +/// +/// Sorted front-to-back by the z-distance in front of the camera. +/// +/// Used to render all 3D meshes with materials that have no transparency. +pub struct Opaque3dPrepass { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for Opaque3dPrepass { + type SortKey = FloatOrd; + + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} + +/// Alpha mask phase of the 3D prepass. +/// +/// Sorted front-to-back by the z-distance in front of the camera. +/// +/// Used to render all meshes with a material with an alpha mask. +pub struct AlphaMask3dPrepass { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for AlphaMask3dPrepass { + type SortKey = FloatOrd; + + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs new file mode 100644 index 00000000000000..017f063e129e7c --- /dev/null +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -0,0 +1,134 @@ +use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryState; +use bevy_render::{ + camera::ExtractedCamera, + prelude::Color, + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_phase::RenderPhase, + render_resource::{ + LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, + }, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use super::{AlphaMask3dPrepass, Opaque3dPrepass, ViewPrepassTextures}; + +/// Render node used by the prepass. +/// +/// By default, inserted before the main pass in the render graph. +pub struct PrepassNode { + main_view_query: QueryState< + ( + &'static ExtractedCamera, + &'static RenderPhase, + &'static RenderPhase, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + ), + With, + >, +} + +impl PrepassNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + } + } +} + +impl Node for PrepassNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let Ok(( + camera, + opaque_prepass_phase, + alpha_mask_prepass_phase, + view_depth_texture, + view_prepass_textures, + )) = self.main_view_query.get_manual(world, view_entity) else { + return Ok(()); + }; + + if opaque_prepass_phase.items.is_empty() && alpha_mask_prepass_phase.items.is_empty() { + return Ok(()); + } + + let mut color_attachments = vec![]; + if let Some(view_normals_texture) = &view_prepass_textures.normal { + color_attachments.push(Some(RenderPassColorAttachment { + view: &view_normals_texture.default_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color::BLACK.into()), + store: true, + }, + })); + } + + { + // Set up the pass descriptor with the depth attachment and optional color attachments + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &view_depth_texture.view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: true, + }), + stencil_ops: None, + }), + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Always run opaque pass to ensure screen is cleared + { + // Run the prepass, sorted front-to-back + #[cfg(feature = "trace")] + let _opaque_prepass_span = info_span!("opaque_prepass").entered(); + opaque_prepass_phase.render(&mut render_pass, world, view_entity); + } + + if !alpha_mask_prepass_phase.items.is_empty() { + // Run the prepass, sorted front-to-back + #[cfg(feature = "trace")] + let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); + alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity); + } + } + + if let Some(prepass_depth_texture) = &view_prepass_textures.depth { + // Copy depth buffer to texture + render_context.command_encoder.copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 8366dbe673e1f5..992d2504f1d6d2 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -5,13 +5,16 @@ mod bundle; mod light; mod material; mod pbr_material; +mod prepass; mod render; pub use alpha::*; +use bevy_utils::default; pub use bundle::*; pub use light::*; pub use material::*; pub use pbr_material::*; +pub use prepass::*; pub use render::*; use bevy_window::ModifiesWindows; @@ -67,14 +70,27 @@ pub const SHADOWS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11350275143789590502); pub const PBR_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4805239651767701046); +pub const PBR_PREPASS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9407115064344201137); pub const PBR_FUNCTIONS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292); pub const SHADOW_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1836745567947005696); /// Sets up the entire PBR infrastructure of bevy. -#[derive(Default)] -pub struct PbrPlugin; +pub struct PbrPlugin { + /// Controls if the prepass is enabled for the StandardMaterial. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + pub prepass_enabled: bool, +} + +impl Default for PbrPlugin { + fn default() -> Self { + Self { + prepass_enabled: true, + } + } +} impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { @@ -122,6 +138,12 @@ impl Plugin for PbrPlugin { "render/depth.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PBR_PREPASS_SHADER_HANDLE, + "render/pbr_prepass.wgsl", + Shader::from_wgsl + ); app.register_type::() .register_type::() @@ -135,7 +157,10 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .add_plugin(MeshRenderPlugin) - .add_plugin(MaterialPlugin::::default()) + .add_plugin(MaterialPlugin:: { + prepass_enabled: self.prepass_enabled, + ..default() + }) .init_resource::() .init_resource::() .init_resource::() diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 622659effd435e..ec6b6680a753d4 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,6 +1,6 @@ use crate::{ - AlphaMode, DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup, - SetMeshViewBindGroup, + AlphaMode, DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, PrepassPlugin, + SetMeshBindGroup, SetMeshViewBindGroup, }; use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; @@ -133,6 +133,19 @@ pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'stat 0.0 } + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader + /// will be used. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader + /// will be used. + #[allow(unused_variables)] + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input. #[allow(unused_variables)] @@ -149,11 +162,22 @@ pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'stat /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. -pub struct MaterialPlugin(PhantomData); +pub struct MaterialPlugin { + /// Controls if the prepass is enabled for the Material. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + /// + /// When it is enabled, it will automatically add the [`PrepassPlugin`] + /// required to make the prepass work on this Material. + pub prepass_enabled: bool, + pub _marker: PhantomData, +} impl Default for MaterialPlugin { fn default() -> Self { - Self(Default::default()) + Self { + prepass_enabled: true, + _marker: Default::default(), + } } } @@ -164,6 +188,7 @@ where fn build(&self, app: &mut App) { app.add_asset::() .add_plugin(ExtractComponentPlugin::>::extract_visible()); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() @@ -180,6 +205,10 @@ where ) .add_system_to_stage(RenderStage::Queue, queue_material_meshes::); } + + if self.prepass_enabled { + app.add_plugin(PrepassPlugin::::default()); + } } } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index da179b7ccd095e..629fee943c4d07 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,4 +1,7 @@ -use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE}; +use crate::{ + AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_PREPASS_SHADER_HANDLE, + PBR_SHADER_HANDLE, +}; use bevy_asset::Handle; use bevy_math::Vec4; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, TypeUuid}; @@ -414,12 +417,11 @@ impl Material for StandardMaterial { key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { if key.bind_group_data.normal_map { - descriptor - .fragment - .as_mut() - .unwrap() - .shader_defs - .push("STANDARDMATERIAL_NORMAL_MAP".into()); + if let Some(fragment) = descriptor.fragment.as_mut() { + fragment + .shader_defs + .push("STANDARDMATERIAL_NORMAL_MAP".into()); + } } descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; if let Some(label) = &mut descriptor.label { @@ -428,6 +430,10 @@ impl Material for StandardMaterial { Ok(()) } + fn prepass_fragment_shader() -> ShaderRef { + PBR_PREPASS_SHADER_HANDLE.typed().into() + } + fn fragment_shader() -> ShaderRef { PBR_SHADER_HANDLE.typed().into() } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs new file mode 100644 index 00000000000000..0a368da0ec94c2 --- /dev/null +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -0,0 +1,605 @@ +use bevy_app::Plugin; +use bevy_asset::{load_internal_asset, AssetServer, Handle, HandleUntyped}; +use bevy_core_pipeline::{ + prelude::Camera3d, + prepass::{ + AlphaMask3dPrepass, DepthPrepass, NormalPrepass, Opaque3dPrepass, ViewPrepassTextures, + DEPTH_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, + }, +}; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{ + lifetimeless::{Read, SRes}, + Commands, Query, Res, ResMut, Resource, SystemParamItem, + }, + world::{FromWorld, World}, +}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + camera::ExtractedCamera, + mesh::MeshVertexBufferLayout, + prelude::{Camera, Mesh}, + render_asset::RenderAssets, + render_phase::{ + sort_phase_system, AddRenderCommand, DrawFunctions, PhaseItem, RenderCommand, + RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, + }, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingType, BlendState, BufferBindingType, ColorTargetState, + ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, Extent3d, FragmentState, + FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState, + RenderPipelineDescriptor, Shader, ShaderDefVal, ShaderRef, ShaderStages, ShaderType, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + StencilFaceState, StencilState, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, VertexState, + }, + renderer::RenderDevice, + texture::TextureCache, + view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities}, + Extract, RenderApp, RenderStage, +}; +use bevy_utils::{tracing::error, HashMap}; + +use crate::{ + AlphaMode, DrawMesh, Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, + MeshPipelineKey, MeshUniform, RenderMaterials, SetMaterialBindGroup, SetMeshBindGroup, + MAX_DIRECTIONAL_LIGHTS, +}; + +use std::{hash::Hash, marker::PhantomData}; + +pub const PREPASS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 921124473254008983); + +pub const PREPASS_BINDINGS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 5533152893177403494); + +pub struct PrepassPlugin(PhantomData); + +impl Default for PrepassPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for PrepassPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + PREPASS_SHADER_HANDLE, + "prepass.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + PREPASS_BINDINGS_SHADER_HANDLE, + "prepass_bindings.wgsl", + Shader::from_wgsl + ); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_system_to_stage(RenderStage::Extract, extract_camera_prepass_phase) + .add_system_to_stage(RenderStage::Prepare, prepare_prepass_textures) + .add_system_to_stage(RenderStage::Queue, queue_prepass_view_bind_group::) + .add_system_to_stage(RenderStage::Queue, queue_prepass_material_meshes::) + .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage( + RenderStage::PhaseSort, + sort_phase_system::, + ) + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::() + .init_resource::>>() + .add_render_command::>() + .add_render_command::>(); + } +} + +#[derive(Resource)] +pub struct PrepassPipeline { + pub view_layout: BindGroupLayout, + pub mesh_layout: BindGroupLayout, + pub skinned_mesh_layout: BindGroupLayout, + pub material_layout: BindGroupLayout, + pub material_vertex_shader: Option>, + pub material_fragment_shader: Option>, + pub material_pipeline: MaterialPipeline, + _marker: PhantomData, +} + +impl FromWorld for PrepassPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let asset_server = world.resource::(); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + // View + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }, + ], + label: Some("prepass_view_layout"), + }); + + let mesh_pipeline = world.resource::(); + + PrepassPipeline { + view_layout, + mesh_layout: mesh_pipeline.mesh_layout.clone(), + skinned_mesh_layout: mesh_pipeline.skinned_mesh_layout.clone(), + material_vertex_shader: match M::prepass_vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + material_fragment_shader: match M::prepass_fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + material_layout: M::bind_group_layout(render_device), + material_pipeline: world.resource::>().clone(), + _marker: PhantomData, + } + } +} + +impl SpecializedMeshPipeline for PrepassPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = MaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let mut bind_group_layout = vec![self.view_layout.clone()]; + let mut shader_defs = Vec::new(); + let mut vertex_attributes = Vec::new(); + + // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. + // The main limitation right now is that bind group order is hardcoded in shaders. + bind_group_layout.insert(1, self.material_layout.clone()); + + if key.mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.mesh_key.contains(MeshPipelineKey::ALPHA_MASK) { + shader_defs.push("ALPHA_MASK".into()); + } + + if layout.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + shader_defs.push(ShaderDefVal::Int( + "MAX_DIRECTIONAL_LIGHTS".to_string(), + MAX_DIRECTIONAL_LIGHTS as i32, + )); + + if layout.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); + } + + if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(2)); + shader_defs.push("NORMAL_PREPASS".into()); + + if layout.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); + } + } + + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) + { + shader_defs.push("SKINNED".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(4)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(5)); + bind_group_layout.insert(2, self.skinned_mesh_layout.clone()); + } else { + bind_group_layout.insert(2, self.mesh_layout.clone()); + } + + let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; + + // The fragment shader is only used when the normal prepass is enabled or the material uses an alpha mask + let fragment = if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) + || key.mesh_key.contains(MeshPipelineKey::ALPHA_MASK) + { + // Use the fragment shader from the material if present + let frag_shader_handle = if let Some(handle) = &self.material_fragment_shader { + handle.clone() + } else { + PREPASS_SHADER_HANDLE.typed::() + }; + + let mut targets = vec![]; + // When the normal prepass is enabled we need a target to be able to write to it. + if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + targets.push(Some(ColorTargetState { + format: TextureFormat::Rgb10a2Unorm, + blend: Some(BlendState::REPLACE), + write_mask: ColorWrites::ALL, + })); + } + + Some(FragmentState { + shader: frag_shader_handle, + entry_point: "fragment".into(), + shader_defs: shader_defs.clone(), + targets, + }) + } else { + None + }; + + // Use the vertex shader from the material if present + let vert_shader_handle = if let Some(handle) = &self.material_vertex_shader { + handle.clone() + } else { + PREPASS_SHADER_HANDLE.typed::() + }; + + let mut descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: vert_shader_handle, + entry_point: "vertex".into(), + shader_defs, + buffers: vec![vertex_buffer_layout], + }, + fragment, + layout: Some(bind_group_layout), + primitive: PrimitiveState { + topology: key.mesh_key.primitive_topology(), + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: DEPTH_PREPASS_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("prepass_pipeline".into()), + }; + + // This is a bit risky because it's possible to change something that would + // break the prepass but be fine in the main pass. + // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. + M::specialize(&self.material_pipeline, &mut descriptor, layout, key)?; + + Ok(descriptor) + } +} + +// Extract the render phases for the prepass +pub fn extract_camera_prepass_phase( + mut commands: Commands, + cameras_3d: Extract< + Query< + ( + Entity, + &Camera, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + ), + With, + >, + >, +) { + for (entity, camera, depth_prepass, normal_prepass) in cameras_3d.iter() { + if !camera.is_active { + continue; + } + + let mut entity = commands.get_or_spawn(entity); + if depth_prepass.is_some() || normal_prepass.is_some() { + entity.insert(( + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + if depth_prepass.is_some() { + entity.insert(DepthPrepass); + } + if normal_prepass.is_some() { + entity.insert(NormalPrepass); + } + } +} + +// Prepares the textures used by the prepass +pub fn prepare_prepass_textures( + mut commands: Commands, + mut texture_cache: ResMut, + msaa: Res, + render_device: Res, + views_3d: Query< + ( + Entity, + &ExtractedCamera, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + ), + ( + With>, + With>, + ), + >, +) { + let mut depth_textures = HashMap::default(); + let mut normal_textures = HashMap::default(); + for (entity, camera, depth_prepass, normal_prepass) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let cached_depth_texture = depth_prepass.is_some().then(|| { + depth_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: Some("prepass_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: DEPTH_PREPASS_FORMAT, + usage: TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }; + texture_cache.get(&render_device, descriptor) + }) + .clone() + }); + + let cached_normals_texture = normal_prepass.is_some().then(|| { + normal_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_normal_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: NORMAL_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }, + ) + }) + .clone() + }); + + commands.entity(entity).insert(ViewPrepassTextures { + depth: cached_depth_texture, + normal: cached_normals_texture, + size, + }); + } +} + +#[derive(Default, Resource)] +pub struct PrepassViewBindGroup { + bind_group: Option, +} + +pub fn queue_prepass_view_bind_group( + render_device: Res, + prepass_pipeline: Res>, + view_uniforms: Res, + mut prepass_view_bind_group: ResMut, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + prepass_view_bind_group.bind_group = + Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding, + }], + label: Some("prepass_view_bind_group"), + layout: &prepass_pipeline.view_layout, + })); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_prepass_material_meshes( + opaque_draw_functions: Res>, + alpha_mask_draw_functions: Res>, + prepass_pipeline: Res>, + mut pipelines: ResMut>>, + pipeline_cache: Res, + msaa: Res, + render_meshes: Res>, + render_materials: Res>, + material_meshes: Query<(&Handle, &Handle, &MeshUniform)>, + mut views: Query<( + &ExtractedView, + &VisibleEntities, + &mut RenderPhase, + &mut RenderPhase, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + )>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let opaque_draw_prepass = opaque_draw_functions + .read() + .get_id::>() + .unwrap(); + let alpha_mask_draw_prepass = alpha_mask_draw_functions + .read() + .get_id::>() + .unwrap(); + for ( + view, + visible_entities, + mut opaque_phase, + mut alpha_mask_phase, + depth_prepass, + normal_prepass, + ) in &mut views + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples); + if depth_prepass.is_some() { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + let rangefinder = view.rangefinder3d(); + + for visible_entity in &visible_entities.entities { + let Ok((material_handle, mesh_handle, mesh_uniform)) = material_meshes.get(*visible_entity) else { + continue; + }; + + let (Some(material), Some(mesh)) = ( + render_materials.get(material_handle), + render_meshes.get(mesh_handle), + ) else { + continue; + }; + + let mut mesh_key = + MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | view_key; + let alpha_mode = material.properties.alpha_mode; + match alpha_mode { + AlphaMode::Opaque => {} + AlphaMode::Mask(_) => mesh_key |= MeshPipelineKey::ALPHA_MASK, + AlphaMode::Blend => continue, + } + + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &prepass_pipeline, + MaterialPipelineKey { + mesh_key, + bind_group_data: material.key.clone(), + }, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + let distance = + rangefinder.distance(&mesh_uniform.transform) + material.properties.depth_bias; + match alpha_mode { + AlphaMode::Opaque => { + opaque_phase.add(Opaque3dPrepass { + entity: *visible_entity, + draw_function: opaque_draw_prepass, + pipeline_id, + distance, + }); + } + AlphaMode::Mask(_) => { + alpha_mask_phase.add(AlphaMask3dPrepass { + entity: *visible_entity, + draw_function: alpha_mask_draw_prepass, + pipeline_id, + distance, + }); + } + AlphaMode::Blend => {} + } + } + } +} + +pub struct SetPrepassViewBindGroup; +impl RenderCommand

for SetPrepassViewBindGroup { + type Param = SRes; + type ViewWorldQuery = Read; + type ItemWorldQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + view_uniform_offset: &'_ ViewUniformOffset, + _entity: (), + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + pass.set_bind_group( + I, + prepass_view_bind_group.bind_group.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + RenderCommandResult::Success + } +} + +pub type DrawPrepass = ( + SetItemPipeline, + SetPrepassViewBindGroup<0>, + SetMaterialBindGroup, + SetMeshBindGroup<2>, + DrawMesh, +); diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl new file mode 100644 index 00000000000000..d2050675f891ad --- /dev/null +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -0,0 +1,81 @@ +#import bevy_pbr::prepass_bindings +#import bevy_pbr::mesh_functions + +// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can +// pass them to custom prepass shaders like pbr_prepass.wgsl. +struct Vertex { + @location(0) position: vec3, + +#ifdef VERTEX_UVS + @location(1) uv: vec2, +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS + @location(2) normal: vec3, +#ifdef VERTEX_TANGENTS + @location(3) tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS + +#ifdef SKINNED + @location(4) joint_indices: vec4, + @location(5) joint_weights: vec4, +#endif // SKINNED +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS +} + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef SKINNED + var model = skin_model(vertex.joint_indices, vertex.joint_weights); +#else // SKINNED + var model = mesh.model; +#endif // SKINNED + + out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); + +#ifdef VERTEX_UVS + out.uv = vertex.uv; +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS +#ifdef SKINNED + out.world_normal = skin_normals(model, vertex.normal); +#else // SKINNED + out.world_normal = mesh_normal_local_to_world(vertex.normal); +#endif // SKINNED + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_tangent_local_to_world(model, vertex.tangent); +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS + + return out; +} + +#ifdef NORMAL_PREPASS +struct FragmentInput { + @location(1) world_normal: vec3, +} + +@fragment +fn fragment(in: FragmentInput) -> @location(0) vec4 { + return vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +} +#endif // NORMAL_PREPASS diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl new file mode 100644 index 00000000000000..cd338af0ed7da5 --- /dev/null +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -0,0 +1,18 @@ +#define_import_path bevy_pbr::prepass_bindings + +#import bevy_pbr::mesh_view_types +#import bevy_pbr::mesh_types + +@group(0) @binding(0) +var view: View; + +// Material bindings will be in @group(1) + +@group(2) @binding(0) +var mesh: Mesh; + +#ifdef SKINNED +@group(2) @binding(1) +var joint_matrices: SkinnedMesh; +#import bevy_pbr::skinning +#endif diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 92b65621922ac5..dc4d78876d9e5f 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -5,6 +5,7 @@ use crate::{ }; use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_core_pipeline::prepass::ViewPrepassTextures; use bevy_ecs::{ prelude::*, query::ROQueryItem, @@ -19,12 +20,14 @@ use bevy_render::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, GpuBufferInfo, Mesh, MeshVertexBufferLayout, }, + prelude::Msaa, render_asset::RenderAssets, render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, DefaultImageSampler, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, + BevyDefault, DefaultImageSampler, FallbackImagesDepth, FallbackImagesMsaa, GpuImage, Image, + ImageSampler, TextureFormatPixelInfo, }, view::{ComputedVisibility, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, Extract, RenderApp, RenderStage, @@ -254,6 +257,7 @@ pub fn extract_skinned_meshes( #[derive(Resource, Clone)] pub struct MeshPipeline { pub view_layout: BindGroupLayout, + pub view_layout_multisampled: BindGroupLayout, pub mesh_layout: BindGroupLayout, pub skinned_mesh_layout: BindGroupLayout, // This dummy white texture is to be used in place of optional StandardMaterial textures @@ -272,8 +276,12 @@ impl FromWorld for MeshPipeline { let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ + /// Returns the appropriate bind group layout vec based on the parameters + fn layout_entries( + clustered_forward_buffer_binding_type: BufferBindingType, + multisampled: bool, + ) -> Vec { + let mut entries = vec![ // View BindGroupLayoutEntry { binding: 0, @@ -381,6 +389,7 @@ impl FromWorld for MeshPipeline { }, count: None, }, + // Globals BindGroupLayoutEntry { binding: 9, visibility: ShaderStages::VERTEX_FRAGMENT, @@ -391,10 +400,45 @@ impl FromWorld for MeshPipeline { }, count: None, }, - ], + ]; + if cfg!(not(feature = "webgl")) { + // Depth texture + entries.push(BindGroupLayoutEntry { + binding: 10, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled, + sample_type: TextureSampleType::Depth, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }); + // Normal texture + entries.push(BindGroupLayoutEntry { + binding: 11, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }); + } + entries + } + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { label: Some("mesh_view_layout"), + entries: &layout_entries(clustered_forward_buffer_binding_type, false), }); + let view_layout_multisampled = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("mesh_view_layout_multisampled"), + entries: &layout_entries(clustered_forward_buffer_binding_type, true), + }); + let mesh_binding = BindGroupLayoutEntry { binding: 0, visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, @@ -480,6 +524,7 @@ impl FromWorld for MeshPipeline { MeshPipeline { view_layout, + view_layout_multisampled, mesh_layout, skinned_mesh_layout, clustered_forward_buffer_binding_type, @@ -516,6 +561,9 @@ bitflags::bitflags! { const HDR = (1 << 1); const TONEMAP_IN_SHADER = (1 << 2); const DEBAND_DITHER = (1 << 3); + const DEPTH_PREPASS = (1 << 4); + const NORMAL_PREPASS = (1 << 5); + const ALPHA_MASK = (1 << 6); const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; } @@ -607,7 +655,14 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); } - let mut bind_group_layout = vec![self.view_layout.clone()]; + let mut bind_group_layout = match key.msaa_samples() { + 1 => vec![self.view_layout.clone()], + _ => { + shader_defs.push("MULTISAMPLED".into()); + vec![self.view_layout_multisampled.clone()] + } + }; + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) { @@ -646,9 +701,10 @@ impl SpecializedMeshPipeline for MeshPipeline { } } - let format = match key.contains(MeshPipelineKey::HDR) { - true => ViewTarget::TEXTURE_FORMAT_HDR, - false => TextureFormat::bevy_default(), + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() }; Ok(RenderPipelineDescriptor { @@ -681,7 +737,7 @@ impl SpecializedMeshPipeline for MeshPipeline { depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth32Float, depth_write_enabled, - depth_compare: CompareFunction::Greater, + depth_compare: CompareFunction::GreaterEqual, stencil: StencilState { front: StencilFaceState::IGNORE, back: StencilFaceState::IGNORE, @@ -803,7 +859,15 @@ pub fn queue_mesh_view_bind_groups( light_meta: Res, global_light_meta: Res, view_uniforms: Res, - views: Query<(Entity, &ViewShadowBindings, &ViewClusterBindings)>, + views: Query<( + Entity, + &ViewShadowBindings, + &ViewClusterBindings, + Option<&ViewPrepassTextures>, + )>, + mut fallback_images: FallbackImagesMsaa, + mut fallback_depths: FallbackImagesDepth, + msaa: Res, globals_buffer: Res, ) { if let (Some(view_binding), Some(light_binding), Some(point_light_binding), Some(globals)) = ( @@ -812,58 +876,94 @@ pub fn queue_mesh_view_bind_groups( global_light_meta.gpu_point_lights.binding(), globals_buffer.buffer.binding(), ) { - for (entity, view_shadow_bindings, view_cluster_bindings) in &views { + for (entity, view_shadow_bindings, view_cluster_bindings, prepass_textures) in &views { + let layout = if msaa.samples > 1 { + &mesh_pipeline.view_layout_multisampled + } else { + &mesh_pipeline.view_layout + }; + + let mut entries = vec![ + BindGroupEntry { + binding: 0, + resource: view_binding.clone(), + }, + BindGroupEntry { + binding: 1, + resource: light_binding.clone(), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::TextureView( + &view_shadow_bindings.point_light_depth_texture_view, + ), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::Sampler(&shadow_pipeline.point_light_sampler), + }, + BindGroupEntry { + binding: 4, + resource: BindingResource::TextureView( + &view_shadow_bindings.directional_light_depth_texture_view, + ), + }, + BindGroupEntry { + binding: 5, + resource: BindingResource::Sampler(&shadow_pipeline.directional_light_sampler), + }, + BindGroupEntry { + binding: 6, + resource: point_light_binding.clone(), + }, + BindGroupEntry { + binding: 7, + resource: view_cluster_bindings.light_index_lists_binding().unwrap(), + }, + BindGroupEntry { + binding: 8, + resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(), + }, + BindGroupEntry { + binding: 9, + resource: globals.clone(), + }, + ]; + + // When using WebGL with MSAA, we can't create the fallback textures required by the prepass + // When using WebGL, and MSAA is disabled, we can't bind the textures either + if cfg!(not(feature = "webgl")) { + let depth_view = match prepass_textures.and_then(|x| x.depth.as_ref()) { + Some(texture) => &texture.default_view, + None => { + &fallback_depths + .image_for_samplecount(msaa.samples) + .texture_view + } + }; + entries.push(BindGroupEntry { + binding: 10, + resource: BindingResource::TextureView(depth_view), + }); + + let normal_view = match prepass_textures.and_then(|x| x.normal.as_ref()) { + Some(texture) => &texture.default_view, + None => { + &fallback_images + .image_for_samplecount(msaa.samples) + .texture_view + } + }; + entries.push(BindGroupEntry { + binding: 11, + resource: BindingResource::TextureView(normal_view), + }); + } + let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: view_binding.clone(), - }, - BindGroupEntry { - binding: 1, - resource: light_binding.clone(), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::TextureView( - &view_shadow_bindings.point_light_depth_texture_view, - ), - }, - BindGroupEntry { - binding: 3, - resource: BindingResource::Sampler(&shadow_pipeline.point_light_sampler), - }, - BindGroupEntry { - binding: 4, - resource: BindingResource::TextureView( - &view_shadow_bindings.directional_light_depth_texture_view, - ), - }, - BindGroupEntry { - binding: 5, - resource: BindingResource::Sampler( - &shadow_pipeline.directional_light_sampler, - ), - }, - BindGroupEntry { - binding: 6, - resource: point_light_binding.clone(), - }, - BindGroupEntry { - binding: 7, - resource: view_cluster_bindings.light_index_lists_binding().unwrap(), - }, - BindGroupEntry { - binding: 8, - resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(), - }, - BindGroupEntry { - binding: 9, - resource: globals.clone(), - }, - ], + entries: &entries, label: Some("mesh_view_bind_group"), - layout: &mesh_pipeline.view_layout, + layout, }); commands.entity(entity).insert(MeshViewBindGroup { diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index cfe9ae87ef6a3a..999d78152c2c1c 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -43,3 +43,15 @@ var cluster_offsets_and_counts: ClusterOffsetsAndCounts; @group(0) @binding(9) var globals: Globals; + +#ifdef MULTISAMPLED +@group(0) @binding(10) +var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(11) +var normal_prepass_texture: texture_multisampled_2d; +#else +@group(0) @binding(10) +var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(11) +var normal_prepass_texture: texture_2d; +#endif \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index f851862f250c64..5da8ca9030d237 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -5,13 +5,13 @@ #endif -fn alpha_discard(material: StandardMaterial, output_color: vec4) -> vec4{ +fn alpha_discard(material: StandardMaterial, output_color: vec4) -> vec4 { var color = output_color; - if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { + if (material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u { // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 color.a = 1.0; - } else if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { - if (color.a >= material.alpha_cutoff) { + } else if (material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u { + if color.a >= material.alpha_cutoff { // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque color.a = 1.0; } else { @@ -75,7 +75,7 @@ fn apply_normal_mapping( #ifdef STANDARDMATERIAL_NORMAL_MAP // Nt is the tangent-space normal. var Nt = textureSample(normal_map_texture, normal_map_sampler, uv).rgb; - if ((standard_material_flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { + if (standard_material_flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { // Only use the xy components and derive z for 2-component normal maps. Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); @@ -83,7 +83,7 @@ fn apply_normal_mapping( Nt = Nt * 2.0 - 1.0; } // Normal maps authored for DirectX require flipping the y component - if ((standard_material_flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u) { + if (standard_material_flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u { Nt.y = -Nt.y; } // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from @@ -106,7 +106,7 @@ fn calculate_view( is_orthographic: bool, ) -> vec3 { var V: vec3; - if (is_orthographic) { + if is_orthographic { // Orthographic view vector V = normalize(vec3(view.view_proj[0].z, view.view_proj[1].z, view.view_proj[2].z)); } else { @@ -151,6 +151,7 @@ fn pbr_input_new() -> PbrInput { return pbr_input; } +#ifndef NORMAL_PREPASS fn pbr( in: PbrInput, ) -> vec4 { @@ -232,10 +233,11 @@ fn pbr( let specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); output_color = vec4( - light_accum + - (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + - emissive.rgb * output_color.a, - output_color.a); + light_accum + + (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + + emissive.rgb * output_color.a, + output_color.a + ); output_color = cluster_debug_visualization( output_color, @@ -247,6 +249,7 @@ fn pbr( return output_color; } +#endif // NORMAL_PREPASS #ifdef TONEMAP_IN_SHADER fn tone_mapping(in: vec4) -> vec4 { @@ -257,11 +260,11 @@ fn tone_mapping(in: vec4) -> vec4 { // Not needed with sRGB buffer // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); } -#endif +#endif // TONEMAP_IN_SHADER #ifdef DEBAND_DITHER fn dither(color: vec4, pos: vec2) -> vec4 { return vec4(color.rgb + screen_space_dither(pos.xy), color.a); } -#endif +#endif // DEBAND_DITHER diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl new file mode 100644 index 00000000000000..54584ef682666d --- /dev/null +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -0,0 +1,77 @@ +#import bevy_pbr::prepass_bindings +#import bevy_pbr::pbr_bindings +#import bevy_pbr::pbr_functions + +struct FragmentInput { + @builtin(front_facing) is_front: bool, + @builtin(position) frag_coord: vec4, +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif // VERTEX_UVS +#ifdef NORMAL_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS +}; + +// We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff +fn prepass_alpha_discard(in: FragmentInput) { +#ifdef ALPHA_MASK + var output_color: vec4 = material.base_color; + +#ifdef VERTEX_UVS + if (material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { + output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv); + } +#endif // VERTEX_UVS + + if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) && output_color.a < material.alpha_cutoff { + discard; + } +#endif // ALPHA_MASK +} + +#ifdef NORMAL_PREPASS + +@fragment +fn fragment(in: FragmentInput) -> @location(0) vec4 { + prepass_alpha_discard(in); + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if (material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + let world_normal = prepare_world_normal( + in.world_normal, + (material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u, + in.is_front, + ); + + let normal = apply_normal_mapping( + material.flags, + world_normal, +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + in.world_tangent, +#endif // STANDARDMATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#ifdef VERTEX_UVS + in.uv, +#endif // VERTEX_UVS + ); + + return vec4(normal * 0.5 + vec3(0.5), 1.0); + } else { + return vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); + } +} + +#else // NORMAL_PREPASS + +@fragment +fn fragment(in: FragmentInput) { + prepass_alpha_discard(in); +} + +#endif // NORMAL_PREPASS + diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 20b71ee83457e5..641010868f0869 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -11,7 +11,7 @@ fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3 { vec3(1.0) ); - return value * mix( vec3(1.0), rgb, vec3(saturation)); + return value * mix(vec3(1.0), rgb, vec3(saturation)); } fn random1D(s: f32) -> f32 { @@ -25,3 +25,25 @@ fn random1D(s: f32) -> f32 { fn coords_to_viewport_uv(position: vec2, viewport: vec4) -> vec2 { return (position - viewport.xy) / viewport.zw; } + +#ifndef NORMAL_PREPASS +fn prepass_normal(frag_coord: vec4, sample_index: u32) -> vec3 { +#ifdef MULTISAMPLED + let normal_sample = textureLoad(normal_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let normal_sample = textureLoad(normal_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return normal_sample.xyz * 2.0 - vec3(1.0); +} +#endif // NORMAL_PREPASS + +#ifndef DEPTH_PREPASS +fn prepass_depth(frag_coord: vec4, sample_index: u32) -> f32 { +#ifdef MULTISAMPLED + let depth_sample = textureLoad(depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let depth_sample = textureLoad(depth_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return depth_sample; +} +#endif // DEPTH_PREPASS \ No newline at end of file diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index 6f0a32e3604ac0..9b8ef0c500498e 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -1,13 +1,17 @@ use crate::{render_resource::*, texture::DefaultImageSampler}; -use bevy_derive::Deref; -use bevy_ecs::{prelude::FromWorld, system::Resource}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{FromWorld, Res, ResMut}, + system::{Resource, SystemParam}, +}; use bevy_math::Vec2; +use bevy_utils::HashMap; use wgpu::{Extent3d, TextureDimension, TextureFormat}; use crate::{ prelude::Image, renderer::{RenderDevice, RenderQueue}, - texture::{BevyDefault, GpuImage, ImageSampler}, + texture::{image::TextureFormatPixelInfo, BevyDefault, GpuImage, ImageSampler}, }; /// A [`RenderApp`](crate::RenderApp) resource that contains the default "fallback image", @@ -17,36 +21,117 @@ use crate::{ #[derive(Resource, Deref)] pub struct FallbackImage(GpuImage); +fn fallback_image_new( + render_device: &RenderDevice, + render_queue: &RenderQueue, + default_sampler: &DefaultImageSampler, + format: TextureFormat, + samples: u32, +) -> GpuImage { + // TODO make this configurable + let data = vec![255; format.pixel_size()]; + + let mut image = Image::new_fill(Extent3d::default(), TextureDimension::D2, &data, format); + image.texture_descriptor.sample_count = samples; + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + + // We can't create textures with data when it's a depth texture or when using multiple samples + let texture = if format.describe().sample_type == TextureSampleType::Depth || samples > 1 { + render_device.create_texture(&image.texture_descriptor) + } else { + render_device.create_texture_with_data(render_queue, &image.texture_descriptor, &image.data) + }; + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + let sampler = match image.sampler_descriptor { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor), + }; + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), + } +} + impl FromWorld for FallbackImage { fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { let render_device = world.resource::(); let render_queue = world.resource::(); let default_sampler = world.resource::(); - let image = Image::new_fill( - Extent3d::default(), - TextureDimension::D2, - &[255u8; 4], - TextureFormat::bevy_default(), - ); - let texture = render_device.create_texture_with_data( + Self(fallback_image_new( + render_device, render_queue, - &image.texture_descriptor, - &image.data, - ); - let texture_view = texture.create_view(&TextureViewDescriptor::default()); - let sampler = match image.sampler_descriptor { - ImageSampler::Default => (**default_sampler).clone(), - ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor), - }; - Self(GpuImage { - texture, - texture_view, - texture_format: image.texture_descriptor.format, - sampler, - size: Vec2::new( - image.texture_descriptor.size.width as f32, - image.texture_descriptor.size.height as f32, - ), + default_sampler, + TextureFormat::bevy_default(), + 1, + )) + } +} + +// TODO these could be combined in one FallbackImage cache. + +/// A Cache of fallback textures that uses the sample count as a key +/// +/// # WARNING +/// Images using MSAA with sample count > 1 are not initialized with data, therefore, +/// you shouldn't sample them before writing data to them first. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct FallbackImageMsaaCache(HashMap); + +/// A Cache of fallback depth textures that uses the sample count as a key +/// +/// # WARNING +/// Detph images are never initialized with data, therefore, +/// you shouldn't sample them before writing data to them first. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct FallbackImageDepthCache(HashMap); + +#[derive(SystemParam)] +pub struct FallbackImagesMsaa<'w> { + cache: ResMut<'w, FallbackImageMsaaCache>, + render_device: Res<'w, RenderDevice>, + render_queue: Res<'w, RenderQueue>, + default_sampler: Res<'w, DefaultImageSampler>, +} + +impl<'w> FallbackImagesMsaa<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32) -> &GpuImage { + self.cache.entry(sample_count).or_insert_with(|| { + fallback_image_new( + &self.render_device, + &self.render_queue, + &self.default_sampler, + TextureFormat::bevy_default(), + sample_count, + ) + }) + } +} + +#[derive(SystemParam)] +pub struct FallbackImagesDepth<'w> { + cache: ResMut<'w, FallbackImageDepthCache>, + render_device: Res<'w, RenderDevice>, + render_queue: Res<'w, RenderQueue>, + default_sampler: Res<'w, DefaultImageSampler>, +} + +impl<'w> FallbackImagesDepth<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32) -> &GpuImage { + self.cache.entry(sample_count).or_insert_with(|| { + fallback_image_new( + &self.render_device, + &self.render_queue, + &self.default_sampler, + TextureFormat::Depth32Float, + sample_count, + ) }) } } diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 89637d730b2fc8..0517bf6a288dec 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -102,6 +102,8 @@ impl Plugin for ImagePlugin { .insert_resource(DefaultImageSampler(default_sampler)) .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() .add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system); } } diff --git a/examples/README.md b/examples/README.md index f5ff72e6752ba5..72b877bc9a3ee1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -267,6 +267,7 @@ Example | Description [Material](../examples/shader/shader_material.rs) | A shader and a material that uses it [Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language [Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates +[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the depth texture generated in a prepass [Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) diff --git a/examples/shader/shader_prepass.rs b/examples/shader/shader_prepass.rs new file mode 100644 index 00000000000000..6e0f9a934e9ec0 --- /dev/null +++ b/examples/shader/shader_prepass.rs @@ -0,0 +1,244 @@ +//! Bevy has an optional prepass that is controlled per-material. A prepass is a rendering pass that runs before the main pass. +//! It will optionally generate various view textures. Currently it supports depth and normal textures. +//! The textures are not generated for any material using alpha blending. +//! +//! # WARNING +//! The prepass currently doesn't work on `WebGL`. + +use bevy::{ + core_pipeline::prepass::{DepthPrepass, NormalPrepass}, + pbr::{NotShadowCaster, PbrPlugin}, + prelude::*, + reflect::TypeUuid, + render::render_resource::{AsBindGroup, ShaderRef}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(PbrPlugin { + // The prepass is enabled by default on the StandardMaterial, + // but you can disable it if you need to. + // prepass_enabled: false, + ..default() + })) + .add_plugin(MaterialPlugin::::default()) + .add_plugin(MaterialPlugin:: { + // This material only needs to read the prepass textures, + // but the meshes using it should not contribute to the prepass render, so we can disable it. + prepass_enabled: false, + ..default() + }) + .add_startup_system(setup) + .add_system(rotate) + .add_system(update) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut std_materials: ResMut>, + mut depth_materials: ResMut>, + asset_server: Res, +) { + // camera + commands.spawn(( + Camera3dBundle { + transform: Transform::from_xyz(-2.0, 3., 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }, + // To enable the prepass you need to add the components associated with the ones you need + // This will write the depth buffer to a texture that you can use in the main pass + DepthPrepass, + // This will generate a texture containing world normals (with normal maps applied) + NormalPrepass, + )); + + // plane + commands.spawn(PbrBundle { + mesh: meshes.add(shape::Plane { size: 5.0 }.into()), + material: std_materials.add(Color::rgb(0.3, 0.5, 0.3).into()), + ..default() + }); + + // A quad that shows the outputs of the prepass + // To make it easy, we just draw a big quad right in front of the camera. For a real application, this isn't ideal. + commands.spawn(( + MaterialMeshBundle { + mesh: meshes.add(shape::Quad::new(Vec2::new(20.0, 20.0)).into()), + material: depth_materials.add(PrepassOutputMaterial { + show_depth: 0.0, + show_normal: 0.0, + }), + transform: Transform::from_xyz(-0.75, 1.25, 3.0) + .looking_at(Vec3::new(2.0, -2.5, -5.0), Vec3::Y), + ..default() + }, + NotShadowCaster, + )); + + // Opaque cube using the StandardMaterial + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: std_materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(-1.0, 0.5, 0.0), + ..default() + }, + Rotates, + )); + + // Cube with alpha mask + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: std_materials.add(StandardMaterial { + alpha_mode: AlphaMode::Mask(1.0), + base_color_texture: Some(asset_server.load("branding/icon.png")), + ..default() + }), + + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + + // Cube with alpha blending. + // Transparent materials are ignored by the prepass + commands.spawn(MaterialMeshBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(CustomMaterial { + color: Color::WHITE, + color_texture: Some(asset_server.load("branding/icon.png")), + alpha_mode: AlphaMode::Blend, + }), + transform: Transform::from_xyz(1.0, 0.5, 0.0), + ..default() + }); + + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + + let style = TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 18.0, + color: Color::WHITE, + }; + + commands.spawn( + TextBundle::from_sections(vec![ + TextSection::new("Prepass Output: transparent\n", style.clone()), + TextSection::new("\n\n", style.clone()), + TextSection::new("Controls\n", style.clone()), + TextSection::new("---------------\n", style.clone()), + TextSection::new("Space - Change output\n", style), + ]) + .with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ..default() + }), + ); +} + +// This is the struct that will be passed to your shader +#[derive(AsBindGroup, TypeUuid, Debug, Clone)] +#[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] +pub struct CustomMaterial { + #[uniform(0)] + color: Color, + #[texture(1)] + #[sampler(2)] + color_texture: Option>, + alpha_mode: AlphaMode, +} + +/// Not shown in this example, but if you need to specialize your material, the specialize +/// function will also be used by the prepass +impl Material for CustomMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/custom_material.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } + + // You can override the default shaders used in the prepass if your material does + // anything not supported by the default prepass + // fn prepass_fragment_shader() -> ShaderRef { + // "shaders/custom_material.wgsl".into() + // } +} + +#[derive(Component)] +struct Rotates; + +fn rotate(mut q: Query<&mut Transform, With>, time: Res