use crate::material_bind_groups::{ FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, }; #[cfg(feature = "meshlet")] use crate::meshlet::{ prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, InstanceManager, }; use crate::*; use bevy_asset::prelude::AssetChanged; use bevy_asset::{Asset, AssetEvents, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; use bevy_core_pipeline::prepass::{AlphaMask3dPrepass, Opaque3dPrepass}; use bevy_core_pipeline::{ core_3d::{ AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, ScreenSpaceTransmissionQuality, Transmissive3d, Transparent3d, }, prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}, tonemapping::Tonemapping, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Tick; use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, system::{ lifetimeless::{SRes, SResMut}, SystemParamItem, }, }; use bevy_platform::collections::hash_map::Entry; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::extract_cameras; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; use bevy_render::render_asset::prepare_assets; use bevy_render::renderer::RenderQueue; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, extract_resource::ExtractResource, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::*, renderer::RenderDevice, sync_world::MainEntity, view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewVisibility}, Extract, }; use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; use bevy_utils::Parallel; use core::{hash::Hash, marker::PhantomData}; use tracing::error; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level /// way to render [`Mesh3d`] entities with custom shader logic. /// /// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. /// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. /// /// # Example /// /// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, /// check out the [`AsBindGroup`] documentation. /// /// ``` /// # use bevy_pbr::{Material, MeshMaterial3d}; /// # use bevy_ecs::prelude::*; /// # use bevy_image::Image; /// # use bevy_reflect::TypePath; /// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}}; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; /// # use bevy_math::primitives::Capsule3d; /// # /// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] /// pub struct CustomMaterial { /// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to /// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. /// #[uniform(0)] /// color: LinearRgba, /// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just /// // add the sampler attribute with a different binding index. /// #[texture(1)] /// #[sampler(2)] /// color_texture: Handle, /// } /// /// // All functions on `Material` have default impls. You only need to implement the /// // functions that are relevant for your material. /// impl Material for CustomMaterial { /// fn fragment_shader() -> ShaderRef { /// "shaders/custom_material.wgsl".into() /// } /// } /// /// // Spawn an entity with a mesh using `CustomMaterial`. /// fn setup( /// mut commands: Commands, /// mut meshes: ResMut>, /// mut materials: ResMut>, /// asset_server: Res /// ) { /// commands.spawn(( /// Mesh3d(meshes.add(Capsule3d::default())), /// MeshMaterial3d(materials.add(CustomMaterial { /// color: RED.into(), /// color_texture: asset_server.load("some_image.png"), /// })), /// )); /// } /// ``` /// /// In WGSL shaders, the material's binding would look like this: /// /// ```wgsl /// @group(2) @binding(0) var color: vec4; /// @group(2) @binding(1) var color_texture: texture_2d; /// @group(2) @binding(2) var color_sampler: sampler; /// ``` pub trait Material: Asset + AsBindGroup + Clone + Sized { /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader /// will be used. fn vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader /// will be used. fn fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. #[inline] fn alpha_mode(&self) -> AlphaMode { 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] 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 { 0.0 } #[inline] /// Returns whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). /// /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires /// rendering to take place in a separate [`Transmissive3d`] pass. fn reads_view_transmission_texture(&self) -> bool { false } /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader /// will be used. /// /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps /// required for shadow mapping. fn prepass_vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader /// will be used. /// /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps /// required for shadow mapping. fn prepass_fragment_shader() -> ShaderRef { 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. 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. /// /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. #[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. /// /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. #[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. /// /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. #[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. #[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: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. pub struct MaterialPlugin { /// Controls if the prepass is enabled for the Material. /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. /// /// When it is enabled, it will automatically add the [`PrepassPlugin`] /// required to make the prepass work on this Material. pub prepass_enabled: bool, /// Controls if shadows are enabled for the Material. pub shadows_enabled: bool, /// Debugging flags that can optionally be set when constructing the renderer. pub debug_flags: RenderDebugFlags, pub _marker: PhantomData, } impl Default for MaterialPlugin { fn default() -> Self { Self { prepass_enabled: true, shadows_enabled: true, debug_flags: RenderDebugFlags::default(), _marker: Default::default(), } } } impl Plugin for MaterialPlugin where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { app.init_asset::() .register_type::>() .init_resource::>() .add_plugins((RenderAssetPlugin::>::default(),)) .add_systems( PostUpdate, ( mark_meshes_as_changed_if_their_materials_changed::.ambiguous_with_all(), check_entities_needing_specialization::.after(AssetEvents), ) .after(mark_3d_meshes_as_changed_if_their_assets_changed), ); if self.shadows_enabled { app.add_systems( PostUpdate, check_light_entities_needing_specialization:: .after(check_entities_needing_specialization::), ); } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>>() .add_systems( ExtractSchedule, ( ( extract_mesh_materials::, early_sweep_material_instances::, ) .chain() .before(late_sweep_material_instances) .before(ExtractMeshesSet), extract_entities_needs_specialization::.after(extract_cameras), ), ) .add_systems( Render, ( specialize_material_meshes:: .in_set(RenderSet::PrepareMeshes) .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), queue_material_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>), ), ) .add_systems( Render, ( prepare_material_bind_groups::, write_material_bind_group_buffers::, ) .chain() .in_set(RenderSet::PrepareBindGroups) .after(prepare_assets::>), ); if self.shadows_enabled { render_app .init_resource::() .init_resource::() .init_resource::>() .add_systems( Render, ( check_views_lights_need_specialization.in_set(RenderSet::PrepareAssets), // specialize_shadows:: also needs to run after prepare_assets::>, // which is fine since ManageViews is after PrepareAssets specialize_shadows:: .in_set(RenderSet::ManageViews) .after(prepare_lights), queue_shadows:: .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>), ), ); } #[cfg(feature = "meshlet")] render_app.add_systems( Render, queue_material_meshlet_meshes:: .in_set(RenderSet::QueueMeshes) .run_if(resource_exists::), ); #[cfg(feature = "meshlet")] render_app.add_systems( Render, prepare_material_meshlet_meshes_main_opaque_pass:: .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>) .before(queue_material_meshlet_meshes::) .run_if(resource_exists::), ); } if self.shadows_enabled || self.prepass_enabled { // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin app.add_plugins(PrepassPipelinePlugin::::default()); } if self.prepass_enabled { app.add_plugins(PrepassPlugin::::new(self.debug_flags)); } } fn finish(&self, app: &mut App) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() .init_resource::>(); } } } /// A dummy [`AssetId`] that we use as a placeholder whenever a mesh doesn't /// have a material. /// /// See the comments in [`RenderMaterialInstances::mesh_material`] for more /// information. static DUMMY_MESH_MATERIAL: AssetId = AssetId::::invalid(); /// A key uniquely identifying a specialized [`MaterialPipeline`]. pub struct MaterialPipelineKey { pub mesh_key: MeshPipelineKey, pub bind_group_data: M::Data, } impl Eq for MaterialPipelineKey where M::Data: PartialEq {} impl PartialEq for MaterialPipelineKey where M::Data: PartialEq, { fn eq(&self, other: &Self) -> bool { self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data } } impl Clone for MaterialPipelineKey where M::Data: Clone, { fn clone(&self) -> Self { Self { mesh_key: self.mesh_key, bind_group_data: self.bind_group_data.clone(), } } } impl Hash for MaterialPipelineKey where M::Data: Hash, { fn hash(&self, state: &mut H) { self.mesh_key.hash(state); self.bind_group_data.hash(state); } } /// Render pipeline data for a given [`Material`]. #[derive(Resource)] pub struct MaterialPipeline { pub mesh_pipeline: MeshPipeline, pub material_layout: BindGroupLayout, pub vertex_shader: Option>, pub fragment_shader: Option>, /// Whether this material *actually* uses bindless resources, taking the /// platform support (or lack thereof) of bindless resources into account. pub bindless: bool, pub marker: PhantomData, } impl Clone for MaterialPipeline { fn clone(&self) -> Self { Self { mesh_pipeline: self.mesh_pipeline.clone(), material_layout: self.material_layout.clone(), vertex_shader: self.vertex_shader.clone(), fragment_shader: self.fragment_shader.clone(), bindless: self.bindless, marker: PhantomData, } } } impl SpecializedMeshPipeline for MaterialPipeline where M::Data: PartialEq + Eq + Hash + Clone, { type Key = MaterialPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?; if let Some(vertex_shader) = &self.vertex_shader { descriptor.vertex.shader = vertex_shader.clone(); } if let Some(fragment_shader) = &self.fragment_shader { descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } descriptor.layout.insert(2, self.material_layout.clone()); M::specialize(self, &mut descriptor, layout, key)?; // If bindless mode is on, add a `BINDLESS` define. if self.bindless { descriptor.vertex.shader_defs.push("BINDLESS".into()); if let Some(ref mut fragment) = descriptor.fragment { fragment.shader_defs.push("BINDLESS".into()); } } Ok(descriptor) } } impl FromWorld for MaterialPipeline { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); let render_device = world.resource::(); MaterialPipeline { mesh_pipeline: world.resource::().clone(), material_layout: M::bind_group_layout(render_device), vertex_shader: match M::vertex_shader() { ShaderRef::Default => None, ShaderRef::Handle(handle) => Some(handle), ShaderRef::Path(path) => Some(asset_server.load(path)), }, fragment_shader: match M::fragment_shader() { ShaderRef::Default => None, ShaderRef::Handle(handle) => Some(handle), ShaderRef::Path(path) => Some(asset_server.load(path)), }, bindless: material_uses_bindless_resources::(render_device), marker: PhantomData, } } } type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetMeshBindGroup<1>, SetMaterialBindGroup, DrawMesh, ); /// Sets the bind group for a given [`Material`] at the configured `I` index. pub struct SetMaterialBindGroup(PhantomData); impl RenderCommand

for SetMaterialBindGroup { type Param = ( SRes>>, SRes, SRes>, ); type ViewQuery = (); type ItemQuery = (); #[inline] fn render<'w>( item: &P, _view: (), _item_query: Option<()>, (materials, material_instances, material_bind_group_allocator): SystemParamItem< 'w, '_, Self::Param, >, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); let material_bind_group_allocator = material_bind_group_allocator.into_inner(); let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { return RenderCommandResult::Skip; }; let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { return RenderCommandResult::Skip; }; let Some(material) = materials.get(material_asset_id) else { return RenderCommandResult::Skip; }; let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) else { return RenderCommandResult::Skip; }; let Some(bind_group) = material_bind_group.bind_group() else { return RenderCommandResult::Skip; }; pass.set_bind_group(I, bind_group, &[]); RenderCommandResult::Success } } /// Stores all extracted instances of all [`Material`]s in the render world. #[derive(Resource, Default)] pub struct RenderMaterialInstances { /// Maps from each entity in the main world to the /// [`RenderMaterialInstance`] associated with it. pub instances: MainEntityHashMap, /// A monotonically-increasing counter, which we use to sweep /// [`RenderMaterialInstances::instances`] when the entities and/or required /// components are removed. current_change_tick: Tick, } impl RenderMaterialInstances { /// Returns the mesh material ID for the entity with the given mesh, or a /// dummy mesh material ID if the mesh has no material ID. /// /// Meshes almost always have materials, but in very specific circumstances /// involving custom pipelines they won't. (See the /// `specialized_mesh_pipelines` example.) pub(crate) fn mesh_material(&self, entity: MainEntity) -> UntypedAssetId { match self.instances.get(&entity) { Some(render_instance) => render_instance.asset_id, None => DUMMY_MESH_MATERIAL.into(), } } } /// The material associated with a single mesh instance in the main world. /// /// Note that this uses an [`UntypedAssetId`] and isn't generic over the /// material type, for simplicity. pub struct RenderMaterialInstance { /// The material asset. pub(crate) asset_id: UntypedAssetId, /// The [`RenderMaterialInstances::current_change_tick`] at which this /// material instance was last modified. last_change_tick: Tick, } pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey { match alpha_mode { // Premultiplied and Add share the same pipeline key // They're made distinct in the PBR shader, via `premultiply_alpha()` AlphaMode::Premultiplied | AlphaMode::Add => MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA, AlphaMode::Blend => MeshPipelineKey::BLEND_ALPHA, AlphaMode::Multiply => MeshPipelineKey::BLEND_MULTIPLY, AlphaMode::Mask(_) => MeshPipelineKey::MAY_DISCARD, AlphaMode::AlphaToCoverage => match *msaa { Msaa::Off => MeshPipelineKey::MAY_DISCARD, _ => MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE, }, _ => MeshPipelineKey::NONE, } } pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { 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, } } pub const fn screen_space_specular_transmission_pipeline_key( screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality, ) -> MeshPipelineKey { match screen_space_transmissive_blur_quality { ScreenSpaceTransmissionQuality::Low => { MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW } ScreenSpaceTransmissionQuality::Medium => { MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM } ScreenSpaceTransmissionQuality::High => { MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH } ScreenSpaceTransmissionQuality::Ultra => { MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA } } } /// A system that ensures that /// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes /// whose materials changed. /// /// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers /// meshes that were newly extracted, and it writes information from the /// [`RenderMaterialInstances`] into the /// [`crate::render::mesh::MeshInputUniform`], we must tell /// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a /// mesh if its material changed. Otherwise, the material binding information in /// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly. /// The easiest way to ensure that /// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh /// is to mark its [`Mesh3d`] as changed, so that's what this system does. fn mark_meshes_as_changed_if_their_materials_changed( mut changed_meshes_query: Query< &mut Mesh3d, Or<(Changed>, AssetChanged>)>, >, ) where M: Material, { for mut mesh in &mut changed_meshes_query { mesh.set_changed(); } } /// Fills the [`RenderMaterialInstances`] resources from the meshes in the /// scene. fn extract_mesh_materials( mut material_instances: ResMut, changed_meshes_query: Extract< Query< (Entity, &ViewVisibility, &MeshMaterial3d), Or<(Changed, Changed>)>, >, >, ) { let last_change_tick = material_instances.current_change_tick; for (entity, view_visibility, material) in &changed_meshes_query { if view_visibility.get() { material_instances.instances.insert( entity.into(), RenderMaterialInstance { asset_id: material.id().untyped(), last_change_tick, }, ); } else { material_instances .instances .remove(&MainEntity::from(entity)); } } } /// Removes mesh materials from [`RenderMaterialInstances`] when their /// [`MeshMaterial3d`] components are removed. /// /// This is tricky because we have to deal with the case in which a material of /// type A was removed and replaced with a material of type B in the same frame /// (which is actually somewhat common of an operation). In this case, even /// though an entry will be present in `RemovedComponents>`, /// we must not remove the entry in `RenderMaterialInstances` which corresponds /// to material B. To handle this case, we use change ticks to avoid removing /// the entry if it was updated this frame. /// /// This is the first of two sweep phases. Because this phase runs once per /// material type, we need a second phase in order to guarantee that we only /// bump [`RenderMaterialInstances::current_change_tick`] once. fn early_sweep_material_instances( mut material_instances: ResMut, mut removed_materials_query: Extract>>, ) where M: Material, { let last_change_tick = material_instances.current_change_tick; for entity in removed_materials_query.read() { if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { // Only sweep the entry if it wasn't updated this frame. if occupied_entry.get().last_change_tick != last_change_tick { occupied_entry.remove(); } } } } /// Removes mesh materials from [`RenderMaterialInstances`] when their /// [`ViewVisibility`] components are removed. /// /// This runs after all invocations of [`early_sweep_material_instances`] and is /// responsible for bumping [`RenderMaterialInstances::current_change_tick`] in /// preparation for a new frame. fn late_sweep_material_instances( mut material_instances: ResMut, mut removed_visibilities_query: Extract>, ) { let last_change_tick = material_instances.current_change_tick; for entity in removed_visibilities_query.read() { if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { // Only sweep the entry if it wasn't updated this frame. It's // possible that a `ViewVisibility` component was removed and // re-added in the same frame. if occupied_entry.get().last_change_tick != last_change_tick { occupied_entry.remove(); } } } material_instances .current_change_tick .set(last_change_tick.get() + 1); } pub fn extract_entities_needs_specialization( entities_needing_specialization: Extract>>, mut entity_specialization_ticks: ResMut>, mut removed_mesh_material_components: Extract>>, mut specialized_material_pipeline_cache: ResMut>, mut specialized_prepass_material_pipeline_cache: Option< ResMut>, >, mut specialized_shadow_material_pipeline_cache: Option< ResMut>, >, views: Query<&ExtractedView>, ticks: SystemChangeTick, ) where M: Material, { for entity in entities_needing_specialization.iter() { // Update the entity's specialization tick with this run's tick entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); } // Clean up any despawned entities for entity in removed_mesh_material_components.read() { entity_specialization_ticks.remove(&MainEntity::from(entity)); for view in views { if let Some(cache) = specialized_material_pipeline_cache.get_mut(&view.retained_view_entity) { cache.remove(&MainEntity::from(entity)); } if let Some(cache) = specialized_prepass_material_pipeline_cache .as_mut() .and_then(|c| c.get_mut(&view.retained_view_entity)) { cache.remove(&MainEntity::from(entity)); } if let Some(cache) = specialized_shadow_material_pipeline_cache .as_mut() .and_then(|c| c.get_mut(&view.retained_view_entity)) { cache.remove(&MainEntity::from(entity)); } } } } #[derive(Resource, Deref, DerefMut, Clone, Debug)] pub struct EntitiesNeedingSpecialization { #[deref] pub entities: Vec, _marker: PhantomData, } impl Default for EntitiesNeedingSpecialization { fn default() -> Self { Self { entities: Default::default(), _marker: Default::default(), } } } #[derive(Resource, Deref, DerefMut, Clone, Debug)] pub struct EntitySpecializationTicks { #[deref] pub entities: MainEntityHashMap, _marker: PhantomData, } impl Default for EntitySpecializationTicks { fn default() -> Self { Self { entities: MainEntityHashMap::default(), _marker: Default::default(), } } } /// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. #[derive(Resource, Deref, DerefMut)] pub struct SpecializedMaterialPipelineCache { // view entity -> view pipeline cache #[deref] map: HashMap>, marker: PhantomData, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. #[derive(Deref, DerefMut)] pub struct SpecializedMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, marker: PhantomData, } impl Default for SpecializedMaterialPipelineCache { fn default() -> Self { Self { map: HashMap::default(), marker: PhantomData, } } } impl Default for SpecializedMaterialViewPipelineCache { fn default() -> Self { Self { map: MainEntityHashMap::default(), marker: PhantomData, } } } pub fn check_entities_needing_specialization( needs_specialization: Query< Entity, ( Or<( Changed, AssetChanged, Changed>, AssetChanged>, )>, With>, ), >, mut par_local: Local>>, mut entities_needing_specialization: ResMut>, ) where M: Material, { entities_needing_specialization.clear(); needs_specialization .par_iter() .for_each(|entity| par_local.borrow_local_mut().push(entity)); par_local.drain_into(&mut entities_needing_specialization); } pub fn specialize_material_meshes( render_meshes: Res>, render_materials: Res>>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, ( material_bind_group_allocator, opaque_render_phases, alpha_mask_render_phases, transmissive_render_phases, transparent_render_phases, ): ( Res>, Res>, Res>, Res>, Res>, ), views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, entity_specialization_ticks: Res>, view_specialization_ticks: Res, mut specialized_material_pipeline_cache: ResMut>, mut pipelines: ResMut>>, pipeline: Res>, pipeline_cache: Res, ticks: SystemChangeTick, ) where M::Data: PartialEq + Eq + Hash + Clone, { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_views: HashSet = HashSet::default(); for (view, visible_entities) in &views { all_views.insert(view.retained_view_entity); if !transparent_render_phases.contains_key(&view.retained_view_entity) && !opaque_render_phases.contains_key(&view.retained_view_entity) && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) && !transmissive_render_phases.contains_key(&view.retained_view_entity) { continue; } let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { continue; }; let view_tick = view_specialization_ticks .get(&view.retained_view_entity) .unwrap(); let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache .entry(view.retained_view_entity) .or_default(); for (_, visible_entity) in visible_entities.iter::() { let Some(material_instance) = render_material_instances.instances.get(visible_entity) else { continue; }; let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { continue; }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); let last_specialized_tick = view_specialized_material_pipeline_cache .get(visible_entity) .map(|(tick, _)| *tick); let needs_specialization = last_specialized_tick.is_none_or(|tick| { view_tick.is_newer_than(tick, ticks.this_run()) || entity_tick.is_newer_than(tick, ticks.this_run()) }); if !needs_specialization { continue; } let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; let Some(material) = render_materials.get(material_asset_id) else { continue; }; let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) else { continue; }; let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( material.properties.alpha_mode, &Msaa::from_samples(view_key.msaa_samples()), )); let mut mesh_key = *view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) | mesh_pipeline_key_bits; if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { mesh_key |= MeshPipelineKey::LIGHTMAPPED; if lightmap.bicubic_sampling { mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; } } if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; } if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { // If the previous frame have skins or morph targets, note that. if mesh_instance .flags .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) { mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; } if mesh_instance .flags .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) { mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; } } let key = MaterialPipelineKey { mesh_key, bind_group_data: material_bind_group .get_extra_data(material.binding.slot) .clone(), }; let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { error!("{}", err); continue; } }; view_specialized_material_pipeline_cache .insert(*visible_entity, (ticks.this_run(), pipeline_id)); } } // Delete specialized pipelines belonging to views that have expired. specialized_material_pipeline_cache .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); } /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. pub fn queue_material_meshes( render_materials: Res>>, render_mesh_instances: Res, render_material_instances: Res, mesh_allocator: Res, gpu_preprocessing_support: Res, mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, specialized_material_pipeline_cache: ResMut>, ) where M::Data: PartialEq + Eq + Hash + Clone, { for (view, visible_entities) in &views { let ( Some(opaque_phase), Some(alpha_mask_phase), Some(transmissive_phase), Some(transparent_phase), ) = ( opaque_render_phases.get_mut(&view.retained_view_entity), alpha_mask_render_phases.get_mut(&view.retained_view_entity), transmissive_render_phases.get_mut(&view.retained_view_entity), transparent_render_phases.get_mut(&view.retained_view_entity), ) else { continue; }; let Some(view_specialized_material_pipeline_cache) = specialized_material_pipeline_cache.get(&view.retained_view_entity) else { continue; }; let rangefinder = view.rangefinder3d(); for (render_entity, visible_entity) in visible_entities.iter::() { let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache .get(visible_entity) .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) else { continue; }; // Skip the entity if it's cached in a bin and up to date. if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick) || alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick) { continue; } let Some(material_instance) = render_material_instances.instances.get(visible_entity) else { continue; }; let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { continue; }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; let Some(material) = render_materials.get(material_asset_id) else { continue; }; // Fetch the slabs that this mesh resides in. let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); match material.properties.render_phase_type { RenderPhaseType::Transmissive => { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; transmissive_phase.add(Transmissive3d { entity: (*render_entity, *visible_entity), draw_function: material.properties.draw_function_id, pipeline: pipeline_id, distance, batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, indexed: index_slab.is_some(), }); } RenderPhaseType::Opaque => { if material.properties.render_method == OpaqueRendererMethod::Deferred { // Even though we aren't going to insert the entity into // a bin, we still want to update its cache entry. That // way, we know we don't need to re-examine it in future // frames. opaque_phase.update_cache(*visible_entity, None, current_change_tick); continue; } let batch_set_key = Opaque3dBatchSetKey { pipeline: pipeline_id, draw_function: material.properties.draw_function_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, lightmap_slab: mesh_instance.shared.lightmap_slab_index.map(|index| *index), }; let bin_key = Opaque3dBinKey { asset_id: mesh_instance.mesh_asset_id.into(), }; opaque_phase.add( batch_set_key, bin_key, (*render_entity, *visible_entity), mesh_instance.current_uniform_index, BinnedRenderPhaseType::mesh( mesh_instance.should_batch(), &gpu_preprocessing_support, ), current_change_tick, ); } // Alpha mask RenderPhaseType::AlphaMask => { let batch_set_key = OpaqueNoLightmap3dBatchSetKey { draw_function: material.properties.draw_function_id, pipeline: pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, }; let bin_key = OpaqueNoLightmap3dBinKey { asset_id: mesh_instance.mesh_asset_id.into(), }; alpha_mask_phase.add( batch_set_key, bin_key, (*render_entity, *visible_entity), mesh_instance.current_uniform_index, BinnedRenderPhaseType::mesh( mesh_instance.should_batch(), &gpu_preprocessing_support, ), current_change_tick, ); } RenderPhaseType::Transparent => { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; transparent_phase.add(Transparent3d { entity: (*render_entity, *visible_entity), draw_function: material.properties.draw_function_id, pipeline: pipeline_id, distance, batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, indexed: index_slab.is_some(), }); } } } } } /// Default render method used for opaque materials. #[derive(Default, Resource, Clone, Debug, ExtractResource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] 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, PartialEq, Reflect)] #[reflect(Default, Clone, PartialEq)] 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, /// The bits in the [`MeshPipelineKey`] for this material. /// /// These are precalculated so that we can just "or" them together in /// [`queue_material_meshes`]. pub mesh_pipeline_key_bits: MeshPipelineKey, /// Add a bias to the view depth of the mesh which can be used to force a specific render order /// for meshes with equal depth, to avoid z-fighting. /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. pub depth_bias: f32, /// Whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). /// /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires /// rendering to take place in a separate [`Transmissive3d`] pass. pub reads_view_transmission_texture: bool, pub render_phase_type: RenderPhaseType, pub draw_function_id: DrawFunctionId, pub prepass_draw_function_id: Option, pub deferred_draw_function_id: Option, } #[derive(Clone, Copy)] pub enum RenderPhaseType { Opaque, AlphaMask, Transmissive, Transparent, } /// A resource that maps each untyped material ID to its binding. /// /// This duplicates information in `RenderAssets`, but it doesn't have the /// `M` type parameter, so it can be used in untyped contexts like /// [`crate::render::mesh::collect_meshes_for_gpu_building`]. #[derive(Resource, Default, Deref, DerefMut)] pub struct RenderMaterialBindings(HashMap); /// Data prepared for a [`Material`] instance. pub struct PreparedMaterial { pub binding: MaterialBindingId, pub properties: MaterialProperties, pub phantom: PhantomData, } impl RenderAsset for PreparedMaterial { type SourceAsset = M; type Param = ( SRes, SRes>, SRes, SResMut>, SResMut, SRes>, SRes>, SRes>, SRes>, SRes>, SRes>, SRes>, SRes>, M::Param, ); fn prepare_asset( material: Self::SourceAsset, material_id: AssetId, ( render_device, pipeline, default_opaque_render_method, bind_group_allocator, render_material_bindings, opaque_draw_functions, alpha_mask_draw_functions, transmissive_draw_functions, transparent_draw_functions, opaque_prepass_draw_functions, alpha_mask_prepass_draw_functions, opaque_deferred_draw_functions, alpha_mask_deferred_draw_functions, material_param, ): &mut SystemParamItem, ) -> Result> { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); let draw_transparent_pbr = transparent_draw_functions.read().id::>(); let draw_opaque_prepass = opaque_prepass_draw_functions .read() .get_id::>(); let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions .read() .get_id::>(); let draw_opaque_deferred = opaque_deferred_draw_functions .read() .get_id::>(); let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions .read() .get_id::>(); let render_method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, OpaqueRendererMethod::Auto => default_opaque_render_method.0, }; let mut mesh_pipeline_key_bits = MeshPipelineKey::empty(); mesh_pipeline_key_bits.set( MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, material.reads_view_transmission_texture(), ); let reads_view_transmission_texture = mesh_pipeline_key_bits.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); let render_phase_type = match material.alpha_mode() { AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => { RenderPhaseType::Transparent } _ if reads_view_transmission_texture => RenderPhaseType::Transmissive, AlphaMode::Opaque | AlphaMode::AlphaToCoverage => RenderPhaseType::Opaque, AlphaMode::Mask(_) => RenderPhaseType::AlphaMask, }; let draw_function_id = match render_phase_type { RenderPhaseType::Opaque => draw_opaque_pbr, RenderPhaseType::AlphaMask => draw_alpha_mask_pbr, RenderPhaseType::Transmissive => draw_transmissive_pbr, RenderPhaseType::Transparent => draw_transparent_pbr, }; let prepass_draw_function_id = match render_phase_type { RenderPhaseType::Opaque => draw_opaque_prepass, RenderPhaseType::AlphaMask => draw_alpha_mask_prepass, _ => None, }; let deferred_draw_function_id = match render_phase_type { RenderPhaseType::Opaque => draw_opaque_deferred, RenderPhaseType::AlphaMask => draw_alpha_mask_deferred, _ => None, }; match material.unprepared_bind_group( &pipeline.material_layout, render_device, material_param, false, ) { Ok(unprepared) => { // Allocate or update the material. let binding = match render_material_bindings.entry(material_id.into()) { Entry::Occupied(mut occupied_entry) => { // TODO: Have a fast path that doesn't require // recreating the bind group if only buffer contents // change. For now, we just delete and recreate the bind // group. bind_group_allocator.free(*occupied_entry.get()); let new_binding = bind_group_allocator .allocate_unprepared(unprepared, &pipeline.material_layout); *occupied_entry.get_mut() = new_binding; new_binding } Entry::Vacant(vacant_entry) => *vacant_entry.insert( bind_group_allocator .allocate_unprepared(unprepared, &pipeline.material_layout), ), }; Ok(PreparedMaterial { binding, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, draw_function_id, prepass_draw_function_id, render_method, mesh_pipeline_key_bits, deferred_draw_function_id, }, phantom: PhantomData, }) } Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } Err(AsBindGroupError::CreateBindGroupDirectly) => { // This material has opted out of automatic bind group creation // and is requesting a fully-custom bind group. Invoke // `as_bind_group` as requested, and store the resulting bind // group in the slot. match material.as_bind_group( &pipeline.material_layout, render_device, material_param, ) { Ok(prepared_bind_group) => { // Store the resulting bind group directly in the slot. let material_binding_id = bind_group_allocator.allocate_prepared(prepared_bind_group); render_material_bindings.insert(material_id.into(), material_binding_id); Ok(PreparedMaterial { binding: material_binding_id, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, draw_function_id, prepass_draw_function_id, render_method, mesh_pipeline_key_bits, deferred_draw_function_id, }, phantom: PhantomData, }) } Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } fn unload_asset( source_asset: AssetId, (_, _, _, bind_group_allocator, render_material_bindings, ..): &mut SystemParamItem< Self::Param, >, ) { let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped()) else { return; }; bind_group_allocator.free(material_binding_id); } } #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct MaterialBindGroupId(pub Option); impl MaterialBindGroupId { pub fn new(id: BindGroupId) -> Self { Self(Some(id)) } } impl From for MaterialBindGroupId { fn from(value: BindGroup) -> Self { Self::new(value.id()) } } /// Creates and/or recreates any bind groups that contain materials that were /// modified this frame. pub fn prepare_material_bind_groups( mut allocator: ResMut>, render_device: Res, fallback_image: Res, fallback_resources: Res, ) where M: Material, { allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); } /// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] /// manages to the GPU. /// /// Non-bindless allocators don't currently manage any buffers, so this method /// only has an effect for bindless allocators. pub fn write_material_bind_group_buffers( mut allocator: ResMut>, render_device: Res, render_queue: Res, ) where M: Material, { allocator.write_buffers(&render_device, &render_queue); }