use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ core_2d::{AlphaMask2d, AlphaMask2dBinKey, Opaque2d, Opaque2dBinKey, Transparent2d}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::EntityHashMap, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_math::FloatOrd; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, render_phase::{ AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, texture::{FallbackImage, GpuImage}, view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::error; use std::{hash::Hash, marker::PhantomData}; use crate::{ DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup, WithMesh2d, }; /// 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}; /// # use bevy_color::LinearRgba; /// # 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: 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 `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: LinearRgba::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 } fn alpha_mode(&self) -> AlphaMode2d { AlphaMode2d::Opaque } /// Customizes the default [`RenderPipelineDescriptor`]. #[allow(unused_variables)] #[inline] fn specialize( descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: Material2dKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } /// Sets how a 2d material's base color alpha channel is used for transparency. /// Currently, this only works with [`Mesh2d`](crate::mesh2d::Mesh2d). Sprites are always transparent. /// /// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. /// We use a separate type because 2d doesn't support all the transparency modes that 3d does. #[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] #[reflect(Default, Debug)] pub enum AlphaMode2d { /// Base color alpha values are overridden to be fully opaque (1.0). #[default] Opaque, /// Reduce transparency to fully opaque or fully transparent /// based on a threshold. /// /// Compares the base color alpha value to the specified threshold. /// If the value is below the threshold, /// considers the color to be fully transparent (alpha is set to 0.0). /// If it is equal to or above the threshold, /// considers the color to be fully opaque (alpha is set to 1.0). Mask(f32), /// The base color alpha value defines the opacity of the color. /// Standard alpha-blending is used to blend the fragment's color /// with the color behind it. Blend, } /// 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::() .add_plugins(RenderAssetPlugin::>::default()); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>() .init_resource::>>() .add_systems(ExtractSchedule, extract_material_meshes_2d::) .add_systems( Render, queue_material2d_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>), ); } } fn finish(&self, app: &mut App) { if let Some(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: &MeshVertexBufferLayoutRef, ) -> 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 ViewQuery = (); type ItemQuery = (); #[inline] fn render<'w>( item: &P, _view: (), _item_query: Option<()>, (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::Skip; }; let Some(material2d) = materials.get(*material_instance) else { return RenderCommandResult::Skip; }; pass.set_bind_group(I, &material2d.bind_group, &[]); RenderCommandResult::Success } } pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode2d) -> Mesh2dPipelineKey { match alpha_mode { AlphaMode2d::Blend => Mesh2dPipelineKey::BLEND_ALPHA, AlphaMode2d::Mask(_) => Mesh2dPipelineKey::MAY_DISCARD, _ => Mesh2dPipelineKey::NONE, } } pub 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( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, transparent_draw_functions: Res>, material2d_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, render_meshes: Res>, render_materials: Res>>, mut render_mesh_instances: ResMut, render_material_instances: Res>, mut transparent_render_phases: ResMut>, mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, mut views: Query<( Entity, &ExtractedView, &VisibleEntities, &Msaa, Option<&Tonemapping>, Option<&DebandDither>, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, { if render_material_instances.is_empty() { return; } for (view_entity, view, visible_entities, msaa, tonemapping, dither) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { continue; }; let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view_entity) else { continue; }; let draw_transparent_2d = transparent_draw_functions.read().id::>(); let draw_opaque_2d = opaque_draw_functions.read().id::>(); let draw_alpha_mask_2d = alpha_mask_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.iter::() { 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(material_2d) = 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()) | material_2d.properties.mesh_pipeline_key_bits; let pipeline_id = pipelines.specialize( &pipeline_cache, &material2d_pipeline, Material2dKey { mesh_key, bind_group_data: material_2d.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_2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.world_from_local.translation.z; match material_2d.properties.alpha_mode { AlphaMode2d::Opaque => { let bin_key = Opaque2dBinKey { pipeline: pipeline_id, draw_function: draw_opaque_2d, asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material_2d.get_bind_group_id().0, }; opaque_phase.add( bin_key, *visible_entity, BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), ); } AlphaMode2d::Mask(_) => { let bin_key = AlphaMask2dBinKey { pipeline: pipeline_id, draw_function: draw_alpha_mask_2d, asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material_2d.get_bind_group_id().0, }; alpha_mask_phase.add( bin_key, *visible_entity, BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), ); } AlphaMode2d::Blend => { transparent_phase.add(Transparent2d { entity: *visible_entity, draw_function: draw_transparent_2d, 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 + material_2d.properties.depth_bias), // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::NONE, }); } } } } } #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct Material2dBindGroupId(pub Option); /// Common [`Material2d`] properties, calculated for a specific material instance. pub struct Material2dProperties { /// The [`AlphaMode2d`] of this material. pub alpha_mode: AlphaMode2d, /// 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 pub depth_bias: f32, /// The bits in the [`Mesh2dPipelineKey`] for this material. /// /// These are precalculated so that we can just "or" them together in /// [`queue_material2d_meshes`]. pub mesh_pipeline_key_bits: Mesh2dPipelineKey, } /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { pub bindings: Vec<(u32, OwnedBindingResource)>, pub bind_group: BindGroup, pub key: T::Data, pub properties: Material2dProperties, } impl PreparedMaterial2d { pub fn get_bind_group_id(&self) -> Material2dBindGroupId { Material2dBindGroupId(Some(self.bind_group.id())) } } impl RenderAsset for PreparedMaterial2d { type SourceAsset = M; type Param = ( SRes, SRes>, SRes, SRes>, ); fn prepare_asset( material: Self::SourceAsset, (render_device, images, fallback_image, pipeline): &mut SystemParamItem, ) -> Result> { match material.as_bind_group( &pipeline.material2d_layout, render_device, images, fallback_image, ) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, key: prepared.data, properties: Material2dProperties { depth_bias: material.depth_bias(), alpha_mode: material.alpha_mode(), mesh_pipeline_key_bits, }, }) } Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } } /// 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(), } } }