use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ core_2d::Transparent2d, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_log::error; 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, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::{EntityHashMap, FloatOrd, HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; use crate::{ DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup, }; /// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`] /// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level /// way to render [`Mesh2dHandle`] entities with custom shader logic. /// /// Material2ds 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 Material2d implementation. The [`AsBindGroup`] derive has many features. To see what else is available, /// check out the [`AsBindGroup`] documentation. /// ``` /// # use bevy_sprite::{Material2d, MaterialMesh2dBundle}; /// # use bevy_ecs::prelude::*; /// # use bevy_reflect::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 `Material2d` have default impls. You only need to implement the /// // functions that are relevant for your material. /// impl Material2d 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(MaterialMesh2dBundle { /// 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 /// struct CustomMaterial { /// color: vec4, /// } /// /// @group(2) @binding(0) var material: CustomMaterial; /// @group(2) @binding(1) var color_texture: texture_2d; /// @group(2) @binding(2) var color_sampler: sampler; /// ``` pub trait Material2d: AsBindGroup + Asset + 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 } /// Add a bias to the view depth of the mesh which can be used to force a specific render order. #[inline] fn depth_bias(&self) -> f32 { 0.0 } /// Customizes the default [`RenderPipelineDescriptor`]. #[allow(unused_variables)] #[inline] fn specialize( descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, key: Material2dKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`] /// asset type (which includes [`Material2d`] types). pub struct Material2dPlugin(PhantomData); impl Default for Material2dPlugin { fn default() -> Self { Self(Default::default()) } } impl Plugin for Material2dPlugin 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 .add_render_command::>() .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::>>() .add_systems( ExtractSchedule, (extract_materials_2d::, extract_material_meshes_2d::), ) .add_systems( Render, ( prepare_materials_2d:: .in_set(RenderSet::PrepareAssets) .after(prepare_assets::), queue_material2d_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_materials_2d::), ), ); } } fn finish(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } } } #[derive(Resource, Deref, DerefMut)] pub struct RenderMaterial2dInstances(EntityHashMap>); impl Default for RenderMaterial2dInstances { fn default() -> Self { Self(Default::default()) } } fn extract_material_meshes_2d( 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()); } } } /// Render pipeline data for a given [`Material2d`] #[derive(Resource)] pub struct Material2dPipeline { pub mesh2d_pipeline: Mesh2dPipeline, pub material2d_layout: BindGroupLayout, pub vertex_shader: Option>, pub fragment_shader: Option>, marker: PhantomData, } pub struct Material2dKey { pub mesh_key: Mesh2dPipelineKey, pub bind_group_data: M::Data, } impl Eq for Material2dKey where M::Data: PartialEq {} impl PartialEq for Material2dKey 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 Material2dKey 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 Material2dKey where M::Data: Hash, { fn hash(&self, state: &mut H) { self.mesh_key.hash(state); self.bind_group_data.hash(state); } } impl Clone for Material2dPipeline { fn clone(&self) -> Self { Self { mesh2d_pipeline: self.mesh2d_pipeline.clone(), material2d_layout: self.material2d_layout.clone(), vertex_shader: self.vertex_shader.clone(), fragment_shader: self.fragment_shader.clone(), marker: PhantomData, } } } impl SpecializedMeshPipeline for Material2dPipeline where M::Data: PartialEq + Eq + Hash + Clone, { type Key = Material2dKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayout, ) -> Result { let mut descriptor = self.mesh2d_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 = vec![ self.mesh2d_pipeline.view_layout.clone(), self.mesh2d_pipeline.mesh_layout.clone(), self.material2d_layout.clone(), ]; M::specialize(&mut descriptor, layout, key)?; Ok(descriptor) } } impl FromWorld for Material2dPipeline { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); let render_device = world.resource::(); let material2d_layout = M::bind_group_layout(render_device); Material2dPipeline { mesh2d_pipeline: world.resource::().clone(), material2d_layout, 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 DrawMaterial2d = ( SetItemPipeline, SetMesh2dViewBindGroup<0>, SetMesh2dBindGroup<1>, SetMaterial2dBindGroup, DrawMesh2d, ); pub struct SetMaterial2dBindGroup(PhantomData); impl RenderCommand

for SetMaterial2dBindGroup { type Param = ( SRes>, SRes>, ); type ViewData = (); type ItemData = (); #[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_instance) = material_instances.get(&item.entity()) else { return RenderCommandResult::Failure; }; let Some(material2d) = materials.get(material_instance) else { return RenderCommandResult::Failure; }; pass.set_bind_group(I, &material2d.bind_group, &[]); RenderCommandResult::Success } } const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { match tonemapping { Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD, Tonemapping::ReinhardLuminance => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE, Tonemapping::AcesFitted => Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED, Tonemapping::AgX => Mesh2dPipelineKey::TONEMAP_METHOD_AGX, Tonemapping::SomewhatBoringDisplayTransform => { Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM } Tonemapping::TonyMcMapface => Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, Tonemapping::BlenderFilmic => Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, } } #[allow(clippy::too_many_arguments)] pub fn queue_material2d_meshes( transparent_draw_functions: Res>, material2d_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>, mut views: Query<( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, Option<&DebandDither>, &mut RenderPhase, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, { if render_material_instances.is_empty() { return; } for (view, visible_entities, tonemapping, dither, mut transparent_phase) in &mut views { let draw_transparent_pbr = transparent_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); if !view.hdr { if let Some(tonemapping) = tonemapping { view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; view_key |= tonemapping_pipeline_key(*tonemapping); } if let Some(DebandDither::Enabled) = dither { view_key |= Mesh2dPipelineKey::DEBAND_DITHER; } } 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(material2d) = render_materials.get(material_asset_id) else { continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; let mesh_key = view_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); let pipeline_id = pipelines.specialize( &pipeline_cache, &material2d_pipeline, Material2dKey { mesh_key, bind_group_data: material2d.key.clone(), }, &mesh.layout, ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { error!("{}", err); continue; } }; mesh_instance.material_bind_group_id = material2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.transform.translation.z; transparent_phase.add(Transparent2d { entity: *visible_entity, draw_function: draw_transparent_pbr, pipeline: pipeline_id, // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the // lowest sort key and getting closer should increase. As we have // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance sort_key: FloatOrd(mesh_z + material2d.depth_bias), // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, dynamic_offset: None, }); } } } #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct Material2dBindGroupId(Option); /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { pub bindings: Vec<(u32, OwnedBindingResource)>, pub bind_group: BindGroup, pub key: T::Data, pub depth_bias: f32, } impl PreparedMaterial2d { pub fn get_bind_group_id(&self) -> Material2dBindGroupId { Material2dBindGroupId(Some(self.bind_group.id())) } } #[derive(Resource)] pub struct ExtractedMaterials2d { extracted: Vec<(AssetId, M)>, removed: Vec>, } impl Default for ExtractedMaterials2d { fn default() -> Self { Self { extracted: Default::default(), removed: Default::default(), } } } /// Stores all prepared representations of [`Material2d`] assets for as long as they exist. #[derive(Resource, Deref, DerefMut)] pub struct RenderMaterials2d(HashMap, PreparedMaterial2d>); impl Default for RenderMaterials2d { fn default() -> Self { Self(Default::default()) } } /// This system extracts all created or modified assets of the corresponding [`Material2d`] type /// into the "render world". pub fn extract_materials_2d( 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(ExtractedMaterials2d { extracted: extracted_assets, removed, }); } /// All [`Material2d`] 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 [`Material2d`] type /// which where extracted this frame for the GPU. pub fn prepare_materials_2d( 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 { match prepare_material2d( &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 (asset_id, material) in std::mem::take(&mut extracted_assets.extracted) { match prepare_material2d( &material, &render_device, &images, &fallback_image, &pipeline, ) { Ok(prepared_asset) => { render_materials.insert(asset_id, prepared_asset); } Err(AsBindGroupError::RetryNextUpdate) => { prepare_next_frame.assets.push((asset_id, material)); } } } } fn prepare_material2d( material: &M, render_device: &RenderDevice, images: &RenderAssets, fallback_image: &FallbackImage, pipeline: &Material2dPipeline, ) -> Result, AsBindGroupError> { let prepared = material.as_bind_group( &pipeline.material2d_layout, render_device, images, fallback_image, )?; Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, key: prepared.data, depth_bias: material.depth_bias(), }) } /// A component bundle for entities with a [`Mesh2dHandle`] and a [`Material2d`]. #[derive(Bundle, Clone)] pub struct MaterialMesh2dBundle { pub mesh: Mesh2dHandle, pub material: Handle, pub transform: Transform, pub global_transform: GlobalTransform, /// User indication of whether an entity is visible pub visibility: Visibility, // Inherited visibility of an entity. pub inherited_visibility: InheritedVisibility, // Indication of whether an entity is visible in any view. pub view_visibility: ViewVisibility, } impl Default for MaterialMesh2dBundle { fn default() -> Self { Self { mesh: Default::default(), material: Default::default(), transform: Default::default(), global_transform: Default::default(), visibility: Default::default(), inherited_visibility: Default::default(), view_visibility: Default::default(), } } }