diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index a80a6d5af5503..6075aca6cca97 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -67,6 +67,14 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { ShaderRef::Default } + /// Specifies the shadow batch key (see [`Material::shadow_material_key`]). By default, extended materials + /// do not batch for shadows. + /// If the extension doesn't modify vertices or apply discards based on extended material properties, you + /// may be able to improve shadow performance by returning the base material key. + fn shadow_material_key(&self, _base_key: Option) -> Option { + None + } + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input. /// Specialization for the base material is applied before this function is called. @@ -211,6 +219,11 @@ impl Material for ExtendedMaterial { } } + fn shadow_material_key(&self) -> Option { + self.extension + .shadow_material_key(self.base.shadow_material_key()) + } + fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 8e2b3bd71d91b..ba0150f79d8e3 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -180,6 +180,13 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } + + /// Specify a batch key to use for shadows. Returning Some(value) allows batching of distinct materials which will produce + /// the same depth-prepass output (such as opaque [`StandardMaterial`]s with different textures), rendering with the first + /// material of the batch for all meshes. + fn shadow_material_key(&self) -> Option { + None + } } /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] @@ -773,16 +780,33 @@ pub struct MaterialProperties { pub struct PreparedMaterial { pub bindings: Vec<(u32, OwnedBindingResource)>, pub bind_group: BindGroup, + pub shadow_bind_group_id: MaterialBindGroupId, pub key: T::Data, pub properties: MaterialProperties, } -#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] -pub struct MaterialBindGroupId(Option); +#[derive(Component, Clone, Copy, Default)] +pub enum MaterialBindGroupId { + #[default] + Unbatched, + Instance(BindGroupId), + Group(u64), +} + +impl PartialEq for MaterialBindGroupId { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Instance(l0), Self::Instance(r0)) => l0 == r0, + (Self::Group(l0), Self::Group(r0)) => l0 == r0, + // unbatched should never report equal to prevent batching + _ => false, + } + } +} impl PreparedMaterial { pub fn get_bind_group_id(&self) -> MaterialBindGroupId { - MaterialBindGroupId(Some(self.bind_group.id())) + MaterialBindGroupId::Instance(self.bind_group.id()) } } @@ -937,9 +961,14 @@ fn prepare_material( OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, OpaqueRendererMethod::Auto => default_opaque_render_method, }; + let shadow_bind_group_id = match material.shadow_material_key() { + Some(key) => MaterialBindGroupId::Group(key), + None => MaterialBindGroupId::Instance(prepared.bind_group.id()), + }; Ok(PreparedMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, + shadow_bind_group_id, key: prepared.data, properties: MaterialProperties { alpha_mode: material.alpha_mode(), diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index ef106c010e793..24421364687d4 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -801,6 +801,11 @@ impl Material for StandardMaterial { PBR_SHADER_HANDLE.into() } + fn shadow_material_key(&self) -> Option { + // we can batch all pure opaque materials together for shadow rendering + (self.alpha_mode == AlphaMode::Opaque).then_some(0) + } + fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 8c2294c66c692..74208725c627b 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,3 +1,4 @@ +use bevy_asset::AssetId; 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}; @@ -1590,7 +1591,7 @@ pub fn queue_shadows( shadow_draw_functions: Res>, prepass_pipeline: Res>, render_meshes: Res>, - render_mesh_instances: Res, + mut render_mesh_instances: ResMut, render_materials: Res>, render_material_instances: Res>, mut pipelines: ResMut>>, @@ -1635,7 +1636,7 @@ pub fn queue_shadows( // NOTE: Lights with shadow mapping disabled will have no visible entities // so no meshes will be queued for entity in visible_entities.iter().copied() { - let Some(mesh_instance) = render_mesh_instances.get(&entity) else { + let Some(mesh_instance) = render_mesh_instances.get_mut(&entity) else { continue; }; if !mesh_instance.shadow_caster { @@ -1685,11 +1686,13 @@ pub fn queue_shadows( } }; + mesh_instance.shadow_material_bind_group_id = material.shadow_bind_group_id; + shadow_phase.add(Shadow { draw_function: draw_shadow_mesh, pipeline: pipeline_id, entity, - distance: 0.0, // TODO: sort front-to-back + asset_id: mesh_instance.mesh_asset_id, batch_range: 0..1, dynamic_offset: None, }); @@ -1699,7 +1702,7 @@ pub fn queue_shadows( } pub struct Shadow { - pub distance: f32, + pub asset_id: AssetId, pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, @@ -1708,7 +1711,7 @@ pub struct Shadow { } impl PhaseItem for Shadow { - type SortKey = usize; + type SortKey = (usize, AssetId); #[inline] fn entity(&self) -> Entity { @@ -1717,7 +1720,7 @@ impl PhaseItem for Shadow { #[inline] fn sort_key(&self) -> Self::SortKey { - self.pipeline.id() + (self.pipeline.id(), self.asset_id) } #[inline] @@ -1727,10 +1730,11 @@ impl PhaseItem for Shadow { #[inline] fn sort(items: &mut [Self]) { - // The shadow phase is sorted by pipeline id for performance reasons. + // The shadow phase is sorted by pipeline id and mesh id for performance reasons. // Grouping all draw commands using the same pipeline together performs - // better than rebinding everything at a high rate. - radsort::sort_by_key(items, |item| item.sort_key()); + // better than rebinding everything at a high rate, and grouping by mesh + // allows batching to reduce draw calls. + items.sort_unstable_by_key(Self::sort_key); } #[inline] diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 172a032b0401e..090f4911fbad5 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -145,7 +145,7 @@ 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::, batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, ) @@ -257,6 +257,7 @@ pub struct RenderMeshInstance { pub transforms: MeshTransforms, pub mesh_asset_id: AssetId, pub material_bind_group_id: MaterialBindGroupId, + pub shadow_material_bind_group_id: MaterialBindGroupId, pub shadow_caster: bool, pub automatic_batching: bool, } @@ -328,6 +329,7 @@ pub fn extract_meshes( transforms, shadow_caster: !not_shadow_caster, material_bind_group_id: MaterialBindGroupId::default(), + shadow_material_bind_group_id: MaterialBindGroupId::default(), automatic_batching: !no_automatic_batching, }, )); @@ -503,6 +505,36 @@ impl GetBatchData for MeshPipeline { } } +pub struct ShadowMeshPipeline; +impl GetBatchData for ShadowMeshPipeline { + type Param = (SRes, SRes); + // The material bind group ID, the mesh ID, and the lightmap ID, + // respectively. + type CompareData = (MaterialBindGroupId, AssetId, Option>); + + type BufferData = MeshUniform; + + fn get_batch_data( + (mesh_instances, lightmaps): &SystemParamItem, + entity: Entity, + ) -> Option<(Self::BufferData, Option)> { + let mesh_instance = mesh_instances.get(&entity)?; + let maybe_lightmap = lightmaps.render_lightmaps.get(&entity); + + Some(( + MeshUniform::new( + &mesh_instance.transforms, + maybe_lightmap.map(|lightmap| lightmap.uv_rect), + ), + mesh_instance.automatic_batching.then_some(( + mesh_instance.shadow_material_bind_group_id, + mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.image), + )), + )) + } +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index ebbee17032176..b1a4fd554dd08 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -41,6 +41,10 @@ struct Args { /// the number of different textures from which to randomly select the material base color. 0 means no textures. #[argh(option, default = "0")] material_texture_count: usize, + + /// enable cascaded shadow map + #[argh(switch)] + shadows: bool, } #[derive(Default, Clone)] @@ -185,7 +189,13 @@ fn setup( } } - commands.spawn(DirectionalLightBundle { ..default() }); + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + shadows_enabled: args.shadows, + ..default() + }, + ..default() + }); } fn init_textures(args: &Args, images: &mut Assets) -> Vec> {