use alloc::borrow::Cow; use bevy_asset::{Asset, Handle}; use bevy_ecs::system::SystemParamItem; use bevy_platform_support::{collections::HashSet, hash::FixedHasher}; use bevy_reflect::{impl_type_path, Reflect}; use bevy_render::{ alpha::AlphaMode, mesh::MeshVertexBufferLayoutRef, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, }; use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey}; pub struct MaterialExtensionPipeline { pub mesh_pipeline: MeshPipeline, pub material_layout: BindGroupLayout, pub vertex_shader: Option>, pub fragment_shader: Option>, pub bindless: bool, } pub struct MaterialExtensionKey { pub mesh_key: MeshPipelineKey, pub bind_group_data: E::Data, } /// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`. /// /// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct. pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader /// will be used. fn vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader /// will be used. fn fragment_shader() -> ShaderRef { ShaderRef::Default } // Returns this material’s AlphaMode. If None is returned, the base material alpha mode will be used. fn alpha_mode() -> Option { None } /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader /// will be used. fn prepass_vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader /// will be used. fn prepass_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader /// will be used. fn deferred_vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader /// will be used. fn deferred_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh fragment shader will be used. #[cfg(feature = "meshlet")] fn meshlet_mesh_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh prepass fragment shader will be used. #[cfg(feature = "meshlet")] fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh deferred fragment shader will be used. #[cfg(feature = "meshlet")] fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. /// Specialization for the base material is applied before this function is called. #[expect( unused_variables, reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." )] #[inline] fn specialize( pipeline: &MaterialExtensionPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialExtensionKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } /// A material that extends a base [`Material`] with additional shaders and data. /// /// The data from both materials will be combined and made available to the shader /// so that shader functions built for the base material (and referencing the base material /// bindings) will work as expected, and custom alterations based on custom data can also be used. /// /// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base /// material's vertex shader. /// /// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base /// fragment shader. /// /// When used with `StandardMaterial` as the base, all the standard material fields are /// present, so the `pbr_fragment` shader functions can be called from the extension shader (see /// the `extended_material` example). #[derive(Asset, Clone, Debug, Reflect)] #[reflect(type_path = false)] #[reflect(Clone)] pub struct ExtendedMaterial { pub base: B, pub extension: E, } impl Default for ExtendedMaterial where B: Material + Default, E: MaterialExtension + Default, { fn default() -> Self { Self { base: B::default(), extension: E::default(), } } } // We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` // causes the `TypePath` derive to not generate an implementation. impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); impl AsBindGroup for ExtendedMaterial { type Data = (::Data, ::Data); type Param = (::Param, ::Param); fn bindless_slot_count() -> Option { // We only enable bindless if both the base material and its extension // are bindless. If we do enable bindless, we choose the smaller of the // two slab size limits. match (B::bindless_slot_count()?, E::bindless_slot_count()?) { (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Auto) => { Some(BindlessSlabResourceLimit::Auto) } (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Custom(limit)) | (BindlessSlabResourceLimit::Custom(limit), BindlessSlabResourceLimit::Auto) => { Some(BindlessSlabResourceLimit::Custom(limit)) } ( BindlessSlabResourceLimit::Custom(base_limit), BindlessSlabResourceLimit::Custom(extended_limit), ) => Some(BindlessSlabResourceLimit::Custom( base_limit.min(extended_limit), )), } } fn unprepared_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, mut force_non_bindless: bool, ) -> Result, AsBindGroupError> { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); // add together the bindings of the base material and the user material let UnpreparedBindGroup { mut bindings, data: base_data, } = B::unprepared_bind_group( &self.base, layout, render_device, base_param, force_non_bindless, )?; let extended_bindgroup = E::unprepared_bind_group( &self.extension, layout, render_device, extended_param, force_non_bindless, )?; bindings.extend(extended_bindgroup.bindings.0); Ok(UnpreparedBindGroup { bindings, data: (base_data, extended_bindgroup.data), }) } fn bind_group_layout_entries( render_device: &RenderDevice, mut force_non_bindless: bool, ) -> Vec where Self: Sized, { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); // Add together the bindings of the standard material and the user // material, skipping duplicate bindings. Duplicate bindings will occur // when bindless mode is on, because of the common bindless resource // arrays, and we need to eliminate the duplicates or `wgpu` will // complain. let mut entries = vec![]; let mut seen_bindings = HashSet::<_>::with_hasher(FixedHasher); for entry in B::bind_group_layout_entries(render_device, force_non_bindless) .into_iter() .chain(E::bind_group_layout_entries(render_device, force_non_bindless).into_iter()) { if seen_bindings.insert(entry.binding) { entries.push(entry); } } entries } fn bindless_descriptor() -> Option { // We're going to combine the two bindless descriptors. let base_bindless_descriptor = B::bindless_descriptor()?; let extended_bindless_descriptor = E::bindless_descriptor()?; // Combining the buffers and index tables is straightforward. let mut buffers = base_bindless_descriptor.buffers.to_vec(); let mut index_tables = base_bindless_descriptor.index_tables.to_vec(); buffers.extend(extended_bindless_descriptor.buffers.iter().cloned()); index_tables.extend(extended_bindless_descriptor.index_tables.iter().cloned()); // Combining the resources is a little trickier because the resource // array is indexed by bindless index, so we have to merge the two // arrays, not just concatenate them. let max_bindless_index = base_bindless_descriptor .resources .len() .max(extended_bindless_descriptor.resources.len()); let mut resources = Vec::with_capacity(max_bindless_index); for bindless_index in 0..max_bindless_index { // In the event of a conflicting bindless index, we choose the // base's binding. match base_bindless_descriptor.resources.get(bindless_index) { None | Some(&BindlessResourceType::None) => resources.push( extended_bindless_descriptor .resources .get(bindless_index) .copied() .unwrap_or(BindlessResourceType::None), ), Some(&resource_type) => resources.push(resource_type), } } Some(BindlessDescriptor { resources: Cow::Owned(resources), buffers: Cow::Owned(buffers), index_tables: Cow::Owned(index_tables), }) } } impl Material for ExtendedMaterial { fn vertex_shader() -> ShaderRef { match E::vertex_shader() { ShaderRef::Default => B::vertex_shader(), specified => specified, } } fn fragment_shader() -> ShaderRef { match E::fragment_shader() { ShaderRef::Default => B::fragment_shader(), specified => specified, } } fn alpha_mode(&self) -> AlphaMode { match E::alpha_mode() { Some(specified) => specified, None => B::alpha_mode(&self.base), } } fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { B::opaque_render_method(&self.base) } fn depth_bias(&self) -> f32 { B::depth_bias(&self.base) } fn reads_view_transmission_texture(&self) -> bool { B::reads_view_transmission_texture(&self.base) } fn prepass_vertex_shader() -> ShaderRef { match E::prepass_vertex_shader() { ShaderRef::Default => B::prepass_vertex_shader(), specified => specified, } } fn prepass_fragment_shader() -> ShaderRef { match E::prepass_fragment_shader() { ShaderRef::Default => B::prepass_fragment_shader(), specified => specified, } } fn deferred_vertex_shader() -> ShaderRef { match E::deferred_vertex_shader() { ShaderRef::Default => B::deferred_vertex_shader(), specified => specified, } } fn deferred_fragment_shader() -> ShaderRef { match E::deferred_fragment_shader() { ShaderRef::Default => B::deferred_fragment_shader(), specified => specified, } } #[cfg(feature = "meshlet")] fn meshlet_mesh_fragment_shader() -> ShaderRef { match E::meshlet_mesh_fragment_shader() { ShaderRef::Default => B::meshlet_mesh_fragment_shader(), specified => specified, } } #[cfg(feature = "meshlet")] fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { match E::meshlet_mesh_prepass_fragment_shader() { ShaderRef::Default => B::meshlet_mesh_prepass_fragment_shader(), specified => specified, } } #[cfg(feature = "meshlet")] fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { match E::meshlet_mesh_deferred_fragment_shader() { ShaderRef::Default => B::meshlet_mesh_deferred_fragment_shader(), specified => specified, } } fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { // Call the base material's specialize function let MaterialPipeline:: { mesh_pipeline, material_layout, vertex_shader, fragment_shader, bindless, .. } = pipeline.clone(); let base_pipeline = MaterialPipeline:: { mesh_pipeline, material_layout, vertex_shader, fragment_shader, bindless, marker: Default::default(), }; let base_key = MaterialPipelineKey:: { mesh_key: key.mesh_key, bind_group_data: key.bind_group_data.0, }; B::specialize(&base_pipeline, descriptor, layout, base_key)?; // Call the extended material's specialize function afterwards let MaterialPipeline:: { mesh_pipeline, material_layout, vertex_shader, fragment_shader, bindless, .. } = pipeline.clone(); E::specialize( &MaterialExtensionPipeline { mesh_pipeline, material_layout, vertex_shader, fragment_shader, bindless, }, descriptor, layout, MaterialExtensionKey { mesh_key: key.mesh_key, bind_group_data: key.bind_group_data.1, }, ) } }