From e030b24a46f8526bbceb1a3cb83fe8309e2dea81 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Thu, 19 Jan 2023 22:11:13 +0000 Subject: [PATCH] Add depth and normal prepass (#6284) # Objective - Add a configurable prepass - A depth prepass is useful for various shader effects and to reduce overdraw. It can be expansive depending on the scene so it's important to be able to disable it if you don't need any effects that uses it or don't suffer from excessive overdraw. - The goal is to eventually use it for things like TAA, Ambient Occlusion, SSR and various other techniques that can benefit from having a prepass. ## Solution The prepass node is inserted before the main pass. It runs for each `Camera3d` with a prepass component (`DepthPrepass`, `NormalPrepass`). The presence of one of those components is used to determine which textures are generated in the prepass. When any prepass is enabled, the depth buffer generated will be used by the main pass to reduce overdraw. The prepass runs for each `Material` created with the `MaterialPlugin::prepass_enabled` option set to `true`. You can overload the shader used by the prepass by using `Material::prepass_vertex_shader()` and/or `Material::prepass_fragment_shader()`. It will also use the `Material::specialize()` for more advanced use cases. It is enabled by default on all materials. The prepass works on opaque materials and materials using an alpha mask. Transparent materials are ignored. The `StandardMaterial` overloads the prepass fragment shader to support alpha mask and normal maps. --- ## Changelog - Add a new `PrepassNode` that runs before the main pass - Add a `PrepassPlugin` to extract/prepare/queue the necessary data - Add a `DepthPrepass` and `NormalPrepass` component to control which textures will be created by the prepass and available in later passes. - Add a new `prepass_enabled` flag to the `MaterialPlugin` that will control if a material uses the prepass or not. - Add a new `prepass_enabled` flag to the `PbrPlugin` to control if the StandardMaterial uses the prepass. Currently defaults to false. - Add `Material::prepass_vertex_shader()` and `Material::prepass_fragment_shader()` to control the prepass from the `Material` ## Notes In bevy's sample 3d scene, the performance is actually worse when enabling the prepass, but on more complex scenes the performance is generally better. I would like more testing on this, but @DGriffin91 has reported a very noticeable improvements in some scenes. The prepass is also used by @JMS55 for TAA and GTAO discord thread: This PR was built on top of the work of multiple people Co-Authored-By: @superdump Co-Authored-By: @robtfm Co-Authored-By: @JMS55 Co-authored-by: Charles Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> --- Cargo.toml | 11 + assets/shaders/show_prepass.wgsl | 26 + .../src/core_3d/main_pass_3d_node.rs | 37 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 89 ++- crates/bevy_core_pipeline/src/lib.rs | 4 + crates/bevy_core_pipeline/src/prepass/mod.rs | 147 +++++ crates/bevy_core_pipeline/src/prepass/node.rs | 134 ++++ crates/bevy_pbr/src/lib.rs | 31 +- crates/bevy_pbr/src/material.rs | 37 +- crates/bevy_pbr/src/pbr_material.rs | 20 +- crates/bevy_pbr/src/prepass/mod.rs | 605 ++++++++++++++++++ crates/bevy_pbr/src/prepass/prepass.wgsl | 81 +++ .../src/prepass/prepass_bindings.wgsl | 18 + crates/bevy_pbr/src/render/mesh.rs | 220 +++++-- .../src/render/mesh_view_bindings.wgsl | 12 + crates/bevy_pbr/src/render/pbr_functions.wgsl | 29 +- crates/bevy_pbr/src/render/pbr_prepass.wgsl | 77 +++ crates/bevy_pbr/src/render/utils.wgsl | 24 +- .../bevy_render/src/texture/fallback_image.rs | 139 +++- crates/bevy_render/src/texture/mod.rs | 2 + examples/README.md | 1 + examples/shader/shader_prepass.rs | 244 +++++++ 22 files changed, 1833 insertions(+), 155 deletions(-) create mode 100644 assets/shaders/show_prepass.wgsl create mode 100644 crates/bevy_core_pipeline/src/prepass/mod.rs create mode 100644 crates/bevy_core_pipeline/src/prepass/node.rs create mode 100644 crates/bevy_pbr/src/prepass/mod.rs create mode 100644 crates/bevy_pbr/src/prepass/prepass.wgsl create mode 100644 crates/bevy_pbr/src/prepass/prepass_bindings.wgsl create mode 100644 crates/bevy_pbr/src/render/pbr_prepass.wgsl create mode 100644 examples/shader/shader_prepass.rs 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