use crate::{ render, AlphaMode, DrawMesh, DrawPrepass, EnvironmentMapLight, MeshPipeline, MeshPipelineKey, PrepassPipelinePlugin, PrepassPlugin, RenderMeshInstances, ScreenSpaceAmbientOcclusionSettings, SetMeshBindGroup, SetMeshViewBindGroup, Shadow, }; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, experimental::taa::TemporalAntiAliasSettings, prepass::NormalPrepass, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_render::{ mesh::{Mesh, MeshVertexBufferLayout}, prelude::Image, render_asset::{prepare_assets, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{ AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, texture::FallbackImage, view::{ExtractedView, Msaa, ViewVisibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::error, EntityHashMap, HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; /// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`](crate::MaterialMeshBundle) /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level /// way to render [`Mesh`] 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, MaterialMeshBundle}; /// # use bevy_ecs::prelude::*; /// # use bevy_reflect::{TypeUuid, TypePath}; /// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color}; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; /// /// #[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: Color, /// // 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 using `CustomMaterial`. /// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { /// commands.spawn(MaterialMeshBundle { /// material: materials.add(CustomMaterial { /// color: Color::RED, /// color_texture: asset_server.load("some_image.png"), /// }), /// ..Default::default() /// }); /// } /// ``` /// In WGSL shaders, the material's binding would look like this: /// /// ```wgsl /// @group(1) @binding(0) var color: vec4; /// @group(1) @binding(1) var color_texture: texture_2d; /// @group(1) @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. #[allow(unused_variables)] fn fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. #[inline] fn alpha_mode(&self) -> AlphaMode { AlphaMode::Opaque } #[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 } /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader /// will be used. fn prepass_vertex_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader /// will be used. #[allow(unused_variables)] fn prepass_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input. #[allow(unused_variables)] #[inline] fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, 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, pub _marker: PhantomData, } impl Default for MaterialPlugin { fn default() -> Self { Self { prepass_enabled: true, _marker: Default::default(), } } } impl Plugin for MaterialPlugin where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { app.init_asset::(); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::>>() .add_systems( ExtractSchedule, (extract_materials::, extract_material_meshes::), ) .add_systems( Render, ( prepare_materials:: .in_set(RenderSet::PrepareAssets) .after(prepare_assets::), render::queue_shadows:: .in_set(RenderSet::QueueMeshes) .after(prepare_materials::), queue_material_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_materials::), ), ); } // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin app.add_plugins(PrepassPipelinePlugin::::default()); if self.prepass_enabled { app.add_plugins(PrepassPlugin::::default()); } } fn finish(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } } } /// 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>, 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(), marker: PhantomData, } } } impl SpecializedMeshPipeline for MaterialPipeline where M::Data: PartialEq + Eq + Hash + Clone, { type Key = MaterialPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayout, ) -> 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(1, self.material_layout.clone()); M::specialize(self, &mut descriptor, layout, key)?; 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)), }, marker: PhantomData, } } } type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetMaterialBindGroup, SetMeshBindGroup<2>, 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>); type ViewWorldQuery = (); type ItemWorldQuery = (); #[inline] fn render<'w>( item: &P, _view: (), _item_query: (), (materials, material_instances): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); let Some(material_asset_id) = material_instances.get(&item.entity()) else { return RenderCommandResult::Failure; }; let Some(material) = materials.get(material_asset_id) else { return RenderCommandResult::Failure; }; pass.set_bind_group(I, &material.bind_group, &[]); RenderCommandResult::Success } } #[derive(Resource, Deref, DerefMut)] pub struct RenderMaterialInstances(EntityHashMap>); impl Default for RenderMaterialInstances { fn default() -> Self { Self(Default::default()) } } fn extract_material_meshes( mut material_instances: ResMut>, query: Extract)>>, ) { material_instances.clear(); for (entity, view_visibility, handle) in &query { if view_visibility.get() { material_instances.insert(entity, handle.id()); } } } const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> 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, _ => MeshPipelineKey::NONE, } } 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, } } #[allow(clippy::too_many_arguments)] pub fn queue_material_meshes( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, transparent_draw_functions: Res>, material_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, msaa: Res, render_meshes: Res>, render_materials: Res>, mut render_mesh_instances: ResMut, render_material_instances: Res>, images: Res>, mut views: Query<( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, Option<&DebandDither>, Option<&EnvironmentMapLight>, Option<&ScreenSpaceAmbientOcclusionSettings>, Option<&NormalPrepass>, Option<&TemporalAntiAliasSettings>, &mut RenderPhase, &mut RenderPhase, &mut RenderPhase, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, { for ( view, visible_entities, tonemapping, dither, environment_map, ssao, normal_prepass, taa_settings, mut opaque_phase, mut alpha_mask_phase, mut transparent_phase, ) in &mut views { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); let draw_transparent_pbr = transparent_draw_functions.read().id::>(); let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) | MeshPipelineKey::from_hdr(view.hdr); if normal_prepass.is_some() { view_key |= MeshPipelineKey::NORMAL_PREPASS; } if taa_settings.is_some() { view_key |= MeshPipelineKey::TAA; } let environment_map_loaded = environment_map.is_some_and(|map| map.is_loaded(&images)); if environment_map_loaded { view_key |= MeshPipelineKey::ENVIRONMENT_MAP; } if !view.hdr { if let Some(tonemapping) = tonemapping { view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; view_key |= tonemapping_pipeline_key(*tonemapping); } if let Some(DebandDither::Enabled) = dither { view_key |= MeshPipelineKey::DEBAND_DITHER; } } if ssao.is_some() { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { 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 mut mesh_key = view_key; mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); if mesh.morph_targets.is_some() { mesh_key |= MeshPipelineKey::MORPH_TARGETS; } mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode); let pipeline_id = pipelines.specialize( &pipeline_cache, &material_pipeline, MaterialPipelineKey { mesh_key, bind_group_data: material.key.clone(), }, &mesh.layout, ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { error!("{}", err); continue; } }; mesh_instance.material_bind_group_id = material.get_bind_group_id(); let distance = rangefinder .distance_translation(&mesh_instance.transforms.transform.translation) + 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, }); } 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, }); } AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => { transparent_phase.add(Transparent3d { entity: *visible_entity, draw_function: draw_transparent_pbr, pipeline: pipeline_id, distance, batch_range: 0..1, dynamic_offset: None, }); } } } } } /// Common [`Material`] properties, calculated for a specific material instance. pub struct MaterialProperties { /// 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 /// 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, } /// Data prepared for a [`Material`] instance. pub struct PreparedMaterial { pub bindings: Vec, pub bind_group: BindGroup, pub key: T::Data, pub properties: MaterialProperties, } #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct MaterialBindGroupId(Option); impl PreparedMaterial { pub fn get_bind_group_id(&self) -> MaterialBindGroupId { MaterialBindGroupId(Some(self.bind_group.id())) } } #[derive(Resource)] pub struct ExtractedMaterials { extracted: Vec<(AssetId, M)>, removed: Vec>, } impl Default for ExtractedMaterials { fn default() -> Self { Self { extracted: Default::default(), removed: Default::default(), } } } /// Stores all prepared representations of [`Material`] assets for as long as they exist. #[derive(Resource, Deref, DerefMut)] pub struct RenderMaterials(pub HashMap, PreparedMaterial>); impl Default for RenderMaterials { fn default() -> Self { Self(Default::default()) } } /// This system extracts all created or modified assets of the corresponding [`Material`] type /// into the "render world". pub fn extract_materials( mut commands: Commands, mut events: Extract>>, assets: Extract>>, ) { let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); for event in events.read() { match event { AssetEvent::Added { id } | AssetEvent::Modified { id } => { changed_assets.insert(*id); } AssetEvent::Removed { id } => { changed_assets.remove(id); removed.push(*id); } AssetEvent::LoadedWithDependencies { .. } => { // TODO: handle this } } } let mut extracted_assets = Vec::new(); for id in changed_assets.drain() { if let Some(asset) = assets.get(id) { extracted_assets.push((id, asset.clone())); } } commands.insert_resource(ExtractedMaterials { extracted: extracted_assets, removed, }); } /// All [`Material`] values of a given type that should be prepared next frame. pub struct PrepareNextFrameMaterials { assets: Vec<(AssetId, M)>, } impl Default for PrepareNextFrameMaterials { fn default() -> Self { Self { assets: Default::default(), } } } /// This system prepares all assets of the corresponding [`Material`] type /// which where extracted this frame for the GPU. pub fn prepare_materials( mut prepare_next_frame: Local>, mut extracted_assets: ResMut>, mut render_materials: ResMut>, render_device: Res, images: Res>, fallback_image: Res, pipeline: Res>, ) { let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, material) in queued_assets.into_iter() { match prepare_material( &material, &render_device, &images, &fallback_image, &pipeline, ) { Ok(prepared_asset) => { render_materials.insert(id, prepared_asset); } Err(AsBindGroupError::RetryNextUpdate) => { prepare_next_frame.assets.push((id, material)); } } } for removed in std::mem::take(&mut extracted_assets.removed) { render_materials.remove(&removed); } for (id, material) in std::mem::take(&mut extracted_assets.extracted) { match prepare_material( &material, &render_device, &images, &fallback_image, &pipeline, ) { Ok(prepared_asset) => { render_materials.insert(id, prepared_asset); } Err(AsBindGroupError::RetryNextUpdate) => { prepare_next_frame.assets.push((id, material)); } } } } fn prepare_material( material: &M, render_device: &RenderDevice, images: &RenderAssets, fallback_image: &FallbackImage, pipeline: &MaterialPipeline, ) -> Result, AsBindGroupError> { let prepared = material.as_bind_group( &pipeline.material_layout, render_device, images, fallback_image, )?; Ok(PreparedMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, key: prepared.data, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), }, }) }