From f66742dd99cf7d1048ac73557d5812da280a839f Mon Sep 17 00:00:00 2001 From: Griffin <33357138+DGriffin91@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:10:38 -0700 Subject: [PATCH] Deferred Renderer (#9258) # Objective - Add a [Deferred Renderer](https://en.wikipedia.org/wiki/Deferred_shading) to Bevy. - This allows subsequent passes to access per pixel material information before/during shading. - Accessing this per pixel material information is needed for some features, like GI. It also makes other features (ex. Decals) simpler to implement and/or improves their capability. There are multiple approaches to accomplishing this. The deferred shading approach works well given the limitations of WebGPU and WebGL2. Motivation: [I'm working on a GI solution for Bevy](https://youtu.be/eH1AkL-mwhI) # Solution - The deferred renderer is implemented with a prepass and a deferred lighting pass. - The prepass renders opaque objects into the Gbuffer attachment (`Rgba32Uint`). The PBR shader generates a `PbrInput` in mostly the same way as the forward implementation and then [packs it into the Gbuffer](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/render/pbr.wgsl#L168). - The deferred lighting pass unpacks the `PbrInput` and [feeds it into the pbr() function](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl#L65), then outputs the shaded color data. - There is now a resource [DefaultOpaqueRendererMethod](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L599) that can be used to set the default render method for opaque materials. If materials return `None` from [opaque_render_method()](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L131) the `DefaultOpaqueRendererMethod` will be used. Otherwise, custom materials can also explicitly choose to only support Deferred or Forward by returning the respective [OpaqueRendererMethod](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L603) - Deferred materials can be used seamlessly along with both opaque and transparent forward rendered materials in the same scene. The [deferred rendering example](https://github.com/DGriffin91/bevy/blob/deferred/examples/3d/deferred_rendering.rs) does this. - The deferred renderer does not support MSAA. If any deferred materials are used, MSAA must be disabled. Both TAA and FXAA are supported. - Deferred rendering supports WebGL2/WebGPU. ## Custom deferred materials - Custom materials can support both deferred and forward at the same time. The [StandardMaterial](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/render/pbr.wgsl#L166) does this. So does [this example](https://github.com/DGriffin91/bevy_glowy_orb_tutorial/blob/deferred/assets/shaders/glowy.wgsl#L56). - Custom deferred materials that require PBR lighting can create a `PbrInput`, write it to the deferred GBuffer and let it be rendered by the `PBRDeferredLightingPlugin`. - Custom deferred materials that require custom lighting have two options: 1. Use the base_color channel of the `PbrInput` combined with the `STANDARD_MATERIAL_FLAGS_UNLIT_BIT` flag. [Example.](https://github.com/DGriffin91/bevy_glowy_orb_tutorial/blob/deferred/assets/shaders/glowy.wgsl#L56) (If the unlit bit is set, the base_color is stored as RGB9E5 for extra precision) 2. A Custom Deferred Lighting pass can be created, either overriding the default, or running in addition. The a depth buffer is used to limit rendering to only the required fragments for each deferred lighting pass. Materials can set their respective depth id via the [deferred_lighting_pass_id](https://github.com/DGriffin91/bevy/blob/b79182d2a32cac28c4213c2457a53ac2cc885332/crates/bevy_pbr/src/prepass/prepass_io.wgsl#L95) attachment. The custom deferred lighting pass plugin can then set [its corresponding depth](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl#L37). Then with the lighting pass using [CompareFunction::Equal](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/mod.rs#L335), only the fragments with a depth that equal the corresponding depth written in the material will be rendered. Custom deferred lighting plugins can also be created to render the StandardMaterial. The default deferred lighting plugin can be bypassed with `DefaultPlugins.set(PBRDeferredLightingPlugin { bypass: true })` --------- Co-authored-by: nickrart --- Cargo.toml | 10 + assets/shaders/array_texture.wgsl | 4 +- .../src/core_3d/main_opaque_pass_3d_node.rs | 29 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 126 ++++- .../deferred/copy_deferred_lighting_id.wgsl | 19 + .../src/deferred/copy_lighting_id.rs | 224 +++++++++ crates/bevy_core_pipeline/src/deferred/mod.rs | 149 ++++++ .../bevy_core_pipeline/src/deferred/node.rs | 205 ++++++++ crates/bevy_core_pipeline/src/lib.rs | 3 + crates/bevy_core_pipeline/src/prepass/mod.rs | 11 +- crates/bevy_core_pipeline/src/prepass/node.rs | 59 ++- crates/bevy_core_pipeline/src/skybox/mod.rs | 11 +- crates/bevy_gizmos/src/pipeline_3d.rs | 4 +- .../src/deferred/deferred_lighting.wgsl | 95 ++++ crates/bevy_pbr/src/deferred/mod.rs | 473 ++++++++++++++++++ .../src/deferred/pbr_deferred_functions.wgsl | 129 +++++ .../src/deferred/pbr_deferred_types.wgsl | 86 ++++ crates/bevy_pbr/src/lib.rs | 42 ++ crates/bevy_pbr/src/material.rs | 143 +++++- crates/bevy_pbr/src/pbr_material.rs | 27 +- crates/bevy_pbr/src/prepass/mod.rs | 345 ++++++++++--- crates/bevy_pbr/src/prepass/prepass.wgsl | 118 ++--- .../src/prepass/prepass_bindings.wgsl | 5 - crates/bevy_pbr/src/prepass/prepass_io.wgsl | 114 +++++ crates/bevy_pbr/src/render/light.rs | 11 +- crates/bevy_pbr/src/render/mesh.rs | 78 +-- .../src/render/mesh_view_bindings.wgsl | 1 + crates/bevy_pbr/src/render/pbr.wgsl | 75 ++- crates/bevy_pbr/src/render/pbr_functions.wgsl | 41 +- crates/bevy_pbr/src/render/pbr_prepass.wgsl | 107 +--- .../src/render/pbr_prepass_functions.wgsl | 57 +++ crates/bevy_pbr/src/render/pbr_types.wgsl | 48 +- crates/bevy_pbr/src/render/rgb9e5.wgsl | 63 +++ crates/bevy_pbr/src/render/utils.wgsl | 21 + crates/bevy_pbr/src/ssao/mod.rs | 5 +- .../bevy_render/src/texture/fallback_image.rs | 73 +-- crates/bevy_render/src/texture/mod.rs | 3 +- examples/3d/deferred_rendering.rs | 428 ++++++++++++++++ examples/README.md | 1 + tests/3d/no_prepass.rs | 1 + 40 files changed, 2959 insertions(+), 485 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/deferred/copy_deferred_lighting_id.wgsl create mode 100644 crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs create mode 100644 crates/bevy_core_pipeline/src/deferred/mod.rs create mode 100644 crates/bevy_core_pipeline/src/deferred/node.rs create mode 100644 crates/bevy_pbr/src/deferred/deferred_lighting.wgsl create mode 100644 crates/bevy_pbr/src/deferred/mod.rs create mode 100644 crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl create mode 100644 crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl create mode 100644 crates/bevy_pbr/src/prepass/prepass_io.wgsl create mode 100644 crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl create mode 100644 crates/bevy_pbr/src/render/rgb9e5.wgsl create mode 100644 examples/3d/deferred_rendering.rs diff --git a/Cargo.toml b/Cargo.toml index 6bafce7f4f80ef..053d15abeef9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -635,6 +635,16 @@ description = "Illustrates bloom configuration using HDR and emissive materials" category = "3D Rendering" wasm = true +[[example]] +name = "deferred_rendering" +path = "examples/3d/deferred_rendering.rs" + +[package.metadata.example.deferred_rendering] +name = "Deferred Rendering" +description = "Renders meshes with both forward and deferred pipelines" +category = "3D Rendering" +wasm = true + [[example]] name = "load_gltf" path = "examples/3d/load_gltf.rs" diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index 828bb0689a150c..7945baf3f8a987 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -1,6 +1,6 @@ #import bevy_pbr::mesh_vertex_output MeshVertexOutput #import bevy_pbr::mesh_view_bindings view -#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT +#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, PbrInput, pbr_input_new #import bevy_core_pipeline::tonemapping tone_mapping #import bevy_pbr::pbr_functions as fns @@ -16,7 +16,7 @@ fn fragment( // Prepare a 'processed' StandardMaterial by sampling all textures to resolve // the material members - var pbr_input: fns::PbrInput = fns::pbr_input_new(); + var pbr_input: PbrInput = pbr_input_new(); pbr_input.material.base_color = textureSample(my_array_texture, my_array_texture_sampler, mesh.uv, layer); #ifdef VERTEX_COLORS diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 34d8c299c94c90..a64ed8f4c8beff 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -1,7 +1,7 @@ use crate::{ clear_color::{ClearColor, ClearColorConfig}, core_3d::{Camera3d, Opaque3d}, - prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, skybox::{SkyboxBindGroup, SkyboxPipelineId}, }; use bevy_ecs::{prelude::*, query::QueryItem}; @@ -34,6 +34,7 @@ impl ViewNode for MainOpaquePass3dNode { Option<&'static DepthPrepass>, Option<&'static NormalPrepass>, Option<&'static MotionVectorPrepass>, + Option<&'static DeferredPrepass>, Option<&'static SkyboxPipelineId>, Option<&'static SkyboxBindGroup>, &'static ViewUniformOffset, @@ -53,12 +54,24 @@ impl ViewNode for MainOpaquePass3dNode { depth_prepass, normal_prepass, motion_vector_prepass, + deferred_prepass, skybox_pipeline, skybox_bind_group, view_uniform_offset, ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { + let load = if deferred_prepass.is_none() { + match camera_3d.clear_color { + ClearColorConfig::Default => LoadOp::Clear(world.resource::().0.into()), + ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), + ClearColorConfig::None => LoadOp::Load, + } + } else { + // If the deferred lighting pass has run, don't clear again in this pass. + LoadOp::Load + }; + // Run the opaque pass, sorted front-to-back // NOTE: Scoped to drop the mutable borrow of render_context #[cfg(feature = "trace")] @@ -69,16 +82,9 @@ impl ViewNode for MainOpaquePass3dNode { label: Some("main_opaque_pass_3d"), // NOTE: The opaque pass loads the color // buffer as well as writing to it. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: match camera_3d.clear_color { - ClearColorConfig::Default => { - LoadOp::Clear(world.resource::().0.into()) - } - ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), - ClearColorConfig::None => LoadOp::Load, - }, - store: true, - }))], + color_attachments: &[Some( + target.get_color_attachment(Operations { load, store: true }), + )], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &depth.view, // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it @@ -86,6 +92,7 @@ impl ViewNode for MainOpaquePass3dNode { load: if depth_prepass.is_some() || normal_prepass.is_some() || motion_vector_prepass.is_some() + || deferred_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. diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index e30b20b8d5e49e..b0bee381cc275f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -10,6 +10,9 @@ pub mod graph { pub mod node { pub const MSAA_WRITEBACK: &str = "msaa_writeback"; pub const PREPASS: &str = "prepass"; + pub const DEFERRED_PREPASS: &str = "deferred_prepass"; + pub const COPY_DEFERRED_LIGHTING_ID: &str = "copy_deferred_lighting_id"; + pub const END_PREPASSES: &str = "end_prepasses"; pub const START_MAIN_PASS: &str = "start_main_pass"; pub const MAIN_OPAQUE_PASS: &str = "main_opaque_pass"; pub const MAIN_TRANSPARENT_PASS: &str = "main_transparent_pass"; @@ -24,13 +27,16 @@ pub mod graph { } pub const CORE_3D: &str = graph::NAME; +// PERF: vulkan docs recommend using 24 bit depth for better performance +pub const CORE_3D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + use std::{cmp::Reverse, ops::Range}; pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; pub use main_transparent_pass_3d_node::*; -use bevy_app::{App, Plugin}; +use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::prelude::*; use bevy_render::{ camera::{Camera, ExtractedCamera}, @@ -50,12 +56,17 @@ use bevy_render::{ view::ViewDepthTexture, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{nonmax::NonMaxU32, FloatOrd, HashMap}; +use bevy_utils::{nonmax::NonMaxU32, tracing::warn, FloatOrd, HashMap}; use crate::{ + deferred::{ + copy_lighting_id::CopyDeferredLightingIdNode, node::DeferredGBufferPrepassNode, + AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, + DEFERRED_PREPASS_FORMAT, + }, prepass::{ - node::PrepassNode, AlphaMask3dPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, - Opaque3dPrepass, ViewPrepassTextures, DEPTH_PREPASS_FORMAT, MOTION_VECTOR_PREPASS_FORMAT, + node::PrepassNode, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, + NormalPrepass, Opaque3dPrepass, ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, skybox::SkyboxPlugin, @@ -69,7 +80,8 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() - .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())); + .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) + .add_systems(PostUpdate, check_msaa); let render_app = match app.get_sub_app_mut(RenderApp) { Ok(render_app) => render_app, @@ -82,6 +94,8 @@ impl Plugin for Core3dPlugin { .init_resource::>() .init_resource::>() .init_resource::>() + .init_resource::>() + .init_resource::>() .add_systems(ExtractSchedule, extract_core_3d_camera_phases) .add_systems(ExtractSchedule, extract_camera_prepass_phase) .add_systems( @@ -92,6 +106,8 @@ impl Plugin for Core3dPlugin { sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), prepare_core_3d_depth_textures.in_set(RenderSet::PrepareResources), prepare_prepass_textures.in_set(RenderSet::PrepareResources), ), @@ -101,6 +117,15 @@ impl Plugin for Core3dPlugin { render_app .add_render_sub_graph(CORE_3D) .add_render_graph_node::>(CORE_3D, PREPASS) + .add_render_graph_node::>( + CORE_3D, + DEFERRED_PREPASS, + ) + .add_render_graph_node::>( + CORE_3D, + COPY_DEFERRED_LIGHTING_ID, + ) + .add_render_graph_node::(CORE_3D, END_PREPASSES) .add_render_graph_node::(CORE_3D, START_MAIN_PASS) .add_render_graph_node::>( CORE_3D, @@ -118,6 +143,9 @@ impl Plugin for Core3dPlugin { CORE_3D, &[ PREPASS, + DEFERRED_PREPASS, + COPY_DEFERRED_LIGHTING_ID, + END_PREPASSES, START_MAIN_PASS, MAIN_OPAQUE_PASS, MAIN_TRANSPARENT_PASS, @@ -341,12 +369,14 @@ pub fn extract_camera_prepass_phase( Option<&DepthPrepass>, Option<&NormalPrepass>, Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, ), With, >, >, ) { - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass) in cameras_3d.iter() + for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in + cameras_3d.iter() { if camera.is_active { let mut entity = commands.get_or_spawn(entity); @@ -361,6 +391,13 @@ pub fn extract_camera_prepass_phase( )); } + if deferred_prepass.is_some() { + entity.insert(( + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + if depth_prepass.is_some() { entity.insert(DepthPrepass); } @@ -370,6 +407,9 @@ pub fn extract_camera_prepass_phase( if motion_vector_prepass.is_some() { entity.insert(MotionVectorPrepass); } + if deferred_prepass.is_some() { + entity.insert(DeferredPrepass); + } } } } @@ -428,8 +468,7 @@ pub fn prepare_core_3d_depth_textures( 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, + format: CORE_3D_DEPTH_FORMAT, usage, view_formats: &[], }; @@ -445,6 +484,22 @@ pub fn prepare_core_3d_depth_textures( } } +// Disable MSAA and warn if using deferred rendering +pub fn check_msaa( + mut msaa: ResMut, + deferred_views: Query, With)>, +) { + if !deferred_views.is_empty() { + match *msaa { + Msaa::Off => (), + _ => { + warn!("MSAA is incompatible with deferred rendering and has been disabled."); + *msaa = Msaa::Off; + } + }; + } +} + // Prepares the textures used by the prepass pub fn prepare_prepass_textures( mut commands: Commands, @@ -458,6 +513,7 @@ pub fn prepare_prepass_textures( Option<&DepthPrepass>, Option<&NormalPrepass>, Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, ), ( With>, @@ -467,8 +523,12 @@ pub fn prepare_prepass_textures( ) { let mut depth_textures = HashMap::default(); let mut normal_textures = HashMap::default(); + let mut deferred_textures = HashMap::default(); + let mut deferred_lighting_id_textures = HashMap::default(); let mut motion_vectors_textures = HashMap::default(); - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass) in &views_3d { + for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in + &views_3d + { let Some(physical_target_size) = camera.physical_target_size else { continue; }; @@ -489,7 +549,7 @@ pub fn prepare_prepass_textures( mip_level_count: 1, sample_count: msaa.samples(), dimension: TextureDimension::D2, - format: DEPTH_PREPASS_FORMAT, + format: CORE_3D_DEPTH_FORMAT, usage: TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, @@ -544,10 +604,56 @@ pub fn prepare_prepass_textures( .clone() }); + let cached_deferred_texture = deferred_prepass.is_some().then(|| { + deferred_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_deferred_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) + }) + .clone() + }); + + let deferred_lighting_pass_id_texture = deferred_prepass.is_some().then(|| { + deferred_lighting_id_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("deferred_lighting_pass_id_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_LIGHTING_PASS_ID_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) + }) + .clone() + }); + commands.entity(entity).insert(ViewPrepassTextures { depth: cached_depth_texture, normal: cached_normals_texture, motion_vectors: cached_motion_vectors_texture, + deferred: cached_deferred_texture, + deferred_lighting_pass_id: deferred_lighting_pass_id_texture, size, }); } diff --git a/crates/bevy_core_pipeline/src/deferred/copy_deferred_lighting_id.wgsl b/crates/bevy_core_pipeline/src/deferred/copy_deferred_lighting_id.wgsl new file mode 100644 index 00000000000000..2fc7b1d86748dd --- /dev/null +++ b/crates/bevy_core_pipeline/src/deferred/copy_deferred_lighting_id.wgsl @@ -0,0 +1,19 @@ +#import bevy_pbr::utils +#import bevy_core_pipeline::fullscreen_vertex_shader FullscreenVertexOutput + +@group(0) @binding(0) +var material_id_texture: texture_2d; + +struct FragmentOutput { + @builtin(frag_depth) frag_depth: f32, + +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> FragmentOutput { + var out: FragmentOutput; + // Depth is stored as unorm, so we are dividing the u8 by 255.0 here. + out.frag_depth = f32(textureLoad(material_id_texture, vec2(in.position.xy), 0).x) / 255.0; + return out; +} + diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs new file mode 100644 index 00000000000000..cb68e472035cb2 --- /dev/null +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -0,0 +1,224 @@ +use crate::{ + fullscreen_vertex_shader::fullscreen_shader_vertex_state, + prepass::{DeferredPrepass, ViewPrepassTextures}, +}; +use bevy_app::prelude::*; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_ecs::prelude::*; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::*, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, + view::ViewTarget, + Render, RenderApp, RenderSet, +}; + +use bevy_ecs::query::QueryItem; +use bevy_render::{ + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + BindGroupDescriptor, BindGroupEntry, BindingResource, Operations, PipelineCache, + RenderPassDescriptor, + }, + renderer::RenderContext, +}; + +use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; + +pub const COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE: Handle = + Handle::weak_from_u128(5230948520734987); +pub struct CopyDeferredLightingIdPlugin; + +impl Plugin for CopyDeferredLightingIdPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, + "copy_deferred_lighting_id.wgsl", + Shader::from_wgsl + ); + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.add_systems( + Render, + (prepare_deferred_lighting_id_textures.in_set(RenderSet::PrepareResources),), + ); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +#[derive(Default)] +pub struct CopyDeferredLightingIdNode; +impl CopyDeferredLightingIdNode { + pub const NAME: &str = "copy_deferred_lighting_id"; +} + +impl ViewNode for CopyDeferredLightingIdNode { + type ViewQuery = ( + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static DeferredLightingIdDepthTexture, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (_view_target, view_prepass_textures, deferred_lighting_id_depth_texture): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let copy_deferred_lighting_id_pipeline = world.resource::(); + + let pipeline_cache = world.resource::(); + + let Some(pipeline) = + pipeline_cache.get_render_pipeline(copy_deferred_lighting_id_pipeline.pipeline_id) + else { + return Ok(()); + }; + let Some(deferred_lighting_pass_id_texture) = + &view_prepass_textures.deferred_lighting_pass_id + else { + return Ok(()); + }; + + let bind_group = render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: Some("copy_deferred_lighting_id_bind_group"), + layout: ©_deferred_lighting_id_pipeline.layout, + entries: &[BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &deferred_lighting_pass_id_texture.default_view, + ), + }], + }); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("copy_deferred_lighting_id_pass"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &deferred_lighting_id_depth_texture.texture.default_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: true, + }), + stencil_ops: None, + }), + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +#[derive(Resource)] +struct CopyDeferredLightingIdPipeline { + layout: BindGroupLayout, + pipeline_id: CachedRenderPipelineId, +} + +impl FromWorld for CopyDeferredLightingIdPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("copy_deferred_lighting_id_bind_group_layout"), + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Uint, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }], + }); + + let pipeline_id = + world + .resource_mut::() + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some("copy_deferred_lighting_id_pipeline".into()), + layout: vec![layout.clone()], + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![], + }), + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + push_constant_ranges: vec![], + }); + + Self { + layout, + pipeline_id, + } + } +} + +#[derive(Component)] +pub struct DeferredLightingIdDepthTexture { + pub texture: CachedTexture, +} + +fn prepare_deferred_lighting_id_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + views: Query<(Entity, &ExtractedCamera), With>, +) { + for (entity, camera) in &views { + if let Some(UVec2 { + x: width, + y: height, + }) = camera.physical_target_size + { + let texture_descriptor = TextureDescriptor { + label: Some("deferred_lighting_id_depth_texture_a"), + size: Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC, + view_formats: &[], + }; + let texture = texture_cache.get(&render_device, texture_descriptor); + commands + .entity(entity) + .insert(DeferredLightingIdDepthTexture { texture }); + } + } +} diff --git a/crates/bevy_core_pipeline/src/deferred/mod.rs b/crates/bevy_core_pipeline/src/deferred/mod.rs new file mode 100644 index 00000000000000..a79e62b71998a8 --- /dev/null +++ b/crates/bevy_core_pipeline/src/deferred/mod.rs @@ -0,0 +1,149 @@ +pub mod copy_lighting_id; +pub mod node; + +use std::{cmp::Reverse, ops::Range}; + +use bevy_ecs::prelude::*; +use bevy_render::{ + render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, + render_resource::{CachedRenderPipelineId, TextureFormat}, +}; +use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; + +pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint; +pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint; +pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + +/// Opaque phase of the 3D Deferred pass. +/// +/// 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 Opaque3dDeferred { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, +} + +impl PhaseItem for Opaque3dDeferred { + // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort. + type SortKey = Reverse; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + Reverse(FloatOrd(self.distance)) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + // Key negated to match reversed SortKey ordering + radsort::sort_by_key(items, |item| -item.distance); + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} + +/// Alpha mask phase of the 3D Deferred pass. +/// +/// 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 AlphaMask3dDeferred { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, +} + +impl PhaseItem for AlphaMask3dDeferred { + // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort. + type SortKey = Reverse; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + Reverse(FloatOrd(self.distance)) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + // Key negated to match reversed SortKey ordering + radsort::sort_by_key(items, |item| -item.distance); + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs new file mode 100644 index 00000000000000..f598e8393ea709 --- /dev/null +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -0,0 +1,205 @@ +use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryItem; +use bevy_render::render_graph::ViewNode; + +use bevy_render::{ + camera::ExtractedCamera, + prelude::Color, + render_graph::{NodeRunError, RenderGraphContext}, + render_phase::RenderPhase, + render_resource::{ + LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, + }, + renderer::RenderContext, + view::ViewDepthTexture, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use crate::core_3d::{Camera3d, Camera3dDepthLoadOp}; +use crate::prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass, ViewPrepassTextures}; + +use super::{AlphaMask3dDeferred, Opaque3dDeferred}; + +/// Render node used by the prepass. +/// +/// By default, inserted before the main pass in the render graph. +#[derive(Default)] +pub struct DeferredGBufferPrepassNode; + +impl ViewNode for DeferredGBufferPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static RenderPhase, + &'static RenderPhase, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + &'static Camera3d, + Option<&'static DepthPrepass>, + Option<&'static NormalPrepass>, + Option<&'static MotionVectorPrepass>, + ); + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + opaque_deferred_phase, + alpha_mask_deferred_phase, + view_depth_texture, + view_prepass_textures, + camera_3d, + depth_prepass, + normal_prepass, + motion_vector_prepass, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + + let mut color_attachments = vec![]; + color_attachments.push( + view_prepass_textures + .normal + .as_ref() + .map(|view_normals_texture| RenderPassColorAttachment { + view: &view_normals_texture.default_view, + resolve_target: None, + ops: Operations { + load: if normal_prepass.is_some() { + // Load if the normal_prepass has already run. + // The prepass will have already cleared this for the current frame. + LoadOp::Load + } else { + LoadOp::Clear(Color::BLACK.into()) + }, + store: true, + }, + }), + ); + color_attachments.push(view_prepass_textures.motion_vectors.as_ref().map( + |view_motion_vectors_texture| RenderPassColorAttachment { + view: &view_motion_vectors_texture.default_view, + resolve_target: None, + ops: Operations { + load: if motion_vector_prepass.is_some() { + // Load if the motion_vector_prepass has already run. + // The prepass will have already cleared this for the current frame. + LoadOp::Load + } else { + LoadOp::Clear(Color::BLACK.into()) + }, + store: true, + }, + }, + )); + + // If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors: + // Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format. + // Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT. + // Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9 + // For webgl2 we fallback to manually clearing + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + if let Some(deferred_texture) = &view_prepass_textures.deferred { + render_context.command_encoder().clear_texture( + &deferred_texture.texture, + &bevy_render::render_resource::ImageSubresourceRange::default(), + ); + } + + color_attachments.push( + view_prepass_textures + .deferred + .as_ref() + .map(|deferred_texture| RenderPassColorAttachment { + view: &deferred_texture.default_view, + resolve_target: None, + ops: Operations { + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + load: LoadOp::Load, + #[cfg(not(all(feature = "webgl", target_arch = "wasm32")))] + load: LoadOp::Clear(Default::default()), + store: true, + }, + }), + ); + + color_attachments.push( + view_prepass_textures + .deferred_lighting_pass_id + .as_ref() + .map(|deferred_lighting_pass_id| RenderPassColorAttachment { + view: &deferred_lighting_pass_id.default_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Default::default()), + store: true, + }, + }), + ); + + if color_attachments.iter().all(Option::is_none) { + // All attachments are none: clear the attachment list so that no fragment shader is required. + color_attachments.clear(); + } + + { + // 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("deferred"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &view_depth_texture.view, + depth_ops: Some(Operations { + load: if depth_prepass.is_some() + || normal_prepass.is_some() + || motion_vector_prepass.is_some() + { + // If any prepass runs, it will generate a depth buffer so we should use it. + 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, + }), + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Always run deferred pass to ensure the deferred gbuffer and deferred_lighting_pass_id are cleared. + { + // Run the prepass, sorted front-to-back. + #[cfg(feature = "trace")] + let _opaque_prepass_span = info_span!("opaque_deferred").entered(); + opaque_deferred_phase.render(&mut render_pass, world, view_entity); + } + + if !alpha_mask_deferred_phase.items.is_empty() { + // Run the deferred, sorted front-to-back. + #[cfg(feature = "trace")] + let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred").entered(); + alpha_mask_deferred_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_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index afe06ff08eadc4..b1e14ec80fd21f 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -4,6 +4,7 @@ pub mod clear_color; pub mod contrast_adaptive_sharpening; pub mod core_2d; pub mod core_3d; +pub mod deferred; pub mod fullscreen_vertex_shader; pub mod fxaa; pub mod msaa_writeback; @@ -38,6 +39,7 @@ use crate::{ contrast_adaptive_sharpening::CASPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, + deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, msaa_writeback::MsaaWritebackPlugin, @@ -70,6 +72,7 @@ impl Plugin for CorePipelinePlugin { ExtractResourcePlugin::::default(), Core2dPlugin, Core3dPlugin, + CopyDeferredLightingIdPlugin, BlitPlugin, MsaaWritebackPlugin, TonemappingPlugin, diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index f408a168e7c7c4..ba4de1952c82ea 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -38,7 +38,6 @@ use bevy_render::{ }; use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; -pub const DEPTH_PREPASS_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float; @@ -55,6 +54,10 @@ pub struct NormalPrepass; #[derive(Component, Default, Reflect)] pub struct MotionVectorPrepass; +/// If added to a [`crate::prelude::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes. +#[derive(Component, Default, Reflect)] +pub struct DeferredPrepass; + /// Textures that are written to by the prepass. /// /// This component will only be present if any of the relevant prepass components are also present. @@ -69,6 +72,12 @@ pub struct ViewPrepassTextures { /// The motion vectors texture generated by the prepass. /// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget` pub motion_vectors: Option, + /// The deferred gbuffer generated by the deferred pass. + /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` + pub deferred: Option, + /// A texture that specifies the deferred lighting pass id for a material. + /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` + pub deferred_lighting_pass_id: Option, /// The size of the textures. pub size: Extent3d, } diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index bdf997601372e4..5b64ad7bf42846 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -16,7 +16,7 @@ use bevy_render::{ #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -use super::{AlphaMask3dPrepass, Opaque3dPrepass, ViewPrepassTextures}; +use super::{AlphaMask3dPrepass, DeferredPrepass, Opaque3dPrepass, ViewPrepassTextures}; /// Render node used by the prepass. /// @@ -31,6 +31,7 @@ impl ViewNode for PrepassNode { &'static RenderPhase, &'static ViewDepthTexture, &'static ViewPrepassTextures, + Option<&'static DeferredPrepass>, ); fn run( @@ -43,13 +44,13 @@ impl ViewNode for PrepassNode { alpha_mask_prepass_phase, view_depth_texture, view_prepass_textures, + deferred_prepass, ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); - let mut color_attachments = vec![]; - color_attachments.push( + let mut color_attachments = vec![ view_prepass_textures .normal .as_ref() @@ -61,20 +62,25 @@ impl ViewNode for PrepassNode { store: true, }, }), - ); - color_attachments.push(view_prepass_textures.motion_vectors.as_ref().map( - |view_motion_vectors_texture| RenderPassColorAttachment { - view: &view_motion_vectors_texture.default_view, - resolve_target: None, - ops: Operations { - // Red and Green channels are X and Y components of the motion vectors - // Blue channel doesn't matter, but set to 0.0 for possible faster clear - // https://gpuopen.com/performance/#clears - load: LoadOp::Clear(Color::rgb_linear(0.0, 0.0, 0.0).into()), - store: true, - }, - }, - )); + view_prepass_textures + .motion_vectors + .as_ref() + .map(|view_motion_vectors_texture| RenderPassColorAttachment { + view: &view_motion_vectors_texture.default_view, + resolve_target: None, + ops: Operations { + // Red and Green channels are X and Y components of the motion vectors + // Blue channel doesn't matter, but set to 0.0 for possible faster clear + // https://gpuopen.com/performance/#clears + load: LoadOp::Clear(Color::BLACK.into()), + store: true, + }, + }), + // Use None in place of Deferred attachments + None, + None, + ]; + if color_attachments.iter().all(Option::is_none) { // all attachments are none: clear the attachment list so that no fragment shader is required color_attachments.clear(); @@ -113,16 +119,17 @@ impl ViewNode for PrepassNode { 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, - ); + if deferred_prepass.is_none() { + // Copy if deferred isn't going to + 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_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index bd2035d6b1b357..efc133651930e2 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -11,7 +11,7 @@ use bevy_render::{ render_asset::RenderAssets, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, - BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, + BindGroupLayoutEntry, BindingResource, BindingType, BufferBindingType, CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType, @@ -24,6 +24,8 @@ use bevy_render::{ Render, RenderApp, RenderSet, }; +use crate::core_3d::CORE_3D_DEPTH_FORMAT; + const SKYBOX_SHADER_HANDLE: Handle = Handle::weak_from_u128(55594763423201); pub struct SkyboxPlugin; @@ -121,6 +123,7 @@ impl SkyboxPipeline { struct SkyboxPipelineKey { hdr: bool, samples: u32, + depth_format: TextureFormat, } impl SpecializedRenderPipeline for SkyboxPipeline { @@ -139,7 +142,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline { }, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, + format: key.depth_format, depth_write_enabled: false, depth_compare: CompareFunction::GreaterEqual, stencil: StencilState { @@ -169,7 +172,8 @@ impl SpecializedRenderPipeline for SkyboxPipeline { } else { TextureFormat::bevy_default() }, - blend: Some(BlendState::REPLACE), + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases. + blend: None, write_mask: ColorWrites::ALL, })], }), @@ -195,6 +199,7 @@ fn prepare_skybox_pipelines( SkyboxPipelineKey { hdr: view.hdr, samples: msaa.samples(), + depth_format: CORE_3D_DEPTH_FORMAT, }, ); diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index c9a465f5955493..e63125a0fbb2f0 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; -use bevy_core_pipeline::core_3d::Transparent3d; +use bevy_core_pipeline::core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, @@ -121,7 +121,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { layout, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, + format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, depth_compare: CompareFunction::Greater, stencil: StencilState::default(), diff --git a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl new file mode 100644 index 00000000000000..a8b046e5b4159b --- /dev/null +++ b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -0,0 +1,95 @@ +#import bevy_pbr::prepass_utils +#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT +#import bevy_pbr::pbr_functions as pbr_functions +#import bevy_pbr::pbr_deferred_types as deferred_types +#import bevy_pbr::pbr_deferred_functions pbr_input_from_deferred_gbuffer, unpack_unorm3x4_plus_unorm_20_ +#import bevy_pbr::mesh_view_types FOG_MODE_OFF + +#import bevy_pbr::mesh_view_bindings deferred_prepass_texture, fog, view, screen_space_ambient_occlusion_texture +#import bevy_core_pipeline::tonemapping screen_space_dither, powsafe, tone_mapping + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION +#import bevy_pbr::gtao_utils gtao_multibounce +#endif + +struct FullscreenVertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, +}; + +struct PbrDeferredLightingDepthId { + depth_id: u32, // limited to u8 +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _webgl2_padding_0: f32, + _webgl2_padding_1: f32, + _webgl2_padding_2: f32, +#endif +} +@group(1) @binding(0) +var depth_id: PbrDeferredLightingDepthId; + +@vertex +fn vertex(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput { + // See the full screen vertex shader for explanation above for how this works. + let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; + // Depth is stored as unorm, so we are dividing the u8 depth_id by 255.0 here. + let clip_position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), f32(depth_id.depth_id) / 255.0, 1.0); + + return FullscreenVertexOutput(clip_position, uv); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + var frag_coord = vec4(in.position.xy, 0.0, 0.0); + + let deferred_data = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + +#ifdef WEBGL2 + frag_coord.z = deferred_types::unpack_unorm3x4_plus_unorm_20_(deferred_data.b).w; +#else + frag_coord.z = bevy_pbr::prepass_utils::prepass_depth(in.position, 0u); +#endif + + var pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, deferred_data); + var output_color = vec4(0.0); + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION + let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; + let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); + pbr_input.occlusion = min(pbr_input.occlusion, ssao_multibounce); +#endif // SCREEN_SPACE_AMBIENT_OCCLUSION + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { + output_color = pbr_functions::pbr(pbr_input); + } else { + output_color = pbr_input.material.base_color; + } + + // fog + if (fog.mode != FOG_MODE_OFF && (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) { + output_color = pbr_functions::apply_fog(fog, output_color, pbr_input.world_position.xyz, view.world_position.xyz); + } + +#ifdef TONEMAP_IN_SHADER + output_color = tone_mapping(output_color, view.color_grading); +#ifdef DEBAND_DITHER + var output_rgb = output_color.rgb; + output_rgb = powsafe(output_rgb, 1.0 / 2.2); + output_rgb = output_rgb + screen_space_dither(frag_coord.xy); + // This conversion back to linear space is required because our output texture format is + // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. + output_rgb = powsafe(output_rgb, 2.2); + output_color = vec4(output_rgb, output_color.a); +#endif +#endif +#ifdef PREMULTIPLY_ALPHA + output_color = pbr_functions::premultiply_alpha(material.flags, output_color); +#endif + + return output_color; +} + diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs new file mode 100644 index 00000000000000..564c973eef460a --- /dev/null +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -0,0 +1,473 @@ +use crate::{MeshPipeline, MeshViewBindGroup, ScreenSpaceAmbientOcclusionSettings}; +use bevy_app::prelude::*; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_core_pipeline::{ + clear_color::ClearColorConfig, + core_3d, + deferred::{ + copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + }, + prelude::{Camera3d, ClearColor}, + prepass::DeferredPrepass, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::{ + extract_component::{ + ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, + }, + render_asset::RenderAssets, + render_graph::{NodeRunError, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_resource::{self, Operations, PipelineCache, RenderPassDescriptor}, + renderer::{RenderContext, RenderDevice}, + texture::Image, + view::{ViewTarget, ViewUniformOffset}, + Render, RenderSet, +}; + +use bevy_render::{ + render_graph::RenderGraphApp, render_resource::*, texture::BevyDefault, view::ExtractedView, + RenderApp, +}; + +use crate::{ + EnvironmentMapLight, MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, + ViewLightsUniformOffset, +}; + +pub struct DeferredPbrLightingPlugin; + +pub const DEFERRED_LIGHTING_SHADER_HANDLE: Handle = + Handle::weak_from_u128(2708011359337029741); + +pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1; + +/// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass. +/// Will be automatically added to entities with the [`DeferredPrepass`] component that don't already have a [`PbrDeferredLightingDepthId`]. +#[derive(Component, Clone, Copy, ExtractComponent, ShaderType)] +pub struct PbrDeferredLightingDepthId { + depth_id: u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_0: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_1: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_2: f32, +} + +impl PbrDeferredLightingDepthId { + pub fn new(value: u8) -> PbrDeferredLightingDepthId { + PbrDeferredLightingDepthId { + depth_id: value as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_2: 0.0, + } + } + + pub fn set(&mut self, value: u8) { + self.depth_id = value as u32; + } + + pub fn get(&self) -> u8 { + self.depth_id as u8 + } +} + +impl Default for PbrDeferredLightingDepthId { + fn default() -> Self { + PbrDeferredLightingDepthId { + depth_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + _webgl2_padding_2: 0.0, + } + } +} + +impl Plugin for DeferredPbrLightingPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )) + .add_systems(PostUpdate, insert_deferred_lighting_pass_id_component); + + load_internal_asset!( + app, + DEFERRED_LIGHTING_SHADER_HANDLE, + "deferred_lighting.wgsl", + Shader::from_wgsl + ); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .add_systems( + Render, + (prepare_deferred_lighting_pipelines.in_set(RenderSet::Prepare),), + ) + .add_render_graph_node::>( + core_3d::graph::NAME, + DEFERRED_LIGHTING_PASS, + ) + .add_render_graph_edges( + core_3d::graph::NAME, + &[ + core_3d::graph::node::START_MAIN_PASS, + DEFERRED_LIGHTING_PASS, + core_3d::graph::node::MAIN_OPAQUE_PASS, + ], + ); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +pub const DEFERRED_LIGHTING_PASS: &str = "deferred_opaque_pbr_lighting_pass_3d"; +#[derive(Default)] +pub struct DeferredOpaquePass3dPbrLightingNode; + +impl ViewNode for DeferredOpaquePass3dPbrLightingNode { + type ViewQuery = ( + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static MeshViewBindGroup, + &'static ViewTarget, + &'static DeferredLightingIdDepthTexture, + &'static Camera3d, + &'static DeferredLightingPipeline, + ); + + fn run( + &self, + _graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + view_uniform_offset, + view_lights_offset, + view_fog_offset, + mesh_view_bind_group, + target, + deferred_lighting_id_depth_texture, + camera_3d, + deferred_lighting_pipeline, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let deferred_lighting_layout = world.resource::(); + + let Some(pipeline) = + pipeline_cache.get_render_pipeline(deferred_lighting_pipeline.pipeline_id) + else { + return Ok(()); + }; + + let deferred_lighting_pass_id = + world.resource::>(); + let Some(deferred_lighting_pass_id_binding) = + deferred_lighting_pass_id.uniforms().binding() + else { + return Ok(()); + }; + + let bind_group_1 = render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: Some("deferred_lighting_layout_group_1"), + layout: &deferred_lighting_layout.bind_group_layout_1, + entries: &[BindGroupEntry { + binding: 0, + resource: deferred_lighting_pass_id_binding.clone(), + }], + }); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("deferred_lighting_pass"), + color_attachments: &[Some(target.get_color_attachment(Operations { + load: match camera_3d.clear_color { + ClearColorConfig::Default => { + LoadOp::Clear(world.resource::().0.into()) + } + ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), + ClearColorConfig::None => LoadOp::Load, + }, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &deferred_lighting_id_depth_texture.texture.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: false, + }), + stencil_ops: None, + }), + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.value, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + ], + ); + render_pass.set_bind_group(1, &bind_group_1, &[]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +#[derive(Resource)] +pub struct DeferredLightingLayout { + bind_group_layout_0: BindGroupLayout, + bind_group_layout_1: BindGroupLayout, +} + +#[derive(Component)] +pub struct DeferredLightingPipeline { + pub pipeline_id: CachedRenderPipelineId, +} + +impl SpecializedRenderPipeline for DeferredLightingLayout { + type Key = MeshPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + shader_defs.push("WEBGL2".into()); + + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED ".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) { + shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into()); + } + + if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + let shadow_filter_method = + key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); + if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { + shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13 { + shader_defs.push("SHADOW_FILTER_METHOD_CASTANO_13".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14 { + shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into()); + } + + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); + + RenderPipelineDescriptor { + label: Some("deferred_lighting_pipeline".into()), + layout: vec![ + self.bind_group_layout_0.clone(), + self.bind_group_layout_1.clone(), + ], + vertex: VertexState { + shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: "vertex".into(), + buffers: Vec::new(), + }, + fragment: Some(FragmentState { + shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + 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::default(), + push_constant_ranges: vec![], + } + } +} + +impl FromWorld for DeferredLightingLayout { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("deferred_lighting_layout"), + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX_FRAGMENT, + ty: BindingType::Buffer { + ty: render_resource::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(PbrDeferredLightingDepthId::min_size()), + }, + count: None, + }], + }); + Self { + bind_group_layout_0: world.resource::().view_layout.clone(), + bind_group_layout_1: layout, + } + } +} + +pub fn insert_deferred_lighting_pass_id_component( + mut commands: Commands, + views: Query, Without)>, +) { + for entity in views.iter() { + commands + .entity(entity) + .insert(PbrDeferredLightingDepthId::default()); + } +} + +pub fn prepare_deferred_lighting_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + deferred_lighting_layout: Res, + views: Query< + ( + Entity, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&EnvironmentMapLight>, + Option<&ShadowFilteringMethod>, + Option<&ScreenSpaceAmbientOcclusionSettings>, + ), + With, + >, + images: Res>, +) { + for (entity, view, tonemapping, dither, environment_map, shadow_filter_method, ssao) in &views { + let mut view_key = MeshPipelineKey::from_hdr(view.hdr); + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao.is_some() { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + + let environment_map_loaded = match environment_map { + Some(environment_map) => environment_map.is_loaded(&images), + None => false, + }; + if environment_map_loaded { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Castano13 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + } + ShadowFilteringMethod::Jimenez14 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + } + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &deferred_lighting_layout, view_key); + + commands + .entity(entity) + .insert(DeferredLightingPipeline { pipeline_id }); + } +} diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl new file mode 100644 index 00000000000000..c98d5c19a7f7b9 --- /dev/null +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -0,0 +1,129 @@ +#define_import_path bevy_pbr::pbr_deferred_functions +#import bevy_pbr::pbr_types PbrInput, standard_material_new, STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT +#import bevy_pbr::pbr_deferred_types as deferred_types +#import bevy_pbr::pbr_functions as pbr_functions +#import bevy_pbr::rgb9e5 as rgb9e5 +#import bevy_pbr::mesh_view_bindings as view_bindings +#import bevy_pbr::mesh_view_bindings view +#import bevy_pbr::utils octahedral_encode, octahedral_decode + +// --------------------------- +// from https://github.com/DGriffin91/bevy_coordinate_systems/blob/main/src/transformations.wgsl +// --------------------------- + +/// Convert a ndc space position to world space +fn position_ndc_to_world(ndc_pos: vec3) -> vec3 { + let world_pos = view.inverse_view_proj * vec4(ndc_pos, 1.0); + return world_pos.xyz / world_pos.w; +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// Returns the (0.0, 0.0) .. (1.0, 1.0) position within the viewport for the current render target. +/// [0 .. render target viewport size] eg. [(0.0, 0.0) .. (1280.0, 720.0)] to [(0.0, 0.0) .. (1.0, 1.0)] +fn frag_coord_to_uv(frag_coord: vec2) -> vec2 { + return (frag_coord - view.viewport.xy) / view.viewport.zw; +} + +/// Convert frag coord to ndc. +fn frag_coord_to_ndc(frag_coord: vec4) -> vec3 { + return vec3(uv_to_ndc(frag_coord_to_uv(frag_coord.xy)), frag_coord.z); +} + +// Creates the deferred gbuffer from a PbrInput. +fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { + // Only monochrome occlusion supported. May not be worth including at all. + // Some models have baked occlusion, GLTF only supports monochrome. + // Real time occlusion is applied in the deferred lighting pass. + // Deriving luminance via Rec. 709. coefficients + // https://en.wikipedia.org/wiki/Rec._709 + let occlusion = dot(in.occlusion, vec3(0.2126, 0.7152, 0.0722)); +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( + in.material.reflectance, + in.material.metallic, + occlusion, + in.frag_coord.z)); +#else + var props = deferred_types::pack_unorm4x8_(vec4( + in.material.reflectance, // could be fewer bits + in.material.metallic, // could be fewer bits + occlusion, // is this worth including? + 0.0)); // spare +#endif // WEBGL2 + let flags = deferred_types::deferred_flags_from_mesh_material_flags(in.flags, in.material.flags); + let octahedral_normal = octahedral_encode(normalize(in.N)); + var base_color_srgb = vec3(0.0); + var emissive = in.material.emissive.rgb; + if ((in.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + // Material is unlit, use emissive component of gbuffer for color data. + // Unlit materials are effectively emissive. + emissive = in.material.base_color.rgb; + } else { + base_color_srgb = pow(in.material.base_color.rgb, vec3(1.0 / 2.2)); + } + let deferred = vec4( + deferred_types::pack_unorm4x8_(vec4(base_color_srgb, in.material.perceptual_roughness)), + rgb9e5::vec3_to_rgb9e5_(emissive), + props, + deferred_types::pack_24bit_normal_and_flags(octahedral_normal, flags), + ); + return deferred; +} + +// Creates a PbrInput from the deferred gbuffer. +fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> PbrInput { + var pbr: PbrInput; + pbr.material = standard_material_new(); + + let flags = deferred_types::unpack_flags(gbuffer.a); + let deferred_flags = deferred_types::mesh_material_flags_from_deferred_flags(flags); + pbr.flags = deferred_flags.x; + pbr.material.flags = deferred_flags.y; + + let base_rough = deferred_types::unpack_unorm4x8_(gbuffer.r); + pbr.material.perceptual_roughness = base_rough.a; + let emissive = rgb9e5::rgb9e5_to_vec3_(gbuffer.g); + if ((pbr.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + pbr.material.base_color = vec4(emissive, 1.0); + pbr.material.emissive = vec4(vec3(0.0), 1.0); + } else { + pbr.material.base_color = vec4(pow(base_rough.rgb, vec3(2.2)), 1.0); + pbr.material.emissive = vec4(emissive, 1.0); + } +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); + // Bias to 0.5 since that's the value for almost all materials. + pbr.material.reflectance = saturate(props.r - 0.03333333333); +#else + let props = deferred_types::unpack_unorm4x8_(gbuffer.b); + pbr.material.reflectance = props.r; +#endif // WEBGL2 + pbr.material.metallic = props.g; + pbr.occlusion = vec3(props.b); + let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a); + let N = octahedral_decode(octahedral_normal); + + let world_position = vec4(position_ndc_to_world(frag_coord_to_ndc(frag_coord)), 1.0); + let is_orthographic = view.projection[3].w == 1.0; + let V = pbr_functions::calculate_view(world_position, is_orthographic); + + pbr.frag_coord = frag_coord; + pbr.world_normal = N; + pbr.world_position = world_position; + pbr.N = N; + pbr.V = V; + pbr.is_orthographic = is_orthographic; + + return pbr; +} + + diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl new file mode 100644 index 00000000000000..c9aa026d70d517 --- /dev/null +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl @@ -0,0 +1,86 @@ +#define_import_path bevy_pbr::pbr_deferred_types +#import bevy_pbr::mesh_types MESH_FLAGS_SHADOW_RECEIVER_BIT +#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT + +// Maximum of 8 bits available +const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u; +const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 2u; +const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 4u; + +fn deferred_flags_from_mesh_material_flags(mesh_flags: u32, mat_flags: u32) -> u32 { + var flags = 0u; + flags |= u32((mesh_flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) * DEFERRED_FLAGS_FOG_ENABLED_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) * DEFERRED_FLAGS_UNLIT_BIT; + return flags; +} + +fn mesh_material_flags_from_deferred_flags(deferred_flags: u32) -> vec2 { + var mat_flags = 0u; + var mesh_flags = 0u; + mesh_flags |= u32((deferred_flags & DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * MESH_FLAGS_SHADOW_RECEIVER_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_FOG_ENABLED_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_UNLIT_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_UNLIT_BIT; + return vec2(mesh_flags, mat_flags); +} + +const U12MAXF = 4095.0; +const U16MAXF = 65535.0; +const U20MAXF = 1048575.0; + +// Storing normals as oct24. +// Flags are stored in the remaining 8 bits. +// https://jcgt.org/published/0003/02/01/paper.pdf +// Could possibly go down to oct20 if the space is needed. + +fn pack_24bit_normal_and_flags(octahedral_normal: vec2, flags: u32) -> u32 { + let unorm1 = u32(saturate(octahedral_normal.x) * U12MAXF + 0.5); + let unorm2 = u32(saturate(octahedral_normal.y) * U12MAXF + 0.5); + return (unorm1 & 0xFFFu) | ((unorm2 & 0xFFFu) << 12u) | ((flags & 0xFFu) << 24u); +} + +fn unpack_24bit_normal(packed: u32) -> vec2 { + let unorm1 = packed & 0xFFFu; + let unorm2 = (packed >> 12u) & 0xFFFu; + return vec2(f32(unorm1) / U12MAXF, f32(unorm2) / U12MAXF); +} + +fn unpack_flags(packed: u32) -> u32 { + return (packed >> 24u) & 0xFFu; +} + +// The builtin one didn't work in webgl. +// "'unpackUnorm4x8' : no matching overloaded function found" +// https://github.com/gfx-rs/naga/issues/2006 +fn unpack_unorm4x8_(v: u32) -> vec4 { + return vec4( + f32(v & 0xFFu), + f32((v >> 8u) & 0xFFu), + f32((v >> 16u) & 0xFFu), + f32((v >> 24u) & 0xFFu) + ) / 255.0; +} + +// 'packUnorm4x8' : no matching overloaded function found +// https://github.com/gfx-rs/naga/issues/2006 +fn pack_unorm4x8_(values: vec4) -> u32 { + let v = vec4(saturate(values) * 255.0 + 0.5); + return (v.w << 24u) | (v.z << 16u) | (v.y << 8u) | v.x; +} + +// Pack 3x 4bit unorm + 1x 20bit +fn pack_unorm3x4_plus_unorm_20_(v: vec4) -> u32 { + let sm = vec3(saturate(v.xyz) * 15.0 + 0.5); + let bg = u32(saturate(v.w) * U20MAXF + 0.5); + return (bg << 12u) | (sm.z << 8u) | (sm.y << 4u) | sm.x; +} + +// Unpack 3x 4bit unorm + 1x 20bit +fn unpack_unorm3x4_plus_unorm_20_(v: u32) -> vec4 { + return vec4( + f32(v & 0xfu) / 15.0, + f32((v >> 4u) & 0xFu) / 15.0, + f32((v >> 8u) & 0xFu) / 15.0, + f32((v >> 12u) & 0xFFFFFFu) / U20MAXF, + ); +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 1c64e904d472db..476650e5fe0cc0 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -2,6 +2,7 @@ pub mod wireframe; mod alpha; mod bundle; +pub mod deferred; mod environment_map; mod fog; mod light; @@ -61,6 +62,8 @@ use bevy_render::{ use bevy_transform::TransformSystem; use environment_map::EnvironmentMapPlugin; +use crate::deferred::DeferredPbrLightingPlugin; + pub const PBR_TYPES_SHADER_HANDLE: Handle = Handle::weak_from_u128(1708015359337029744); pub const PBR_BINDINGS_SHADER_HANDLE: Handle = Handle::weak_from_u128(5635987986427308186); pub const UTILS_HANDLE: Handle = Handle::weak_from_u128(1900548483293416725); @@ -74,18 +77,26 @@ pub const PBR_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(16550102 pub const PBR_AMBIENT_HANDLE: Handle = Handle::weak_from_u128(2441520459096337034); pub const PARALLAX_MAPPING_SHADER_HANDLE: Handle = Handle::weak_from_u128(17035894873630133905); +pub const PBR_PREPASS_FUNCTIONS_SHADER_HANDLE: Handle = + Handle::weak_from_u128(73204817249182637); +pub const PBR_DEFERRED_TYPES_HANDLE: Handle = Handle::weak_from_u128(3221241127431430599); +pub const PBR_DEFERRED_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(72019026415438599); +pub const RGB9E5_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(2659010996143919192); /// Sets up the entire PBR infrastructure of bevy. 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, + /// Controls if [`DeferredPbrLightingPlugin`] is added. + pub add_default_deferred_lighting_plugin: bool, } impl Default for PbrPlugin { fn default() -> Self { Self { prepass_enabled: true, + add_default_deferred_lighting_plugin: true, } } } @@ -123,6 +134,18 @@ impl Plugin for PbrPlugin { "render/shadows.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PBR_DEFERRED_TYPES_HANDLE, + "deferred/pbr_deferred_types.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + PBR_DEFERRED_FUNCTIONS_HANDLE, + "deferred/pbr_deferred_functions.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, SHADOW_SAMPLING_HANDLE, @@ -135,6 +158,12 @@ impl Plugin for PbrPlugin { "render/pbr_functions.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + RGB9E5_FUNCTIONS_HANDLE, + "render/rgb9e5.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, PBR_AMBIENT_HANDLE, @@ -142,6 +171,12 @@ impl Plugin for PbrPlugin { Shader::from_wgsl ); load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl); + load_internal_asset!( + app, + PBR_PREPASS_FUNCTIONS_SHADER_HANDLE, + "render/pbr_prepass_functions.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, PBR_PREPASS_SHADER_HANDLE, @@ -178,6 +213,8 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .init_resource::() + .register_type::() + .init_resource::() .add_plugins(( MeshRenderPlugin, MaterialPlugin:: { @@ -188,6 +225,7 @@ impl Plugin for PbrPlugin { EnvironmentMapPlugin, ExtractResourcePlugin::::default(), FogPlugin, + ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), )) .configure_sets( @@ -243,6 +281,10 @@ impl Plugin for PbrPlugin { ), ); + if self.add_default_deferred_lighting_plugin { + app.add_plugins(DeferredPbrLightingPlugin); + } + app.world.resource_mut::>().insert( Handle::::default(), StandardMaterial { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 50738889f5910f..96e43017b877bd 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -8,7 +8,7 @@ use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Hand use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, experimental::taa::TemporalAntiAliasSettings, - prepass::NormalPrepass, + prepass::{DeferredPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; @@ -16,7 +16,9 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; +use bevy_reflect::Reflect; use bevy_render::{ + extract_resource::ExtractResource, mesh::{Mesh, MeshVertexBufferLayout}, prelude::Image, render_asset::{prepare_assets, RenderAssets}, @@ -116,8 +118,16 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { AlphaMode::Opaque } + /// Returns if this material should be rendered by the deferred or forward renderer. + /// for `AlphaMode::Opaque` or `AlphaMode::Mask` materials. + /// If `OpaqueRendererMethod::Auto`, it will default to what is selected in the `DefaultOpaqueRendererMethod` resource. #[inline] - /// Add a bias to the view depth of the mesh which can be used to force a specific render order + fn opaque_render_method(&self) -> OpaqueRendererMethod { + OpaqueRendererMethod::Forward + } + + #[inline] + /// Add a bias to the view depth of the mesh which can be used to force a specific render order. /// for meshes with similar depth, to avoid z-fighting. /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. fn depth_bias(&self) -> f32 { @@ -137,6 +147,19 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { ShaderRef::Default } + /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the default deferred vertex shader + /// will be used. + fn deferred_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred fragment shader. If [`ShaderRef::Default`] is returned, the default deferred fragment shader + /// will be used. + #[allow(unused_variables)] + fn deferred_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)] @@ -422,6 +445,7 @@ pub fn queue_material_meshes( Option<&ShadowFilteringMethod>, Option<&ScreenSpaceAmbientOcclusionSettings>, Option<&NormalPrepass>, + Option<&DeferredPrepass>, Option<&TemporalAntiAliasSettings>, &mut RenderPhase, &mut RenderPhase, @@ -439,6 +463,7 @@ pub fn queue_material_meshes( shadow_filter_method, ssao, normal_prepass, + deferred_prepass, taa_settings, mut opaque_phase, mut alpha_mask_phase, @@ -455,6 +480,11 @@ pub fn queue_material_meshes( if normal_prepass.is_some() { view_key |= MeshPipelineKey::NORMAL_PREPASS; } + + if deferred_prepass.is_some() { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + if taa_settings.is_some() { view_key |= MeshPipelineKey::TAA; } @@ -502,6 +532,13 @@ pub fn queue_material_meshes( let Some(material) = render_materials.get(material_asset_id) else { continue; }; + + let forward = match material.properties.render_method { + OpaqueRendererMethod::Forward => true, + OpaqueRendererMethod::Deferred => false, + OpaqueRendererMethod::Auto => unreachable!(), + }; + let mut mesh_key = view_key; mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); @@ -511,6 +548,10 @@ pub fn queue_material_meshes( } mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode); + if deferred_prepass.is_some() && !forward { + mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &material_pipeline, @@ -535,24 +576,28 @@ pub fn queue_material_meshes( + material.properties.depth_bias; match material.properties.alpha_mode { AlphaMode::Opaque => { - opaque_phase.add(Opaque3d { - entity: *visible_entity, - draw_function: draw_opaque_pbr, - pipeline: pipeline_id, - distance, - batch_range: 0..1, - dynamic_offset: None, - }); + if forward { + opaque_phase.add(Opaque3d { + entity: *visible_entity, + draw_function: draw_opaque_pbr, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } } AlphaMode::Mask(_) => { - alpha_mask_phase.add(AlphaMask3d { - entity: *visible_entity, - draw_function: draw_alpha_mask_pbr, - pipeline: pipeline_id, - distance, - batch_range: 0..1, - dynamic_offset: None, - }); + if forward { + alpha_mask_phase.add(AlphaMask3d { + entity: *visible_entity, + draw_function: draw_alpha_mask_pbr, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } } AlphaMode::Blend | AlphaMode::Premultiplied @@ -572,8 +617,59 @@ pub fn queue_material_meshes( } } +/// Default render method used for opaque materials. +#[derive(Default, Resource, Clone, Debug, ExtractResource, Reflect)] +pub struct DefaultOpaqueRendererMethod(OpaqueRendererMethod); + +impl DefaultOpaqueRendererMethod { + pub fn forward() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Forward) + } + + pub fn deferred() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Deferred) + } + + pub fn set_to_forward(&mut self) { + self.0 = OpaqueRendererMethod::Forward; + } + + pub fn set_to_deferred(&mut self) { + self.0 = OpaqueRendererMethod::Deferred; + } +} + +/// Render method used for opaque materials. +/// +/// The forward rendering main pass draws each mesh entity and shades it according to its +/// corresponding material and the lights that affect it. Some render features like Screen Space +/// Ambient Occlusion require running depth and normal prepasses, that are 'deferred'-like +/// prepasses over all mesh entities to populate depth and normal textures. This means that when +/// using render features that require running prepasses, multiple passes over all visible geometry +/// are required. This can be slow if there is a lot of geometry that cannot be batched into few +/// draws. +/// +/// Deferred rendering runs a prepass to gather not only geometric information like depth and +/// normals, but also all the material properties like base color, emissive color, reflectance, +/// metalness, etc, and writes them into a deferred 'g-buffer' texture. The deferred main pass is +/// then a fullscreen pass that reads data from these textures and executes shading. This allows +/// for one pass over geometry, but is at the cost of not being able to use MSAA, and has heavier +/// bandwidth usage which can be unsuitable for low end mobile or other bandwidth-constrained devices. +/// +/// If a material indicates `OpaqueRendererMethod::Auto`, `DefaultOpaqueRendererMethod` will be used. +#[derive(Default, Clone, Copy, Debug, Reflect)] +pub enum OpaqueRendererMethod { + #[default] + Forward, + Deferred, + Auto, +} + /// Common [`Material`] properties, calculated for a specific material instance. pub struct MaterialProperties { + /// Is this material should be rendered by the deferred renderer when. + /// AlphaMode::Opaque or AlphaMode::Mask + pub render_method: OpaqueRendererMethod, /// The [`AlphaMode`] of this material. pub alpha_mode: AlphaMode, /// Add a bias to the view depth of the mesh which can be used to force a specific render order @@ -676,6 +772,7 @@ impl Default for PrepareNextFrameMaterials { /// This system prepares all assets of the corresponding [`Material`] type /// which where extracted this frame for the GPU. +#[allow(clippy::too_many_arguments)] pub fn prepare_materials( mut prepare_next_frame: Local>, mut extracted_assets: ResMut>, @@ -684,6 +781,7 @@ pub fn prepare_materials( images: Res>, fallback_image: Res, pipeline: Res>, + default_opaque_render_method: Res, ) { let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, material) in queued_assets.into_iter() { @@ -693,6 +791,7 @@ pub fn prepare_materials( &images, &fallback_image, &pipeline, + default_opaque_render_method.0, ) { Ok(prepared_asset) => { render_materials.insert(id, prepared_asset); @@ -714,6 +813,7 @@ pub fn prepare_materials( &images, &fallback_image, &pipeline, + default_opaque_render_method.0, ) { Ok(prepared_asset) => { render_materials.insert(id, prepared_asset); @@ -731,6 +831,7 @@ fn prepare_material( images: &RenderAssets, fallback_image: &FallbackImage, pipeline: &MaterialPipeline, + default_opaque_render_method: OpaqueRendererMethod, ) -> Result, AsBindGroupError> { let prepared = material.as_bind_group( &pipeline.material_layout, @@ -738,6 +839,11 @@ fn prepare_material( images, fallback_image, )?; + let method = match material.opaque_render_method() { + OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, + OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, + OpaqueRendererMethod::Auto => default_opaque_render_method, + }; Ok(PreparedMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, @@ -745,6 +851,7 @@ fn prepare_material( properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), + render_method: method, }, }) } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index f66fc80e3f249a..a07c144374310c 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,6 +1,7 @@ use crate::{ - AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, ParallaxMappingMethod, - PBR_PREPASS_SHADER_HANDLE, PBR_SHADER_HANDLE, + deferred::DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, AlphaMode, Material, MaterialPipeline, + MaterialPipelineKey, OpaqueRendererMethod, ParallaxMappingMethod, PBR_PREPASS_SHADER_HANDLE, + PBR_SHADER_HANDLE, }; use bevy_asset::{Asset, Handle}; use bevy_math::Vec4; @@ -316,6 +317,14 @@ pub struct StandardMaterial { /// /// Default is `16.0`. pub max_parallax_layer_count: f32, + + /// Render method used for opaque materials. (Where `alpha_mode` is [`AlphaMode::Opaque`] or [`AlphaMode::Mask`]) + pub opaque_render_method: OpaqueRendererMethod, + + /// Used for selecting the deferred lighting pass for deferred materials. + /// Default is [`DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID`] for default + /// PBR deferred lighting pass. Ignored in the case of forward materials. + pub deferred_lighting_pass_id: u8, } impl Default for StandardMaterial { @@ -349,6 +358,8 @@ impl Default for StandardMaterial { parallax_depth_scale: 0.1, max_parallax_layer_count: 16.0, parallax_mapping_method: ParallaxMappingMethod::Occlusion, + opaque_render_method: OpaqueRendererMethod::Auto, + deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, } } } @@ -441,6 +452,8 @@ pub struct StandardMaterialUniform { /// Using [`ParallaxMappingMethod::Relief`], how many additional /// steps to use at most to find the depth value. pub max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + pub deferred_lighting_pass_id: u32, } impl AsBindGroupShaderType for StandardMaterial { @@ -514,6 +527,7 @@ impl AsBindGroupShaderType for StandardMaterial { parallax_depth_scale: self.parallax_depth_scale, max_parallax_layer_count: self.max_parallax_layer_count, max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), + deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, } } } @@ -572,6 +586,10 @@ impl Material for StandardMaterial { PBR_PREPASS_SHADER_HANDLE.into() } + fn deferred_fragment_shader() -> ShaderRef { + PBR_SHADER_HANDLE.into() + } + fn fragment_shader() -> ShaderRef { PBR_SHADER_HANDLE.into() } @@ -585,4 +603,9 @@ impl Material for StandardMaterial { fn depth_bias(&self) -> f32 { self.depth_bias } + + #[inline] + fn opaque_render_method(&self) -> OpaqueRendererMethod { + self.opaque_render_method + } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 372757f9351485..53e26506121a0b 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,11 +1,15 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_asset::{load_internal_asset, AssetServer, Handle}; use bevy_core_pipeline::{ + core_3d::CORE_3D_DEPTH_FORMAT, + deferred::{ + AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, + DEFERRED_PREPASS_FORMAT, + }, prelude::Camera3d, prepass::{ - AlphaMask3dPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, Opaque3dPrepass, - ViewPrepassTextures, DEPTH_PREPASS_FORMAT, MOTION_VECTOR_PREPASS_FORMAT, - NORMAL_PREPASS_FORMAT, + AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, + Opaque3dPrepass, ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, }; use bevy_ecs::{ @@ -28,27 +32,28 @@ use bevy_render::{ }, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, - BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, - ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, - DynamicUniformBuffer, FragmentState, FrontFace, MultisampleState, PipelineCache, - PolygonMode, PrimitiveState, PushConstantRange, RenderPipelineDescriptor, Shader, - ShaderRef, ShaderStages, ShaderType, SpecializedMeshPipeline, SpecializedMeshPipelineError, - SpecializedMeshPipelines, StencilFaceState, StencilState, TextureSampleType, - TextureViewDimension, VertexState, + BindGroupLayoutEntry, BindingResource, BindingType, BufferBindingType, ColorTargetState, + ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, DynamicUniformBuffer, + FragmentState, FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState, + PushConstantRange, RenderPipelineDescriptor, Shader, ShaderRef, ShaderStages, ShaderType, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + StencilFaceState, StencilState, TextureAspect, TextureFormat, TextureSampleType, + TextureView, TextureViewDescriptor, TextureViewDimension, VertexState, }, renderer::{RenderDevice, RenderQueue}, - texture::{FallbackImagesDepth, FallbackImagesMsaa}, + texture::{BevyDefault, FallbackImageMsaa}, view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::prelude::GlobalTransform; +use bevy_utils::default; use bevy_utils::tracing::error; use crate::{ prepare_materials, setup_morph_and_skinning_defs, AlphaMode, DrawMesh, Material, MaterialPipeline, MaterialPipelineKey, MeshLayouts, MeshPipeline, MeshPipelineKey, - RenderMaterialInstances, RenderMaterials, RenderMeshInstances, SetMaterialBindGroup, - SetMeshBindGroup, + OpaqueRendererMethod, RenderMaterialInstances, RenderMaterials, RenderMeshInstances, + SetMaterialBindGroup, SetMeshBindGroup, }; use std::{hash::Hash, marker::PhantomData}; @@ -60,6 +65,8 @@ pub const PREPASS_BINDINGS_SHADER_HANDLE: Handle = pub const PREPASS_UTILS_SHADER_HANDLE: Handle = Handle::weak_from_u128(4603948296044544); +pub const PREPASS_IO_SHADER_HANDLE: Handle = Handle::weak_from_u128(81212356509530944); + /// Sets up everything required to use the prepass pipeline. /// /// This does not add the actual prepasses, see [`PrepassPlugin`] for that. @@ -97,6 +104,13 @@ where Shader::from_wgsl ); + load_internal_asset!( + app, + PREPASS_IO_SHADER_HANDLE, + "prepass_io.wgsl", + Shader::from_wgsl + ); + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -172,6 +186,8 @@ where render_app .add_render_command::>() .add_render_command::>() + .add_render_command::>() + .add_render_command::>() .add_systems( Render, queue_prepass_material_meshes:: @@ -225,8 +241,10 @@ pub struct PrepassPipeline { pub view_layout_no_motion_vectors: BindGroupLayout, pub mesh_layouts: MeshLayouts, pub material_layout: BindGroupLayout, - pub material_vertex_shader: Option>, - pub material_fragment_shader: Option>, + pub prepass_material_vertex_shader: Option>, + pub prepass_material_fragment_shader: Option>, + pub deferred_material_vertex_shader: Option>, + pub deferred_material_fragment_shader: Option>, pub material_pipeline: MaterialPipeline, _marker: PhantomData, } @@ -311,12 +329,22 @@ impl FromWorld for PrepassPipeline { view_layout_motion_vectors, view_layout_no_motion_vectors, mesh_layouts: mesh_pipeline.mesh_layouts.clone(), - material_vertex_shader: match M::prepass_vertex_shader() { + prepass_material_vertex_shader: match M::prepass_vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + prepass_material_fragment_shader: match M::prepass_fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + deferred_material_vertex_shader: match M::deferred_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() { + deferred_material_fragment_shader: match M::deferred_fragment_shader() { ShaderRef::Default => None, ShaderRef::Handle(handle) => Some(handle), ShaderRef::Path(path) => Some(asset_server.load(path)), @@ -354,6 +382,11 @@ where // The main limitation right now is that bind group order is hardcoded in shaders. bind_group_layouts.insert(1, self.material_layout.clone()); + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + shader_defs.push("WEBGL2".into()); + + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); + if key.mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { shader_defs.push("DEPTH_PREPASS".into()); } @@ -394,9 +427,15 @@ where } 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 key + .mesh_key + .intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) + { + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(2)); + shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into()); if layout.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push("VERTEX_TANGENTS".into()); vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); @@ -405,15 +444,32 @@ where if key .mesh_key - .contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) + .intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) { - shader_defs.push("MOTION_VECTOR_PREPASS".into()); + shader_defs.push("MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS".into()); + } + + if key.mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + shader_defs.push("DEFERRED_PREPASS".into()); + + if layout.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(6)); + } } if key .mesh_key - .intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::MOTION_VECTOR_PREPASS) + .contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + + if key.mesh_key.intersects( + MeshPipelineKey::NORMAL_PREPASS + | MeshPipelineKey::MOTION_VECTOR_PREPASS + | MeshPipelineKey::DEFERRED_PREPASS, + ) { shader_defs.push("PREPASS_FRAGMENT".into()); } @@ -430,25 +486,40 @@ where let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; // Setup prepass fragment targets - normals in slot 0 (or None if not needed), motion vectors in slot 1 - let mut targets = vec![]; - targets.push( + let mut targets = vec![ key.mesh_key .contains(MeshPipelineKey::NORMAL_PREPASS) .then_some(ColorTargetState { format: NORMAL_PREPASS_FORMAT, - blend: Some(BlendState::REPLACE), + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases. + blend: None, write_mask: ColorWrites::ALL, }), - ); - targets.push( key.mesh_key .contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) .then_some(ColorTargetState { format: MOTION_VECTOR_PREPASS_FORMAT, - blend: Some(BlendState::REPLACE), + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases. + blend: None, write_mask: ColorWrites::ALL, }), - ); + key.mesh_key + .contains(MeshPipelineKey::DEFERRED_PREPASS) + .then_some(ColorTargetState { + format: DEFERRED_PREPASS_FORMAT, + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases. + blend: None, + write_mask: ColorWrites::ALL, + }), + key.mesh_key + .contains(MeshPipelineKey::DEFERRED_PREPASS) + .then_some(ColorTargetState { + format: DEFERRED_LIGHTING_PASS_ID_FORMAT, + blend: None, + write_mask: ColorWrites::ALL, + }), + ]; + if targets.iter().all(Option::is_none) { // if no targets are required then clear the list, so that no fragment shader is required // (though one may still be used for discarding depth buffer writes) @@ -461,13 +532,20 @@ where let fragment_required = !targets.is_empty() || key.mesh_key.contains(MeshPipelineKey::DEPTH_CLAMP_ORTHO) || (key.mesh_key.contains(MeshPipelineKey::MAY_DISCARD) - && self.material_fragment_shader.is_some()); + && self.prepass_material_fragment_shader.is_some()); let fragment = fragment_required.then(|| { // Use the fragment shader from the material - let frag_shader_handle = match self.material_fragment_shader.clone() { - Some(frag_shader_handle) => frag_shader_handle, - _ => PREPASS_SHADER_HANDLE, + let frag_shader_handle = if key.mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + match self.deferred_material_fragment_shader.clone() { + Some(frag_shader_handle) => frag_shader_handle, + _ => PREPASS_SHADER_HANDLE, + } + } else { + match self.prepass_material_fragment_shader.clone() { + Some(frag_shader_handle) => frag_shader_handle, + _ => PREPASS_SHADER_HANDLE, + } }; FragmentState { @@ -479,7 +557,13 @@ where }); // Use the vertex shader from the material if present - let vert_shader_handle = if let Some(handle) = &self.material_vertex_shader { + let vert_shader_handle = if key.mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + if let Some(handle) = &self.deferred_material_vertex_shader { + handle.clone() + } else { + PREPASS_SHADER_HANDLE + } + } else if let Some(handle) = &self.prepass_material_vertex_shader { handle.clone() } else { PREPASS_SHADER_HANDLE @@ -512,7 +596,7 @@ where conservative: false, }, depth_stencil: Some(DepthStencilState { - format: DEPTH_PREPASS_FORMAT, + format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, depth_compare: CompareFunction::GreaterEqual, stencil: StencilState { @@ -546,9 +630,9 @@ where } pub fn get_bind_group_layout_entries( - bindings: [u32; 3], + bindings: [u32; 4], multisampled: bool, -) -> [BindGroupLayoutEntry; 3] { +) -> [BindGroupLayoutEntry; 4] { [ // Depth texture BindGroupLayoutEntry { @@ -583,53 +667,91 @@ pub fn get_bind_group_layout_entries( }, count: None, }, + // Deferred texture + BindGroupLayoutEntry { + binding: bindings[3], + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Uint, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, ] } -pub fn get_bindings<'a>( - prepass_textures: Option<&'a ViewPrepassTextures>, - fallback_images: &'a mut FallbackImagesMsaa, - fallback_depths: &'a mut FallbackImagesDepth, - msaa: &'a Msaa, - bindings: [u32; 3], -) -> [BindGroupEntry<'a>; 3] { +// Needed so the texture views can live long enough. +pub struct PrepassBindingsSet([TextureView; 4]); + +impl PrepassBindingsSet { + pub fn get_entries(&self, bindings: [u32; 4]) -> [BindGroupEntry; 4] { + [ + BindGroupEntry { + binding: bindings[0], + resource: BindingResource::TextureView(&self.0[0]), + }, + BindGroupEntry { + binding: bindings[1], + resource: BindingResource::TextureView(&self.0[1]), + }, + BindGroupEntry { + binding: bindings[2], + resource: BindingResource::TextureView(&self.0[2]), + }, + BindGroupEntry { + binding: bindings[3], + resource: BindingResource::TextureView(&self.0[3]), + }, + ] + } +} + +pub fn get_bindings( + prepass_textures: Option<&ViewPrepassTextures>, + fallback_images: &mut FallbackImageMsaa, + msaa: &Msaa, +) -> PrepassBindingsSet { + let depth_desc = TextureViewDescriptor { + label: Some("prepass_depth"), + aspect: TextureAspect::DepthOnly, + ..default() + }; 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 - } + Some(texture) => texture.texture.create_view(&depth_desc), + None => fallback_images + .image_for_samplecount(msaa.samples(), CORE_3D_DEPTH_FORMAT) + .texture + .create_view(&depth_desc), }; let normal_motion_vectors_fallback = &fallback_images - .image_for_samplecount(msaa.samples()) + .image_for_samplecount(msaa.samples(), TextureFormat::bevy_default()) .texture_view; let normal_view = match prepass_textures.and_then(|x| x.normal.as_ref()) { Some(texture) => &texture.default_view, None => normal_motion_vectors_fallback, - }; + } + .clone(); let motion_vectors_view = match prepass_textures.and_then(|x| x.motion_vectors.as_ref()) { Some(texture) => &texture.default_view, None => normal_motion_vectors_fallback, - }; + } + .clone(); - [ - BindGroupEntry { - binding: bindings[0], - resource: BindingResource::TextureView(depth_view), - }, - BindGroupEntry { - binding: bindings[1], - resource: BindingResource::TextureView(normal_view), - }, - BindGroupEntry { - binding: bindings[2], - resource: BindingResource::TextureView(motion_vectors_view), - }, - ] + let deferred_fallback = &fallback_images + .image_for_samplecount(1, TextureFormat::Rgba32Uint) + .texture_view; + + let deferred_view = match prepass_textures.and_then(|x| x.deferred.as_ref()) { + Some(texture) => &texture.default_view, + None => deferred_fallback, + } + .clone(); + + PrepassBindingsSet([depth_view, normal_view, motion_vectors_view, deferred_view]) } // Extract the render phases for the prepass @@ -754,6 +876,8 @@ pub fn prepare_prepass_view_bind_group( pub fn queue_prepass_material_meshes( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, + opaque_deferred_draw_functions: Res>, + alpha_mask_deferred_draw_functions: Res>, prepass_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, @@ -767,9 +891,12 @@ pub fn queue_prepass_material_meshes( &VisibleEntities, &mut RenderPhase, &mut RenderPhase, + Option<&mut RenderPhase>, + Option<&mut RenderPhase>, Option<&DepthPrepass>, Option<&NormalPrepass>, Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, @@ -782,14 +909,25 @@ pub fn queue_prepass_material_meshes( .read() .get_id::>() .unwrap(); + let opaque_draw_deferred = opaque_deferred_draw_functions + .read() + .get_id::>() + .unwrap(); + let alpha_mask_draw_deferred = alpha_mask_deferred_draw_functions + .read() + .get_id::>() + .unwrap(); for ( view, visible_entities, mut opaque_phase, mut alpha_mask_phase, + mut opaque_deferred_phase, + mut alpha_mask_deferred_phase, depth_prepass, normal_prepass, motion_vector_prepass, + deferred_prepass, ) in &mut views { let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); @@ -803,6 +941,9 @@ pub fn queue_prepass_material_meshes( view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; } + let mut opaque_phase_deferred = opaque_deferred_phase.as_mut(); + let mut alpha_mask_phase_deferred = alpha_mask_deferred_phase.as_mut(); + let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { @@ -834,6 +975,18 @@ pub fn queue_prepass_material_meshes( | AlphaMode::Multiply => continue, } + let forward = match material.properties.render_method { + OpaqueRendererMethod::Forward => true, + OpaqueRendererMethod::Deferred => false, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + let deferred = deferred_prepass.is_some() && !forward; + + if deferred { + mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &prepass_pipeline, @@ -856,24 +1009,52 @@ pub fn queue_prepass_material_meshes( + material.properties.depth_bias; match alpha_mode { AlphaMode::Opaque => { - opaque_phase.add(Opaque3dPrepass { - entity: *visible_entity, - draw_function: opaque_draw_prepass, - pipeline_id, - distance, - batch_range: 0..1, - dynamic_offset: None, - }); + if deferred { + opaque_phase_deferred + .as_mut() + .unwrap() + .add(Opaque3dDeferred { + entity: *visible_entity, + draw_function: opaque_draw_deferred, + pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } else { + opaque_phase.add(Opaque3dPrepass { + entity: *visible_entity, + draw_function: opaque_draw_prepass, + pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } } AlphaMode::Mask(_) => { - alpha_mask_phase.add(AlphaMask3dPrepass { - entity: *visible_entity, - draw_function: alpha_mask_draw_prepass, - pipeline_id, - distance, - batch_range: 0..1, - dynamic_offset: None, - }); + if deferred { + alpha_mask_phase_deferred + .as_mut() + .unwrap() + .add(AlphaMask3dDeferred { + entity: *visible_entity, + draw_function: alpha_mask_draw_deferred, + pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } else { + alpha_mask_phase.add(AlphaMask3dPrepass { + entity: *visible_entity, + draw_function: alpha_mask_draw_prepass, + pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } } AlphaMode::Blend | AlphaMode::Premultiplied diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 396935ff411e7e..fd2021e9eee1ee 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -1,60 +1,15 @@ #import bevy_pbr::prepass_bindings #import bevy_pbr::mesh_functions +#import bevy_pbr::prepass_io Vertex, VertexOutput, FragmentInput, FragmentOutput #import bevy_pbr::skinning #import bevy_pbr::morph #import bevy_pbr::mesh_bindings mesh #import bevy_render::instance_index get_instance_index +#import bevy_pbr::mesh_view_bindings view, previous_view_proj -// 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 { - @builtin(instance_index) instance_index: u32, - @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 - -#ifdef MORPH_TARGETS - @builtin(vertex_index) index: u32, -#endif // MORPH_TARGETS -} - -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 - -#ifdef MOTION_VECTOR_PREPASS - @location(3) world_position: vec4, - @location(4) previous_world_position: vec4, -#endif // MOTION_VECTOR_PREPASS - -#ifdef DEPTH_CLAMP_ORTHO - @location(5) clip_position_unclamped: vec4, -#endif // DEPTH_CLAMP_ORTHO -} +#ifdef DEFERRED_PREPASS +#import bevy_pbr::rgb9e5 +#endif #ifdef MORPH_TARGETS fn morph_vertex(vertex_in: Vertex) -> Vertex { @@ -105,7 +60,7 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.uv = vertex.uv; #endif // VERTEX_UVS -#ifdef NORMAL_PREPASS +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED out.world_normal = bevy_pbr::skinning::skin_normals(model, vertex.normal); #else // SKINNED @@ -126,10 +81,17 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { get_instance_index(vertex_no_morph.instance_index) ); #endif // VERTEX_TANGENTS -#endif // NORMAL_PREPASS +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS -#ifdef MOTION_VECTOR_PREPASS +#ifdef VERTEX_COLORS + out.color = vertex.color; +#endif + +#ifdef MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS out.world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world(model, vec4(vertex.position, 1.0)); +#endif // MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS + +#ifdef MOTION_VECTOR_PREPASS // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. // See https://github.com/gfx-rs/naga/issues/2416 out.previous_world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world( @@ -138,43 +100,16 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { ); #endif // MOTION_VECTOR_PREPASS +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + out.instance_index = get_instance_index(vertex_no_morph.instance_index); +#endif + return out; } #ifdef PREPASS_FRAGMENT -struct FragmentInput { -#ifdef VERTEX_UVS - @location(0) uv: vec2, -#endif // VERTEX_UVS - -#ifdef NORMAL_PREPASS - @location(1) world_normal: vec3, -#endif // NORMAL_PREPASS - -#ifdef MOTION_VECTOR_PREPASS - @location(3) world_position: vec4, - @location(4) previous_world_position: vec4, -#endif // MOTION_VECTOR_PREPASS - -#ifdef DEPTH_CLAMP_ORTHO - @location(5) clip_position_unclamped: vec4, -#endif // DEPTH_CLAMP_ORTHO -} - -struct FragmentOutput { -#ifdef NORMAL_PREPASS - @location(0) normal: vec4, -#endif // NORMAL_PREPASS - -#ifdef MOTION_VECTOR_PREPASS - @location(1) motion_vector: vec2, -#endif // MOTION_VECTOR_PREPASS - -#ifdef DEPTH_CLAMP_ORTHO - @builtin(frag_depth) frag_depth: f32, -#endif // DEPTH_CLAMP_ORTHO -} - @fragment fn fragment(in: FragmentInput) -> FragmentOutput { var out: FragmentOutput; @@ -188,7 +123,7 @@ fn fragment(in: FragmentInput) -> FragmentOutput { #endif // DEPTH_CLAMP_ORTHO #ifdef MOTION_VECTOR_PREPASS - let clip_position_t = bevy_pbr::prepass_bindings::view.unjittered_view_proj * in.world_position; + let clip_position_t = view.unjittered_view_proj * in.world_position; let clip_position = clip_position_t.xy / clip_position_t.w; let previous_clip_position_t = bevy_pbr::prepass_bindings::previous_view_proj * in.previous_world_position; let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; @@ -201,6 +136,15 @@ fn fragment(in: FragmentInput) -> FragmentOutput { out.motion_vector = (clip_position - previous_clip_position) * vec2(0.5, -0.5); #endif // MOTION_VECTOR_PREPASS +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // The is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + return out; } #endif // PREPASS_FRAGMENT diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl index 965af6a19425ea..5f5fef362acf68 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -1,11 +1,6 @@ #define_import_path bevy_pbr::prepass_bindings -#import bevy_render::view View -#import bevy_render::globals Globals #import bevy_pbr::mesh_types -@group(0) @binding(0) var view: View; -@group(0) @binding(1) var globals: Globals; - #ifdef MOTION_VECTOR_PREPASS @group(0) @binding(2) var previous_view_proj: mat4x4; #endif // MOTION_VECTOR_PREPASS diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl new file mode 100644 index 00000000000000..ca49da07e1c0fa --- /dev/null +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -0,0 +1,114 @@ +#define_import_path bevy_pbr::prepass_io + +// 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 { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, + +#ifdef VERTEX_UVS + @location(1) uv: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(2) normal: vec3, +#ifdef VERTEX_TANGENTS + @location(3) tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef SKINNED + @location(4) joint_indices: vec4, + @location(5) joint_weights: vec4, +#endif + +#ifdef VERTEX_COLORS + @location(6) color: vec4, +#endif + +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif // MORPH_TARGETS +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + + @location(3) world_position: vec4, +#ifdef MOTION_VECTOR_PREPASS + @location(4) previous_world_position: vec4, +#endif + +#ifdef DEPTH_CLAMP_ORTHO + @location(5) clip_position_unclamped: vec4, +#endif // DEPTH_CLAMP_ORTHO +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(6) instance_index: u32, +#endif + +#ifdef VERTEX_COLORS + @location(7) color: vec4, +#endif +} + +struct FragmentInput { + @builtin(position) position: vec4, +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + + @location(3) world_position: vec4, +#ifdef MOTION_VECTOR_PREPASS + @location(4) previous_world_position: vec4, +#endif // MOTION_VECTOR_PREPASS + +#ifdef DEPTH_CLAMP_ORTHO + @location(5) clip_position_unclamped: vec4, +#endif // DEPTH_CLAMP_ORTHO +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(6) instance_index: u32, +#endif + +#ifdef VERTEX_COLORS + @location(7) color: vec4, +#endif +}; + +#ifdef PREPASS_FRAGMENT +struct FragmentOutput { +#ifdef NORMAL_PREPASS + @location(0) normal: vec4, +#endif + +#ifdef MOTION_VECTOR_PREPASS + @location(1) motion_vector: vec2, +#endif + +#ifdef DEFERRED_PREPASS + @location(2) deferred: vec4, + @location(3) deferred_lighting_pass_id: u32, +#endif + +#ifdef DEPTH_CLAMP_ORTHO + @builtin(frag_depth) frag_depth: f32, +#endif // DEPTH_CLAMP_ORTHO +} +#endif //PREPASS_FRAGMENT diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index feec375ddc3d30..258ea59f09065d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -6,7 +6,7 @@ use crate::{ PointLight, PointLightShadowMap, PrepassPipeline, RenderMaterialInstances, RenderMaterials, RenderMeshInstances, SpotLight, VisiblePointLights, }; -use bevy_core_pipeline::core_3d::Transparent3d; +use bevy_core_pipeline::core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}; use bevy_ecs::prelude::*; use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ @@ -218,7 +218,6 @@ pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; pub const MAX_CASCADES_PER_LIGHT: usize = 4; #[cfg(all(feature = "webgl", target_arch = "wasm32"))] pub const MAX_CASCADES_PER_LIGHT: usize = 1; -pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; #[derive(Resource, Clone)] pub struct ShadowSamplers { @@ -912,7 +911,7 @@ pub fn prepare_lights( mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, - format: SHADOW_FORMAT, + format: CORE_3D_DEPTH_FORMAT, label: Some("point_light_shadow_map_texture"), usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, view_formats: &[], @@ -933,7 +932,7 @@ pub fn prepare_lights( mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, - format: SHADOW_FORMAT, + format: CORE_3D_DEPTH_FORMAT, label: Some("directional_light_shadow_map_texture"), usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, view_formats: &[], @@ -1172,7 +1171,7 @@ pub fn prepare_lights( dimension: Some(TextureViewDimension::CubeArray), #[cfg(all(feature = "webgl", target_arch = "wasm32"))] dimension: Some(TextureViewDimension::Cube), - aspect: TextureAspect::All, + aspect: TextureAspect::DepthOnly, base_mip_level: 0, mip_level_count: None, base_array_layer: 0, @@ -1187,7 +1186,7 @@ pub fn prepare_lights( dimension: Some(TextureViewDimension::D2Array), #[cfg(all(feature = "webgl", target_arch = "wasm32"))] dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, + aspect: TextureAspect::DepthOnly, base_mip_level: 0, mip_level_count: None, base_array_layer: 0, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 790470c3ae4959..a883b56555697d 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -8,7 +8,8 @@ use crate::{ use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, AssetId, Handle}; use bevy_core_pipeline::{ - core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, + core_3d::{AlphaMask3d, Opaque3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, + deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, prepass::ViewPrepassTextures, tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, @@ -37,8 +38,8 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, DefaultImageSampler, FallbackImageCubemap, FallbackImagesDepth, - FallbackImagesMsaa, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, + BevyDefault, DefaultImageSampler, FallbackImageCubemap, FallbackImageMsaa, GpuImage, Image, + ImageSampler, TextureFormatPixelInfo, }, view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, @@ -137,6 +138,8 @@ impl Plugin for MeshRenderPlugin { batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, ) .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: @@ -513,7 +516,7 @@ impl FromWorld for MeshPipeline { || (cfg!(all(feature = "webgl", target_arch = "wasm32")) && !multisampled) { entries.extend_from_slice(&prepass::get_bind_group_layout_entries( - [17, 18, 19], + [17, 18, 19, 20], multisampled, )); } @@ -637,14 +640,15 @@ bitflags::bitflags! { const DEBAND_DITHER = (1 << 2); const DEPTH_PREPASS = (1 << 3); const NORMAL_PREPASS = (1 << 4); - const MOTION_VECTOR_PREPASS = (1 << 5); - const MAY_DISCARD = (1 << 6); // Guards shader codepaths that may discard, allowing early depth tests in most cases + const DEFERRED_PREPASS = (1 << 5); + const MOTION_VECTOR_PREPASS = (1 << 6); + const MAY_DISCARD = (1 << 7); // Guards shader codepaths that may discard, allowing early depth tests in most cases // See: https://www.khronos.org/opengl/wiki/Early_Fragment_Test - const ENVIRONMENT_MAP = (1 << 7); - const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 8); - const DEPTH_CLAMP_ORTHO = (1 << 9); - const TAA = (1 << 10); - const MORPH_TARGETS = (1 << 11); + const ENVIRONMENT_MAP = (1 << 8); + const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 9); + const DEPTH_CLAMP_ORTHO = (1 << 10); + const TAA = (1 << 11); + const MORPH_TARGETS = (1 << 12); const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); // @@ -862,7 +866,8 @@ impl SpecializedMeshPipeline for MeshPipeline { depth_write_enabled = false; } else { label = "opaque_mesh_pipeline".into(); - blend = Some(BlendState::REPLACE); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; // For the opaque and alpha mask passes, fragments that are closer will replace // the current fragment value in the output and the depth is written to the // depth buffer @@ -874,6 +879,9 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("LOAD_PREPASS_NORMALS".into()); } + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + shader_defs.push("WEBGL2".into()); + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); @@ -978,7 +986,7 @@ impl SpecializedMeshPipeline for MeshPipeline { strip_index_format: None, }, depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, + format: CORE_3D_DEPTH_FORMAT, depth_write_enabled, depth_compare: CompareFunction::GreaterEqual, stencil: StencilState { @@ -1090,10 +1098,9 @@ pub fn prepare_mesh_view_bind_groups( Option<&EnvironmentMapLight>, &Tonemapping, )>, - (images, mut fallback_images, mut fallback_depths, fallback_cubemap): ( + (images, mut fallback_images, fallback_cubemap): ( Res>, - FallbackImagesMsaa, - FallbackImagesDepth, + FallbackImageMsaa, Res, ), msaa: Res, @@ -1124,7 +1131,7 @@ pub fn prepare_mesh_view_bind_groups( ) in &views { let fallback_ssao = fallback_images - .image_for_samplecount(1) + .image_for_samplecount(1, TextureFormat::bevy_default()) .texture_view .clone(); @@ -1205,28 +1212,31 @@ pub fn prepare_mesh_view_bind_groups( get_lut_bindings(&images, &tonemapping_luts, tonemapping, [15, 16]); entries.extend_from_slice(&tonemapping_luts); + let label = Some("mesh_view_bind_group"); + // When using WebGL, we can't have a depth texture with multisampling if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || (cfg!(all(feature = "webgl", target_arch = "wasm32")) && msaa.samples() == 1) { - entries.extend_from_slice(&prepass::get_bindings( - prepass_textures, - &mut fallback_images, - &mut fallback_depths, - &msaa, - [17, 18, 19], - )); + let prepass_bindings = + prepass::get_bindings(prepass_textures, &mut fallback_images, &msaa); + entries.extend_from_slice(&prepass_bindings.get_entries([17, 18, 19, 20])); + commands.entity(entity).insert(MeshViewBindGroup { + value: render_device.create_bind_group(&BindGroupDescriptor { + entries: &entries, + label, + layout, + }), + }); + } else { + commands.entity(entity).insert(MeshViewBindGroup { + value: render_device.create_bind_group(&BindGroupDescriptor { + entries: &entries, + label, + layout, + }), + }); } - - let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &entries, - label: Some("mesh_view_bind_group"), - layout, - }); - - commands.entity(entity).insert(MeshViewBindGroup { - value: view_bind_group, - }); } } } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 4ed5cc3da1a66e..f3b51ddc50abb9 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -49,4 +49,5 @@ @group(0) @binding(17) var depth_prepass_texture: texture_depth_2d; @group(0) @binding(18) var normal_prepass_texture: texture_2d; @group(0) @binding(19) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(20) var deferred_prepass_texture: texture_2d; #endif diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 545b40c5b3a243..125d07d0e2a012 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -3,9 +3,7 @@ #import bevy_pbr::pbr_functions as pbr_functions #import bevy_pbr::pbr_bindings as pbr_bindings #import bevy_pbr::pbr_types as pbr_types -#import bevy_pbr::prepass_utils -#import bevy_pbr::mesh_vertex_output MeshVertexOutput #import bevy_pbr::mesh_bindings mesh #import bevy_pbr::mesh_view_bindings view, fog, screen_space_ambient_occlusion_texture #import bevy_pbr::mesh_view_types FOG_MODE_OFF @@ -18,11 +16,34 @@ #import bevy_pbr::gtao_utils gtao_multibounce #endif +#ifdef DEFERRED_PREPASS +#import bevy_pbr::pbr_deferred_types as pbr_deferred_types +#import bevy_pbr::pbr_deferred_functions as pbr_deferred_functions +#import bevy_pbr::pbr_prepass_functions as pbr_prepass_functions +#import bevy_pbr::prepass_io as prepass_io +#else // DEFERRED_PREPASS +#import bevy_pbr::mesh_vertex_output as mesh_vertex_output +#endif // DEFERRED_PREPASS + +#ifdef MOTION_VECTOR_PREPASS +@group(0) @binding(2) +var previous_view_proj: mat4x4; +#endif // MOTION_VECTOR_PREPASS + + @fragment +#ifdef DEFERRED_PREPASS fn fragment( - in: MeshVertexOutput, - @builtin(front_facing) is_front: bool, -) -> @location(0) vec4 { + in: prepass_io::FragmentInput, + @builtin(front_facing) is_front: bool, + ) -> prepass_io::FragmentOutput { + var out: prepass_io::FragmentOutput; +#else // DEFERRED_PREPASS +fn fragment( + in: mesh_vertex_output::MeshVertexOutput, + @builtin(front_facing) is_front: bool, + ) -> @location(0) vec4 { +#endif // DEFERRED_PREPASS var output_color: vec4 = pbr_bindings::material.base_color; let is_orthographic = view.projection[3].w == 1.0; @@ -57,13 +78,13 @@ fn fragment( if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { output_color = output_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); } -#endif +#endif // VERTEX_UVS // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { // Prepare a 'processed' StandardMaterial by sampling all textures to resolve // the material members - var pbr_input: pbr_functions::PbrInput; + var pbr_input: pbr_types::PbrInput; pbr_input.material.base_color = output_color; pbr_input.material.reflectance = pbr_bindings::material.reflectance; @@ -99,11 +120,13 @@ fn fragment( occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); } #endif +#ifndef DEFERRED_PREPASS #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); occlusion = min(occlusion, ssao_multibounce); -#endif +#endif // SCREEN_SPACE_AMBIENT_OCCLUSION +#endif // DEFERRED_PREPASS pbr_input.occlusion = occlusion; pbr_input.frag_coord = in.position; @@ -139,12 +162,40 @@ fn fragment( pbr_input.occlusion = occlusion; pbr_input.flags = mesh[in.instance_index].flags; - +#ifdef DEFERRED_PREPASS + pbr_functions::alpha_discard(pbr_bindings::material, output_color); + out.deferred = pbr_deferred_functions::deferred_gbuffer_from_pbr_input(pbr_input); + out.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; +#ifdef NORMAL_PREPASS + out.normal = vec4(pbr_input.N * 0.5 + vec3(0.5), 1.0); +#endif // NORMAL_PREPASS +#else // DEFERRED_PREPASS output_color = pbr_functions::pbr(pbr_input); - } else { - output_color = pbr_functions::alpha_discard(pbr_bindings::material, output_color); +#endif // DEFERRED_PREPASS + } else { // if UNLIT_BIT != 0 + pbr_functions::alpha_discard(pbr_bindings::material, output_color); +#ifdef DEFERRED_PREPASS + var pbr_input = pbr_types::pbr_input_new(); + pbr_input.flags = mesh[in.instance_index].flags; + pbr_input.material.flags = pbr_bindings::material.flags; + pbr_input.material.base_color = output_color; + pbr_input.world_position = in.world_position; + pbr_input.world_normal = in.world_normal; + pbr_input.frag_coord = in.position; + out.deferred = pbr_deferred_functions::deferred_gbuffer_from_pbr_input(pbr_input); + out.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; +#ifdef NORMAL_PREPASS + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +#endif +#endif // DEFERRED_PREPASS } +#ifdef DEFERRED_PREPASS +#ifdef MOTION_VECTOR_PREPASS + out.motion_vector = pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position); +#endif // MOTION_VECTOR_PREPASS + return out; +#else //DEFERRED_PREPASS // fog if (fog.mode != FOG_MODE_OFF && (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) { output_color = pbr_functions::apply_fog(fog, output_color, in.world_position.xyz, view.world_position.xyz); @@ -165,5 +216,7 @@ fn fragment( #ifdef PREMULTIPLY_ALPHA output_color = pbr_functions::premultiply_alpha(pbr_bindings::material.flags, output_color); #endif + return output_color; +#endif //DEFERRED_PREPASS } diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 6677eb70e59066..a17457c5058f68 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -17,7 +17,6 @@ #import bevy_pbr::environment_map #endif -#import bevy_pbr::mesh_bindings mesh #import bevy_pbr::mesh_types MESH_FLAGS_SHADOW_RECEIVER_BIT fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) -> vec4 { @@ -137,47 +136,9 @@ fn calculate_view( return V; } -struct PbrInput { - material: pbr_types::StandardMaterial, - occlusion: vec3, - frag_coord: vec4, - world_position: vec4, - // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow - // mapping - world_normal: vec3, - // Normalized normal-mapped world normal used for lighting - N: vec3, - // Normalized view vector in world space, pointing from the fragment world position toward the - // view world position - V: vec3, - is_orthographic: bool, - flags: u32, -}; - -// Creates a PbrInput with default values -fn pbr_input_new() -> PbrInput { - var pbr_input: PbrInput; - - pbr_input.material = pbr_types::standard_material_new(); - pbr_input.occlusion = vec3(1.0); - - pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); - pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); - pbr_input.world_normal = vec3(0.0, 0.0, 1.0); - - pbr_input.is_orthographic = false; - - pbr_input.N = vec3(0.0, 0.0, 1.0); - pbr_input.V = vec3(1.0, 0.0, 0.0); - - pbr_input.flags = 0u; - - return pbr_input; -} - #ifndef PREPASS_FRAGMENT fn pbr( - in: PbrInput, + in: pbr_types::PbrInput, ) -> vec4 { var output_color: vec4 = in.material.base_color; diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl index 542e540309a5c3..c1c05df25ecd7b 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -1,87 +1,22 @@ -#import bevy_pbr::prepass_bindings +#import bevy_pbr::pbr_prepass_functions #import bevy_pbr::pbr_bindings #import bevy_pbr::pbr_types #ifdef NORMAL_PREPASS #import bevy_pbr::pbr_functions #endif // NORMAL_PREPASS -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 - -#ifdef MOTION_VECTOR_PREPASS - @location(3) world_position: vec4, - @location(4) previous_world_position: vec4, -#endif // MOTION_VECTOR_PREPASS - -#ifdef DEPTH_CLAMP_ORTHO - @location(5) clip_position_unclamped: vec4, -#endif // DEPTH_CLAMP_ORTHO -}; - -// Cutoff used for the premultiplied alpha modes BLEND and ADD. -const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; - -// 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 MAY_DISCARD - var output_color: vec4 = bevy_pbr::pbr_bindings::material.base_color; - -#ifdef VERTEX_UVS - if (bevy_pbr::pbr_bindings::material.flags & bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { - output_color = output_color * textureSampleBias(bevy_pbr::pbr_bindings::base_color_texture, bevy_pbr::pbr_bindings::base_color_sampler, in.uv, bevy_pbr::prepass_bindings::view.mip_bias); - } -#endif // VERTEX_UVS - - let alpha_mode = bevy_pbr::pbr_bindings::material.flags & bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; - if alpha_mode == bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { - if output_color.a < bevy_pbr::pbr_bindings::material.alpha_cutoff { - discard; - } - } else if (alpha_mode == bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || alpha_mode == bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD) { - if output_color.a < PREMULTIPLIED_ALPHA_CUTOFF { - discard; - } - } else if alpha_mode == bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED { - if all(output_color < vec4(PREMULTIPLIED_ALPHA_CUTOFF)) { - discard; - } - } - -#endif // MAY_DISCARD -} - +#import bevy_pbr::prepass_io as prepass_io +#import bevy_pbr::mesh_view_bindings view + #ifdef PREPASS_FRAGMENT -struct FragmentOutput { -#ifdef NORMAL_PREPASS - @location(0) normal: vec4, -#endif // NORMAL_PREPASS - -#ifdef MOTION_VECTOR_PREPASS - @location(1) motion_vector: vec2, -#endif // MOTION_VECTOR_PREPASS - -#ifdef DEPTH_CLAMP_ORTHO - @builtin(frag_depth) frag_depth: f32, -#endif // DEPTH_CLAMP_ORTHO -} - @fragment -fn fragment(in: FragmentInput) -> FragmentOutput { - prepass_alpha_discard(in); +fn fragment( + in: prepass_io::FragmentInput, + @builtin(front_facing) is_front: bool, +) -> prepass_io::FragmentOutput { + bevy_pbr::pbr_prepass_functions::prepass_alpha_discard(in); - var out: FragmentOutput; + var out: prepass_io::FragmentOutput; #ifdef DEPTH_CLAMP_ORTHO out.frag_depth = in.clip_position_unclamped.z; @@ -93,7 +28,7 @@ fn fragment(in: FragmentInput) -> FragmentOutput { let world_normal = bevy_pbr::pbr_functions::prepare_world_normal( in.world_normal, (bevy_pbr::pbr_bindings::material.flags & bevy_pbr::pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u, - in.is_front, + is_front, ); let normal = bevy_pbr::pbr_functions::apply_normal_mapping( @@ -107,7 +42,7 @@ fn fragment(in: FragmentInput) -> FragmentOutput { #ifdef VERTEX_UVS in.uv, #endif // VERTEX_UVS - bevy_pbr::prepass_bindings::view.mip_bias, + view.mip_bias, ); out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); @@ -117,24 +52,14 @@ fn fragment(in: FragmentInput) -> FragmentOutput { #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS - let clip_position_t = bevy_pbr::prepass_bindings::view.unjittered_view_proj * in.world_position; - let clip_position = clip_position_t.xy / clip_position_t.w; - let previous_clip_position_t = bevy_pbr::prepass_bindings::previous_view_proj * in.previous_world_position; - let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; - // These motion vectors are used as offsets to UV positions and are stored - // in the range -1,1 to allow offsetting from the one corner to the - // diagonally-opposite corner in UV coordinates, in either direction. - // A difference between diagonally-opposite corners of clip space is in the - // range -2,2, so this needs to be scaled by 0.5. And the V direction goes - // down where clip space y goes up, so y needs to be flipped. - out.motion_vector = (clip_position - previous_clip_position) * vec2(0.5, -0.5); -#endif // MOTION_VECTOR_PREPASS + out.motion_vector = bevy_pbr::pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position); +#endif return out; } #else @fragment -fn fragment(in: FragmentInput) { - prepass_alpha_discard(in); +fn fragment(in: prepass_io::FragmentInput) { + bevy_pbr::pbr_prepass_functions::prepass_alpha_discard(in); } #endif // PREPASS_FRAGMENT diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl new file mode 100644 index 00000000000000..88fb2316e89daf --- /dev/null +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -0,0 +1,57 @@ +#define_import_path bevy_pbr::pbr_prepass_functions +#import bevy_pbr::prepass_io as prepass_io +#import bevy_pbr::prepass_bindings previous_view_proj +#import bevy_pbr::mesh_view_bindings view + +#import bevy_pbr::pbr_bindings as pbr_bindings +#import bevy_pbr::pbr_types as pbr_types + + +// Cutoff used for the premultiplied alpha modes BLEND and ADD. +const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; + +// We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff +fn prepass_alpha_discard(in: prepass_io::FragmentInput) { + +#ifdef MAY_DISCARD + var output_color: vec4 = pbr_bindings::material.base_color; + +#ifdef VERTEX_UVS + if (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { + output_color = output_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, in.uv, view.mip_bias); + } +#endif // VERTEX_UVS + + let alpha_mode = pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { + if output_color.a < pbr_bindings::material.alpha_cutoff { + discard; + } + } else if (alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD) { + if output_color.a < PREMULTIPLIED_ALPHA_CUTOFF { + discard; + } + } else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED { + if all(output_color < vec4(PREMULTIPLIED_ALPHA_CUTOFF)) { + discard; + } + } + +#endif // MAY_DISCARD +} + +#ifdef MOTION_VECTOR_PREPASS +fn calculate_motion_vector(world_position: vec4, previous_world_position: vec4) -> vec2 { + let clip_position_t = view.unjittered_view_proj * world_position; + let clip_position = clip_position_t.xy / clip_position_t.w; + let previous_clip_position_t = previous_view_proj * previous_world_position; + let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; + // These motion vectors are used as offsets to UV positions and are stored + // in the range -1,1 to allow offsetting from the one corner to the + // diagonally-opposite corner in UV coordinates, in either direction. + // A difference between diagonally-opposite corners of clip space is in the + // range -2,2, so this needs to be scaled by 0.5. And the V direction goes + // down where clip space y goes up, so y needs to be flipped. + return (clip_position - previous_clip_position) * vec2(0.5, -0.5); +} +#endif // MOTION_VECTOR_PREPASS \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 85cbed505fb273..c9041fe017a9b6 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -12,8 +12,14 @@ struct StandardMaterial { parallax_depth_scale: f32, max_parallax_layer_count: f32, max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + deferred_lighting_pass_id: u32, }; +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: if these flags are updated or changed. Be sure to also update +// deferred_flags_from_mesh_material_flags and mesh_material_flags_from_deferred_flags +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u; const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u; const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u; @@ -34,6 +40,7 @@ const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 2684354560u; // ↑ To calculate/verify the values above, use the following playground: // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7792f8dd6fc6a8d4d0b6b1776898a7f4 + // Creates a StandardMaterial with default values fn standard_material_new() -> StandardMaterial { var material: StandardMaterial; @@ -49,6 +56,45 @@ fn standard_material_new() -> StandardMaterial { material.parallax_depth_scale = 0.1; material.max_parallax_layer_count = 16.0; material.max_relief_mapping_search_steps = 5u; - + material.deferred_lighting_pass_id = 1u; + return material; } + +struct PbrInput { + material: StandardMaterial, + occlusion: vec3, + frag_coord: vec4, + world_position: vec4, + // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow + // mapping + world_normal: vec3, + // Normalized normal-mapped world normal used for lighting + N: vec3, + // Normalized view vector in world space, pointing from the fragment world position toward the + // view world position + V: vec3, + is_orthographic: bool, + flags: u32, +}; + +// Creates a PbrInput with default values +fn pbr_input_new() -> PbrInput { + var pbr_input: PbrInput; + + pbr_input.material = standard_material_new(); + pbr_input.occlusion = vec3(1.0); + + pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_normal = vec3(0.0, 0.0, 1.0); + + pbr_input.is_orthographic = false; + + pbr_input.N = vec3(0.0, 0.0, 1.0); + pbr_input.V = vec3(1.0, 0.0, 0.0); + + pbr_input.flags = 0u; + + return pbr_input; +} diff --git a/crates/bevy_pbr/src/render/rgb9e5.wgsl b/crates/bevy_pbr/src/render/rgb9e5.wgsl new file mode 100644 index 00000000000000..c635c83dfcc4c8 --- /dev/null +++ b/crates/bevy_pbr/src/render/rgb9e5.wgsl @@ -0,0 +1,63 @@ +#define_import_path bevy_pbr::rgb9e5 + +const RGB9E5_EXPONENT_BITS = 5u; +const RGB9E5_MANTISSA_BITS = 9; +const RGB9E5_MANTISSA_BITSU = 9u; +const RGB9E5_EXP_BIAS = 15; +const RGB9E5_MAX_VALID_BIASED_EXP = 31u; + +//#define MAX_RGB9E5_EXP (RGB9E5_MAX_VALID_BIASED_EXP - RGB9E5_EXP_BIAS) +//#define RGB9E5_MANTISSA_VALUES (1< i32 { + let f = bitcast(x); + let biasedexponent = (f & 0x7F800000u) >> 23u; + return i32(biasedexponent) - 127; +} + +// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_shared_exponent.txt +fn vec3_to_rgb9e5_(rgb_in: vec3) -> u32 { + let rgb = clamp(rgb_in, vec3(0.0), vec3(MAX_RGB9E5_)); + + let maxrgb = max(rgb.r, max(rgb.g, rgb.b)); + var exp_shared = max(-RGB9E5_EXP_BIAS - 1, floor_log2_(maxrgb)) + 1 + RGB9E5_EXP_BIAS; + var denom = exp2(f32(exp_shared - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS)); + + let maxm = i32(floor(maxrgb / denom + 0.5)); + if (maxm == RGB9E5_MANTISSA_VALUES) { + denom *= 2.0; + exp_shared += 1; + } + + let n = vec3(floor(rgb / denom + 0.5)); + + return (u32(exp_shared) << 27u) | (n.b << 18u) | (n.g << 9u) | (n.r << 0u); +} + +// Builtin extractBits() is not working on WEBGL or DX12 +// DX12: HLSL: Unimplemented("write_expr_math ExtractBits") +fn extract_bits(value: u32, offset: u32, bits: u32) -> u32 { + let mask = (1u << bits) - 1u; + return (value >> offset) & mask; +} + +fn rgb9e5_to_vec3_(v: u32) -> vec3 { + let exponent = i32(extract_bits(v, 27u, RGB9E5_EXPONENT_BITS)) - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS; + let scale = exp2(f32(exponent)); + + return vec3( + f32(extract_bits(v, 0u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 9u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 18u, RGB9E5_MANTISSA_BITSU)) + ) * scale; +} diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 6c8a87a5a43efa..89592f89cbf5ed 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -1,4 +1,5 @@ #define_import_path bevy_pbr::utils +#import bevy_pbr::rgb9e5 const PI: f32 = 3.141592653589793; const HALF_PI: f32 = 1.57079632679; @@ -27,3 +28,23 @@ fn random1D(s: f32) -> f32 { fn coords_to_viewport_uv(position: vec2, viewport: vec4) -> vec2 { return (position - viewport.xy) / viewport.zw; } + +// https://jcgt.org/published/0003/02/01/paper.pdf + +// For encoding normals or unit direction vectors as octahedral coordinates. +fn octahedral_encode(v: vec3) -> vec2 { + var n = v / (abs(v.x) + abs(v.y) + abs(v.z)); + let octahedral_wrap = (1.0 - abs(n.yx)) * select(vec2(-1.0), vec2(1.0), n.xy > 0.0); + let n_xy = select(octahedral_wrap, n.xy, n.z >= 0.0); + return n_xy * 0.5 + 0.5; +} + +// For decoding normals or unit direction vectors from octahedral coordinates. +fn octahedral_decode(v: vec2) -> vec3 { + let f = v * 2.0 - 1.0; + var n = vec3(f.xy, 1.0 - abs(f.x) - abs(f.y)); + let t = saturate(-n.z); + let w = select(vec2(t), vec2(-t), n.xy >= vec2(0.0)); + n = vec3(n.xy + w, n.z); + return normalize(n); +} diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 3a31c0afbe6209..eaabea3772654a 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -127,8 +127,8 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { .add_render_graph_edges( CORE_3D, &[ - // PREPASS -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS - bevy_core_pipeline::core_3d::graph::node::PREPASS, + // END_PRE_PASSES -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS + bevy_core_pipeline::core_3d::graph::node::END_PREPASSES, draw_3d_graph::node::SCREEN_SPACE_AMBIENT_OCCLUSION, bevy_core_pipeline::core_3d::graph::node::START_MAIN_PASS, ], @@ -797,6 +797,7 @@ fn prepare_ssao_bind_groups( mip_level_count: Some(1), ..default() }; + let preprocess_depth_bind_group = render_device.create_bind_group(&BindGroupDescriptor { label: Some("ssao_preprocess_depth_bind_group"), layout: &pipelines.preprocess_depth_bind_group_layout, diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index 24e91bc224dcc4..4075963a614a21 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -61,7 +61,6 @@ fn fallback_image_new( value: u8, ) -> GpuImage { // TODO make this configurable per channel - let data = vec![value; format.pixel_size()]; let extents = Extent3d { width: 1, @@ -72,19 +71,29 @@ fn fallback_image_new( }, }; - let image_dimension = dimension.compatible_texture_dimension(); + // We can't create textures with data when it's a depth texture or when using multiple samples + let create_texture_with_data = !format.is_depth_stencil_format() && samples == 1; - let mut image = Image::new_fill(extents, image_dimension, &data, format); + let image_dimension = dimension.compatible_texture_dimension(); + let mut image = if create_texture_with_data { + let data = vec![value; format.pixel_size()]; + Image::new_fill(extents, image_dimension, &data, format) + } else { + let mut image = Image::default(); + image.texture_descriptor.dimension = TextureDimension::D2; + image.texture_descriptor.size = extents; + image.texture_descriptor.format = format; + image + }; image.texture_descriptor.sample_count = samples; if image_dimension == TextureDimension::D2 { 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.is_depth_stencil_format() || samples > 1 { - render_device.create_texture(&image.texture_descriptor) - } else { + let texture = if create_texture_with_data { render_device.create_texture_with_data(render_queue, &image.texture_descriptor, &image.data) + } else { + render_device.create_texture(&image.texture_descriptor) }; let texture_view = texture.create_view(&TextureViewDescriptor { @@ -207,64 +216,30 @@ impl FromWorld for FallbackImageCubemap { } } -// TODO these could be combined in one FallbackImage cache. - -/// A Cache of fallback textures that uses the sample count as a key +/// A Cache of fallback textures that uses the sample count and `TextureFormat` 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 -/// Depth 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(), - TextureViewDimension::D2, - sample_count, - 255, - ) - }) - } -} +pub struct FallbackImageFormatMsaaCache(HashMap<(u32, TextureFormat), GpuImage>); #[derive(SystemParam)] -pub struct FallbackImagesDepth<'w> { - cache: ResMut<'w, FallbackImageDepthCache>, +pub struct FallbackImageMsaa<'w> { + cache: ResMut<'w, FallbackImageFormatMsaaCache>, 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(|| { +impl<'w> FallbackImageMsaa<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32, format: TextureFormat) -> &GpuImage { + self.cache.entry((sample_count, format)).or_insert_with(|| { fallback_image_new( &self.render_device, &self.render_queue, &self.default_sampler, - TextureFormat::Depth32Float, + format, TextureViewDimension::D2, sample_count, 255, diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 7f08ec46c93a3b..731da8ff6f63b3 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -144,8 +144,7 @@ impl Plugin for ImagePlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .init_resource::(); + .init_resource::(); } } } diff --git a/examples/3d/deferred_rendering.rs b/examples/3d/deferred_rendering.rs new file mode 100644 index 00000000000000..951f9b0f6c1ee9 --- /dev/null +++ b/examples/3d/deferred_rendering.rs @@ -0,0 +1,428 @@ +//! This example compares Forward, Forward + Prepass, and Deferred rendering. + +use std::f32::consts::*; + +use bevy::{ + core_pipeline::{ + fxaa::Fxaa, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + }, + pbr::NotShadowReceiver, + pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap}, + pbr::{DefaultOpaqueRendererMethod, NotShadowCaster, OpaqueRendererMethod}, + prelude::*, + render::render_resource::TextureFormat, +}; + +fn main() { + App::new() + .insert_resource(Msaa::Off) + .insert_resource(DefaultOpaqueRendererMethod::deferred()) + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1.0 / 5.0f32, + }) + .insert_resource(DirectionalLightShadowMap { size: 4096 }) + .add_plugins(DefaultPlugins) + .insert_resource(Normal(None)) + .insert_resource(Pause(true)) + .add_systems(Startup, (setup, setup_parallax)) + .add_systems( + Update, + (animate_light_direction, switch_mode, spin, update_normal), + ) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + commands.spawn(( + Camera3dBundle { + camera: Camera { + // Deferred both supports both hdr: true and hdr: false + hdr: false, + ..default() + }, + transform: Transform::from_xyz(0.7, 0.7, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + ..default() + }, + FogSettings { + color: Color::rgba(0.05, 0.05, 0.05, 1.0), + falloff: FogFalloff::Linear { + start: 1.0, + end: 8.0, + }, + ..default() + }, + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + }, + DepthPrepass, + MotionVectorPrepass, + DeferredPrepass, + Fxaa::default(), + )); + + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + shadows_enabled: true, + ..default() + }, + cascade_shadow_config: CascadeShadowConfigBuilder { + num_cascades: 3, + maximum_distance: 10.0, + ..default() + } + .into(), + transform: Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 0.0, -FRAC_PI_4)), + ..default() + }); + + // FlightHelmet + let helmet_scene = asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"); + + commands.spawn(SceneBundle { + scene: helmet_scene.clone(), + ..default() + }); + commands.spawn(SceneBundle { + scene: helmet_scene, + transform: Transform::from_xyz(-3.0, 0.0, -3.0), + ..default() + }); + + let mut forward_mat: StandardMaterial = Color::rgb(0.1, 0.2, 0.1).into(); + forward_mat.opaque_render_method = OpaqueRendererMethod::Forward; + let forward_mat_h = materials.add(forward_mat); + + // Plane + commands.spawn(PbrBundle { + mesh: meshes.add(shape::Plane::from_size(50.0).into()), + material: forward_mat_h.clone(), + ..default() + }); + + let cube_h = meshes.add(Mesh::from(shape::Cube { size: 0.1 })); + let sphere_h = meshes.add(Mesh::from(shape::UVSphere { + radius: 0.125, + sectors: 128, + stacks: 128, + })); + + // Cubes + commands.spawn(PbrBundle { + mesh: cube_h.clone(), + material: forward_mat_h.clone(), + transform: Transform::from_xyz(-0.3, 0.5, -0.2), + ..default() + }); + commands.spawn(PbrBundle { + mesh: cube_h, + material: forward_mat_h, + transform: Transform::from_xyz(0.2, 0.5, 0.2), + ..default() + }); + + let sphere_color = Color::rgb(10.0, 4.0, 1.0); + let sphere_pos = Transform::from_xyz(0.4, 0.5, -0.8); + // Emissive sphere + let mut unlit_mat: StandardMaterial = sphere_color.into(); + unlit_mat.unlit = true; + commands.spawn(( + PbrBundle { + mesh: sphere_h.clone(), + material: materials.add(unlit_mat), + transform: sphere_pos, + ..default() + }, + NotShadowCaster, + )); + // Light + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 1.0, + radius: 0.125, + shadows_enabled: true, + color: sphere_color, + ..default() + }, + transform: sphere_pos, + ..default() + }); + + // Spheres + for i in 0..6 { + let j = i % 3; + let s_val = if i < 3 { 0.0 } else { 0.2 }; + let material = if j == 0 { + materials.add(StandardMaterial { + base_color: Color::rgb(s_val, s_val, 1.0), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + } else if j == 1 { + materials.add(StandardMaterial { + base_color: Color::rgb(s_val, 1.0, s_val), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + } else { + materials.add(StandardMaterial { + base_color: Color::rgb(1.0, s_val, s_val), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + }; + commands.spawn(PbrBundle { + mesh: sphere_h.clone(), + material, + transform: Transform::from_xyz( + j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } - 0.4, + 0.125, + -j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } + 0.4, + ), + ..default() + }); + } + + // sky + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box::default())), + material: materials.add(StandardMaterial { + base_color: Color::hex("888888").unwrap(), + unlit: true, + cull_mode: None, + ..default() + }), + transform: Transform::from_scale(Vec3::splat(1_000_000.0)), + ..default() + }, + NotShadowCaster, + NotShadowReceiver, + )); + + // Example instructions + commands.spawn( + TextBundle::from_section( + "", + TextStyle { + font_size: 18.0, + color: Color::WHITE, + ..default() + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +#[derive(Resource)] +struct Pause(bool); + +fn animate_light_direction( + time: Res