From c8483147cd635da3b1de9c104411a115af201f56 Mon Sep 17 00:00:00 2001 From: Johan Klokkhammer Helsing Date: Sat, 16 Jul 2022 00:20:04 +0000 Subject: [PATCH] Support AsBindGroup for 2d materials as well (#5312) Port changes made to Material in #5053 to Material2d as well. This is more or less an exact copy of the implementation in bevy_pbr; I simply pretended the API existed, then copied stuff over until it started building and the shapes example was working again. # Objective The changes in #5053 makes it possible to add custom materials with a lot less boiler plate. However, the implementation isn't shared with Material 2d as it's a kind of fork of the bevy_pbr version. It should be possible to use AsBindGroup on the 2d version as well. ## Solution This makes the same kind of changes in Material2d in bevy_sprite. This makes the following work: ```rust //! Draws a circular purple bevy in the middle of the screen using a custom shader use bevy::{ prelude::*, reflect::TypeUuid, render::render_resource::{AsBindGroup, ShaderRef}, sprite::{Material2d, Material2dPlugin, MaterialMesh2dBundle}, }; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugin(Material2dPlugin::::default()) .add_startup_system(setup) .run(); } /// set up a simple 2D scene fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, ) { commands.spawn_bundle(MaterialMesh2dBundle { mesh: meshes.add(shape::Circle::new(50.).into()).into(), material: materials.add(CustomMaterial { color: Color::PURPLE, color_texture: Some(asset_server.load("branding/icon.png")), }), transform: Transform::from_translation(Vec3::new(-100., 0., 0.)), ..default() }); commands.spawn_bundle(Camera2dBundle::default()); } /// The Material2d trait is very configurable, but comes with sensible defaults for all methods. /// You only need to implement functions for features that need non-default behavior. See the Material api docs for details! impl Material2d for CustomMaterial { fn fragment_shader() -> ShaderRef { "shaders/custom_material.wgsl".into() } } // This is the struct that will be passed to your shader #[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] pub struct CustomMaterial { #[uniform(0)] color: Color, #[texture(1)] #[sampler(2)] color_texture: Option>, } ``` --- .../bevy_sprite/src/mesh2d/color_material.rs | 149 +----- crates/bevy_sprite/src/mesh2d/material.rs | 483 ++++++++++++------ examples/shader/post_processing.rs | 99 +--- 3 files changed, 340 insertions(+), 391 deletions(-) diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index daa83be3afa016..b0f68db9795e46 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,18 +1,12 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, AssetServer, Assets, Handle, HandleUntyped}; -use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; use bevy_math::Vec4; use bevy_reflect::TypeUuid; use bevy_render::{ - color::Color, - prelude::Shader, - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, - render_resource::*, - renderer::RenderDevice, - texture::Image, + color::Color, prelude::Shader, render_asset::RenderAssets, render_resource::*, texture::Image, }; -use crate::{Material2d, Material2dPipeline, Material2dPlugin, MaterialMesh2dBundle}; +use crate::{Material2d, Material2dPlugin, MaterialMesh2dBundle}; pub const COLOR_MATERIAL_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 3253086872234592509); @@ -44,10 +38,13 @@ impl Plugin for ColorMaterialPlugin { } /// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2dHandle) with a texture tinted by a uniform color -#[derive(Debug, Clone, TypeUuid)] +#[derive(AsBindGroup, Debug, Clone, TypeUuid)] #[uuid = "e228a544-e3ca-4e1e-bb9d-4d8bc1ad8c19"] +#[uniform(0, ColorMaterialUniform)] pub struct ColorMaterial { pub color: Color, + #[texture(1)] + #[sampler(2)] pub texture: Option>, } @@ -90,142 +87,28 @@ bitflags::bitflags! { /// The GPU representation of the uniform data of a [`ColorMaterial`]. #[derive(Clone, Default, ShaderType)] -pub struct ColorMaterialUniformData { +pub struct ColorMaterialUniform { pub color: Vec4, pub flags: u32, } -/// The GPU representation of a [`ColorMaterial`]. -#[derive(Debug, Clone)] -pub struct GpuColorMaterial { - /// A buffer containing the [`ColorMaterialUniformData`] of the material. - pub buffer: Buffer, - /// The bind group specifying how the [`ColorMaterialUniformData`] and - /// the texture of the material are bound. - pub bind_group: BindGroup, - pub flags: ColorMaterialFlags, - pub texture: Option>, -} - -impl RenderAsset for ColorMaterial { - type ExtractedAsset = ColorMaterial; - type PreparedAsset = GpuColorMaterial; - type Param = ( - SRes, - SRes>, - SRes>, - ); - - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() - } - - fn prepare_asset( - material: Self::ExtractedAsset, - (render_device, color_pipeline, gpu_images): &mut SystemParamItem, - ) -> Result> { - let (texture_view, sampler) = if let Some(result) = color_pipeline - .mesh2d_pipeline - .get_image_texture(gpu_images, &material.texture) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; - +impl AsBindGroupShaderType for ColorMaterial { + fn as_bind_group_shader_type(&self, _images: &RenderAssets) -> ColorMaterialUniform { let mut flags = ColorMaterialFlags::NONE; - if material.texture.is_some() { + if self.texture.is_some() { flags |= ColorMaterialFlags::TEXTURE; } - let value = ColorMaterialUniformData { - color: material.color.as_linear_rgba_f32().into(), + ColorMaterialUniform { + color: self.color.as_linear_rgba_f32().into(), flags: flags.bits(), - }; - - let byte_buffer = [0u8; ColorMaterialUniformData::SHADER_SIZE.get() as usize]; - let mut buffer = encase::UniformBuffer::new(byte_buffer); - buffer.write(&value).unwrap(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - label: Some("color_material_uniform_buffer"), - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - contents: buffer.as_ref(), - }); - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::TextureView(texture_view), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::Sampler(sampler), - }, - ], - label: Some("color_material_bind_group"), - layout: &color_pipeline.material2d_layout, - }); - - Ok(GpuColorMaterial { - buffer, - bind_group, - flags, - texture: material.texture, - }) + } } } impl Material2d for ColorMaterial { - fn fragment_shader(_asset_server: &AssetServer) -> Option> { - Some(COLOR_MATERIAL_SHADER_HANDLE.typed()) - } - - #[inline] - fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { - &render_asset.bind_group - } - - fn bind_group_layout( - render_device: &RenderDevice, - ) -> bevy_render::render_resource::BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(ColorMaterialUniformData::min_size()), - }, - count: None, - }, - // Texture - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - }, - count: None, - }, - // Texture Sampler - BindGroupLayoutEntry { - binding: 2, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("color_material_layout"), - }) + fn fragment_shader() -> ShaderRef { + COLOR_MATERIAL_SHADER_HANDLE.typed().into() } } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index f009e473de8ae1..d73bea32687551 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,34 +1,40 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{AddAsset, Asset, AssetServer, Handle}; +use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::{ entity::Entity, + event::EventReader, prelude::{Bundle, World}, + schedule::ParallelSystemDescriptorCoercion, system::{ lifetimeless::{Read, SQuery, SRes}, - Query, Res, ResMut, SystemParamItem, + Commands, Local, Query, Res, ResMut, SystemParamItem, }, world::FromWorld, }; use bevy_log::error; +use bevy_reflect::TypeUuid; use bevy_render::{ extract_component::ExtractComponentPlugin, mesh::{Mesh, MeshVertexBufferLayout}, - render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets}, + prelude::Image, + render_asset::{PrepareAssetLabel, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{ - BindGroup, BindGroupLayout, PipelineCache, RenderPipelineDescriptor, Shader, - SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource, + PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, + SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, + texture::FallbackImage, view::{ComputedVisibility, Msaa, Visibility, VisibleEntities}, - RenderApp, RenderStage, + Extract, RenderApp, RenderStage, }; use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::FloatOrd; +use bevy_utils::{FloatOrd, HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; @@ -39,36 +45,83 @@ use crate::{ /// 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. For materials that can specialize their [`RenderPipelineDescriptor`] -/// based on specific material values, see [`SpecializedMaterial2d`]. [`Material2d`] automatically implements [`SpecializedMaterial2d`] -/// and can be used anywhere that type is used (such as [`Material2dPlugin`]). -pub trait Material2d: Asset + RenderAsset { - /// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material2d::bind_group_layout`]. - fn bind_group(material: &::PreparedAsset) -> &BindGroup; - - /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`Material2d::bind_group`]. - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; - - /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - None - } - - /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - None +/// 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. +/// +/// Materials must also implement [`TypeUuid`] so they can be treated as an [`Asset`](bevy_asset::Asset). +/// +/// # 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::TypeUuid; +/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color}; +/// # use bevy_asset::{Handle, AssetServer, Assets}; +/// +/// #[derive(AsBindGroup, TypeUuid, Debug, Clone)] +/// #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"] +/// 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_bundle(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(1), binding(0)]] +/// var material: CustomMaterial; +/// [[group(1), binding(1)]] +/// var color_texture: texture_2d; +/// [[group(1), binding(2)]] +/// var color_sampler: sampler; +/// ``` +pub trait Material2d: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'static { + /// 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 } - /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. - /// Defaults to an empty array / no dynamic uniform indices. - #[allow(unused_variables)] - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - &[] + /// 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 } /// Customizes the default [`RenderPipelineDescriptor`]. @@ -77,136 +130,48 @@ pub trait Material2d: Asset + RenderAsset { fn specialize( descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayout, + key: Material2dKey, ) -> Result<(), SpecializedMeshPipelineError> { Ok(()) } } -impl SpecializedMaterial2d for M { - type Key = (); - - #[inline] - fn key( - _render_device: &RenderDevice, - _material: &::PreparedAsset, - ) -> Self::Key { - } - - #[inline] - fn specialize( - _key: Self::Key, - descriptor: &mut RenderPipelineDescriptor, - layout: &MeshVertexBufferLayout, - ) -> Result<(), SpecializedMeshPipelineError> { - ::specialize(descriptor, layout) - } - - #[inline] - fn bind_group(material: &::PreparedAsset) -> &BindGroup { - ::bind_group(material) - } - - #[inline] - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - ::bind_group_layout(render_device) - } - - #[inline] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - ::vertex_shader(asset_server) - } - - #[inline] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - ::fragment_shader(asset_server) - } - - #[allow(unused_variables)] - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - ::dynamic_uniform_indices(material) - } -} - -/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`](crate::MaterialMesh2dBundle) -/// to spawn entities that are rendered with a specific [`SpecializedMaterial2d`] type. They serve as an easy to use high level -/// way to render [`Mesh2dHandle`] entities with custom shader logic. [`SpecializedMaterial2d`s](SpecializedMaterial2d) use their [`SpecializedMaterial2d::Key`] -/// to customize their [`RenderPipelineDescriptor`] based on specific material values. The slightly simpler [`Material2d`] trait -/// should be used for materials that do not need specialization. [`Material2d`] types automatically implement [`SpecializedMaterial2d`]. -pub trait SpecializedMaterial2d: Asset + RenderAsset { - /// The key used to specialize this material's [`RenderPipelineDescriptor`]. - type Key: PartialEq + Eq + Hash + Clone + Send + Sync; - - /// Extract the [`SpecializedMaterial2d::Key`] for the "prepared" version of this material. This key will be - /// passed in to the [`SpecializedMaterial2d::specialize`] function when compiling the [`RenderPipeline`](bevy_render::render_resource::RenderPipeline) - /// for a given entity's material. - fn key( - render_device: &RenderDevice, - material: &::PreparedAsset, - ) -> Self::Key; - - /// Specializes the given `descriptor` according to the given `key`. - fn specialize( - key: Self::Key, - descriptor: &mut RenderPipelineDescriptor, - layout: &MeshVertexBufferLayout, - ) -> Result<(), SpecializedMeshPipelineError>; - - /// Returns this material's [`BindGroup`]. This should match the layout returned by [`SpecializedMaterial2d::bind_group_layout`]. - fn bind_group(material: &::PreparedAsset) -> &BindGroup; - - /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`SpecializedMaterial2d::bind_group`]. - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; - - /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn vertex_shader(asset_server: &AssetServer) -> Option> { - None - } - - /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. - /// Defaults to [`None`]. - #[allow(unused_variables)] - fn fragment_shader(asset_server: &AssetServer) -> Option> { - None - } - - /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. - /// Defaults to an empty array / no dynamic uniform indices. - #[allow(unused_variables)] - #[inline] - fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { - &[] - } -} - -/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`SpecializedMaterial2d`] +/// 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); +pub struct Material2dPlugin(PhantomData); -impl Default for Material2dPlugin { +impl Default for Material2dPlugin { fn default() -> Self { Self(Default::default()) } } -impl Plugin for Material2dPlugin { +impl Plugin for Material2dPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ fn build(&self, app: &mut App) { app.add_asset::() - .add_plugin(ExtractComponentPlugin::>::extract_visible()) - .add_plugin(RenderAssetPlugin::::default()); + .add_plugin(ExtractComponentPlugin::>::extract_visible()); 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_system_to_stage(RenderStage::Extract, extract_materials_2d::) + .add_system_to_stage( + RenderStage::Prepare, + prepare_materials_2d::.after(PrepareAssetLabel::PreAssetPrepare), + ) .add_system_to_stage(RenderStage::Queue, queue_material2d_meshes::); } } } -pub struct Material2dPipeline { +/// Render pipeline data for a given [`Material2d`] +pub struct Material2dPipeline { pub mesh2d_pipeline: Mesh2dPipeline, pub material2d_layout: BindGroupLayout, pub vertex_shader: Option>, @@ -214,14 +179,49 @@ pub struct Material2dPipeline { marker: PhantomData, } -#[derive(Eq, PartialEq, Clone, Hash)] -pub struct Material2dKey { +pub struct Material2dKey { pub mesh_key: Mesh2dPipelineKey, - pub material_key: T, + pub bind_group_data: M::Data, } -impl SpecializedMeshPipeline for Material2dPipeline { - type Key = Material2dKey; +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 SpecializedMeshPipeline for Material2dPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = Material2dKey; fn specialize( &self, @@ -242,12 +242,12 @@ impl SpecializedMeshPipeline for Material2dPipeline self.mesh2d_pipeline.mesh_layout.clone(), ]); - M::specialize(key.material_key, &mut descriptor, layout)?; + M::specialize(&mut descriptor, layout, key)?; Ok(descriptor) } } -impl FromWorld for Material2dPipeline { +impl FromWorld for Material2dPipeline { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); let render_device = world.resource::(); @@ -256,8 +256,16 @@ impl FromWorld for Material2dPipeline { Material2dPipeline { mesh2d_pipeline: world.resource::().clone(), material2d_layout, - vertex_shader: M::vertex_shader(asset_server), - fragment_shader: M::fragment_shader(asset_server), + 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, } } @@ -271,11 +279,9 @@ type DrawMaterial2d = ( DrawMesh2d, ); -pub struct SetMaterial2dBindGroup(PhantomData); -impl EntityRenderCommand - for SetMaterial2dBindGroup -{ - type Param = (SRes>, SQuery>>); +pub struct SetMaterial2dBindGroup(PhantomData); +impl EntityRenderCommand for SetMaterial2dBindGroup { + type Param = (SRes>, SQuery>>); fn render<'w>( _view: Entity, item: Entity, @@ -284,32 +290,28 @@ impl EntityRenderCommand ) -> RenderCommandResult { let material2d_handle = query.get(item).unwrap(); let material2d = materials.into_inner().get(material2d_handle).unwrap(); - pass.set_bind_group( - I, - M::bind_group(material2d), - M::dynamic_uniform_indices(material2d), - ); + pass.set_bind_group(I, &material2d.bind_group, &[]); RenderCommandResult::Success } } #[allow(clippy::too_many_arguments)] -pub fn queue_material2d_meshes( +pub fn queue_material2d_meshes( transparent_draw_functions: Res>, material2d_pipeline: Res>, mut pipelines: ResMut>>, mut pipeline_cache: ResMut, - render_device: Res, msaa: Res, render_meshes: Res>, - render_materials: Res>, + render_materials: Res>, material2d_meshes: Query<(&Handle, &Mesh2dHandle, &Mesh2dUniform)>, mut views: Query<(&VisibleEntities, &mut RenderPhase)>, -) { +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ if material2d_meshes.is_empty() { return; } - let render_device = render_device.into_inner(); for (visible_entities, mut transparent_phase) in &mut views { let draw_transparent_pbr = transparent_draw_functions .read() @@ -327,13 +329,12 @@ pub fn queue_material2d_meshes( let mesh_key = msaa_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); - let material_key = M::key(render_device, material2d); let pipeline_id = pipelines.specialize( &mut pipeline_cache, &material2d_pipeline, Material2dKey { mesh_key, - material_key, + bind_group_data: material2d.key.clone(), }, &mesh.layout, ); @@ -366,9 +367,151 @@ pub fn queue_material2d_meshes( } } -/// A component bundle for entities with a [`Mesh2dHandle`] and a [`SpecializedMaterial2d`]. +/// Data prepared for a [`Material2d`] instance. +pub struct PreparedMaterial2d { + pub bindings: Vec, + pub bind_group: BindGroup, + pub key: T::Data, +} + +struct ExtractedMaterials2d { + extracted: Vec<(Handle, 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. +pub type RenderMaterials2d = HashMap, PreparedMaterial2d>; + +/// This system extracts all created or modified assets of the corresponding [`Material2d`] type +/// into the "render world". +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.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + changed_assets.insert(handle.clone_weak()); + } + AssetEvent::Removed { handle } => { + changed_assets.remove(handle); + removed.push(handle.clone_weak()); + } + } + } + + let mut extracted_assets = Vec::new(); + for handle in changed_assets.drain() { + if let Some(asset) = assets.get(&handle) { + extracted_assets.push((handle, 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<(Handle, 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. +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 mut queued_assets = std::mem::take(&mut prepare_next_frame.assets); + for (handle, material) in queued_assets.drain(..) { + match prepare_material2d( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, material)); + } + } + } + + for removed in std::mem::take(&mut extracted_assets.removed) { + render_materials.remove(&removed); + } + + for (handle, 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(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, 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, + }) +} + +/// A component bundle for entities with a [`Mesh2dHandle`] and a [`Material2d`]. #[derive(Bundle, Clone)] -pub struct MaterialMesh2dBundle { +pub struct MaterialMesh2dBundle { pub mesh: Mesh2dHandle, pub material: Handle, pub transform: Transform, @@ -379,7 +522,7 @@ pub struct MaterialMesh2dBundle { pub computed_visibility: ComputedVisibility, } -impl Default for MaterialMesh2dBundle { +impl Default for MaterialMesh2dBundle { fn default() -> Self { Self { mesh: Default::default(), diff --git a/examples/shader/post_processing.rs b/examples/shader/post_processing.rs index 93a7d03b06a6b5..6c853cc6cdb4e9 100644 --- a/examples/shader/post_processing.rs +++ b/examples/shader/post_processing.rs @@ -5,22 +5,17 @@ use bevy::{ core_pipeline::clear_color::ClearColorConfig, - ecs::system::{lifetimeless::SRes, SystemParamItem}, prelude::*, reflect::TypeUuid, render::{ camera::{Camera, RenderTarget}, - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, render_resource::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, - Extent3d, SamplerBindingType, ShaderStages, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, TextureViewDimension, + AsBindGroup, Extent3d, ShaderRef, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, }, - renderer::RenderDevice, view::RenderLayers, }, - sprite::{Material2d, Material2dPipeline, Material2dPlugin, MaterialMesh2dBundle}, + sprite::{Material2d, Material2dPlugin, MaterialMesh2dBundle}, }; fn main() { @@ -44,7 +39,10 @@ fn setup( mut post_processing_materials: ResMut>, mut materials: ResMut>, mut images: ResMut>, + asset_server: Res, ) { + asset_server.watch_for_changes().unwrap(); + let window = windows.get_primary_mut().unwrap(); let size = Extent3d { width: window.physical_width(), @@ -166,92 +164,17 @@ fn main_camera_cube_rotator_system( // Region below declares of the custom material handling post processing effect /// Our custom post processing material -#[derive(TypeUuid, Clone)] +#[derive(AsBindGroup, TypeUuid, Clone)] #[uuid = "bc2f08eb-a0fb-43f1-a908-54871ea597d5"] struct PostProcessingMaterial { /// In this example, this image will be the result of the main camera. + #[texture(0)] + #[sampler(1)] source_image: Handle, } -struct PostProcessingMaterialGPU { - bind_group: BindGroup, -} - impl Material2d for PostProcessingMaterial { - fn bind_group(material: &PostProcessingMaterialGPU) -> &BindGroup { - &material.bind_group - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - label: None, - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - view_dimension: TextureViewDimension::D2, - sample_type: TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - }) - } - - fn fragment_shader(asset_server: &AssetServer) -> Option> { - asset_server.watch_for_changes().unwrap(); - Some(asset_server.load("shaders/custom_material_chromatic_aberration.wgsl")) - } -} - -impl RenderAsset for PostProcessingMaterial { - type ExtractedAsset = PostProcessingMaterial; - type PreparedAsset = PostProcessingMaterialGPU; - type Param = ( - SRes, - SRes>, - SRes>, - ); - - fn prepare_asset( - extracted_asset: PostProcessingMaterial, - (render_device, pipeline, images): &mut SystemParamItem, - ) -> Result> { - let (view, sampler) = if let Some(result) = pipeline - .mesh2d_pipeline - .get_image_texture(images, &Some(extracted_asset.source_image.clone())) - { - result - } else { - return Err(PrepareAssetError::RetryNextUpdate(extracted_asset)); - }; - - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - label: None, - layout: &pipeline.material2d_layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(sampler), - }, - ], - }); - Ok(PostProcessingMaterialGPU { bind_group }) - } - - fn extract_asset(&self) -> PostProcessingMaterial { - self.clone() + fn fragment_shader() -> ShaderRef { + "shaders/custom_material_chromatic_aberration.wgsl".into() } }