Path: blob/main/examples/shader_advanced/manual_material.rs
9354 views
//! A simple 3D scene with light shining over a cube sitting on a plane.12use bevy::{3asset::{AsAssetId, AssetEventSystems},4core_pipeline::core_3d::Opaque3d,5ecs::system::{6lifetimeless::{SRes, SResMut},7SystemChangeTick, SystemParamItem,8},9material::{key::ErasedMeshPipelineKey, MaterialProperties},10pbr::{11base_specialize, late_sweep_material_instances, DrawMaterial,12EntitiesNeedingSpecialization, EntitySpecializationTickPair, EntitySpecializationTicks,13MainPassOpaqueDrawFunction, MaterialBindGroupAllocator, MaterialBindGroupAllocators,14MaterialExtractEntitiesNeedingSpecializationSystems, MaterialExtractionSystems,15MaterialFragmentShader, MeshPipelineKey, PreparedMaterial, RenderMaterialBindings,16RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache,17},18platform::collections::hash_map::Entry,19prelude::*,20render::{21erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError},22render_asset::RenderAssets,23render_phase::DrawFunctions,24render_resource::{25binding_types::{sampler, texture_2d},26AsBindGroup, BindGroupLayoutDescriptor, BindGroupLayoutEntries, BindingResources,27OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,28TextureSampleType, TextureViewDimension, UnpreparedBindGroup,29},30renderer::RenderDevice,31sync_world::MainEntity,32texture::GpuImage,33view::ExtractedView,34Extract, RenderApp, RenderStartup,35},36utils::Parallel,37};38use std::{any::TypeId, sync::Arc};3940const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl";4142fn main() {43App::new()44.add_plugins((DefaultPlugins, ImageMaterialPlugin))45.add_systems(Startup, setup)46.run();47}4849struct ImageMaterialPlugin;5051impl Plugin for ImageMaterialPlugin {52fn build(&self, app: &mut App) {53app.init_asset::<ImageMaterial>()54.add_plugins(ErasedRenderAssetPlugin::<ImageMaterial>::default())55.add_systems(56PostUpdate,57check_entities_needing_specialization.after(AssetEventSystems),58)59.init_resource::<EntitiesNeedingSpecialization<ImageMaterial>>();6061let Some(render_app) = app.get_sub_app_mut(RenderApp) else {62return;63};6465render_app66.add_systems(RenderStartup, init_image_material_resources)67.add_systems(68ExtractSchedule,69(70extract_image_materials,71extract_image_materials_needing_specialization72.in_set(MaterialExtractEntitiesNeedingSpecializationSystems),73sweep_image_materials_needing_specialization74.after(MaterialExtractEntitiesNeedingSpecializationSystems)75.after(MaterialExtractionSystems)76.before(late_sweep_material_instances),77),78);79}80}8182fn init_image_material_resources(83mut commands: Commands,84render_device: Res<RenderDevice>,85mut bind_group_allocators: ResMut<MaterialBindGroupAllocators>,86) {87let bind_group_layout = BindGroupLayoutDescriptor::new(88"image_material_layout",89&BindGroupLayoutEntries::sequential(90ShaderStages::FRAGMENT,91(92texture_2d(TextureSampleType::Float { filterable: false }),93sampler(SamplerBindingType::NonFiltering),94),95),96);97let sampler = render_device.create_sampler(&SamplerDescriptor::default());98commands.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone()));99commands.insert_resource(ImageMaterialBindGroupSampler(sampler));100101bind_group_allocators.insert(102TypeId::of::<ImageMaterial>(),103MaterialBindGroupAllocator::new(104&render_device,105"image_material_allocator",106None,107bind_group_layout,108None,109),110);111}112113#[derive(Resource)]114struct ImageMaterialBindGroupLayout(BindGroupLayoutDescriptor);115116#[derive(Resource)]117struct ImageMaterialBindGroupSampler(Sampler);118119#[derive(Component)]120struct ImageMaterial3d(Handle<ImageMaterial>);121122impl AsAssetId for ImageMaterial3d {123type Asset = ImageMaterial;124125fn as_asset_id(&self) -> AssetId<Self::Asset> {126self.0.id()127}128}129130#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]131struct ImageMaterial {132image: Handle<Image>,133}134135impl ErasedRenderAsset for ImageMaterial {136type SourceAsset = ImageMaterial;137type ErasedAsset = PreparedMaterial;138type Param = (139SRes<DrawFunctions<Opaque3d>>,140SRes<ImageMaterialBindGroupLayout>,141SRes<AssetServer>,142SResMut<MaterialBindGroupAllocators>,143SResMut<RenderMaterialBindings>,144SRes<RenderAssets<GpuImage>>,145SRes<ImageMaterialBindGroupSampler>,146);147148fn prepare_asset(149source_asset: Self::SourceAsset,150asset_id: AssetId<Self::SourceAsset>,151(152opaque_draw_functions,153material_layout,154asset_server,155bind_group_allocators,156render_material_bindings,157gpu_images,158image_material_sampler,159): &mut SystemParamItem<Self::Param>,160) -> std::result::Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>> {161let material_layout = material_layout.0.clone();162let draw_function_id = opaque_draw_functions.read().id::<DrawMaterial>();163let bind_group_allocator = bind_group_allocators164.get_mut(&TypeId::of::<ImageMaterial>())165.unwrap();166let Some(image) = gpu_images.get(&source_asset.image) else {167return Err(PrepareAssetError::RetryNextUpdate(source_asset));168};169let unprepared = UnpreparedBindGroup {170bindings: BindingResources(vec![171(1720,173OwnedBindingResource::TextureView(174TextureViewDimension::D2,175image.texture_view.clone(),176),177),178(1791,180OwnedBindingResource::Sampler(181SamplerBindingType::NonFiltering,182image_material_sampler.0.clone(),183),184),185]),186};187let binding = match render_material_bindings.entry(asset_id.into()) {188Entry::Occupied(mut occupied_entry) => {189bind_group_allocator.free(*occupied_entry.get());190let new_binding =191bind_group_allocator.allocate_unprepared(unprepared, &material_layout);192*occupied_entry.get_mut() = new_binding;193new_binding194}195Entry::Vacant(vacant_entry) => *vacant_entry196.insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)),197};198199let mut properties = MaterialProperties {200material_layout: Some(material_layout),201mesh_pipeline_key_bits: ErasedMeshPipelineKey::new(MeshPipelineKey::empty()),202base_specialize: Some(base_specialize),203..Default::default()204};205properties.add_draw_function(MainPassOpaqueDrawFunction, draw_function_id);206properties.add_shader(MaterialFragmentShader, asset_server.load(SHADER_ASSET_PATH));207208Ok(PreparedMaterial {209binding,210properties: Arc::new(properties),211})212}213}214215/// set up a simple 3D scene216fn setup(217mut commands: Commands,218mut meshes: ResMut<Assets<Mesh>>,219mut materials: ResMut<Assets<ImageMaterial>>,220asset_server: Res<AssetServer>,221) {222// cube223commands.spawn((224Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),225ImageMaterial3d(materials.add(ImageMaterial {226image: asset_server.load("branding/icon.png"),227})),228Transform::from_xyz(0.0, 0.5, 0.0),229));230// light231commands.spawn((232PointLight {233shadow_maps_enabled: true,234..default()235},236Transform::from_xyz(4.0, 8.0, 4.0),237));238// camera239commands.spawn((240Camera3d::default(),241Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),242));243}244245fn extract_image_materials(246mut material_instances: ResMut<RenderMaterialInstances>,247changed_meshes_query: Extract<248Query<249(Entity, &ViewVisibility, &ImageMaterial3d),250Or<(Changed<ViewVisibility>, Changed<ImageMaterial3d>)>,251>,252>,253) {254let last_change_tick = material_instances.current_change_tick;255256for (entity, view_visibility, material) in &changed_meshes_query {257if view_visibility.get() {258material_instances.instances.insert(259entity.into(),260RenderMaterialInstance {261asset_id: material.0.id().untyped(),262last_change_tick,263},264);265} else {266material_instances267.instances268.remove(&MainEntity::from(entity));269}270}271}272273fn check_entities_needing_specialization(274needs_specialization: Query<275Entity,276(277Or<(278Changed<Mesh3d>,279AssetChanged<Mesh3d>,280Changed<ImageMaterial3d>,281AssetChanged<ImageMaterial3d>,282)>,283With<ImageMaterial3d>,284),285>,286mut par_local: Local<Parallel<Vec<Entity>>>,287mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<ImageMaterial>>,288) {289entities_needing_specialization.clear();290291needs_specialization292.par_iter()293.for_each(|entity| par_local.borrow_local_mut().push(entity));294295par_local.drain_into(&mut entities_needing_specialization);296}297298fn extract_image_materials_needing_specialization(299entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<ImageMaterial>>>,300mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,301mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,302mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,303render_material_instances: Res<RenderMaterialInstances>,304views: Query<&ExtractedView>,305ticks: SystemChangeTick,306) {307// Clean up any despawned entities, we do this first in case the removed material was re-added308// the same frame, thus will appear both in the removed components list and have been added to309// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter310for entity in removed_mesh_material_components.read() {311entity_specialization_ticks.remove(&MainEntity::from(entity));312for view in views {313if let Some(cache) =314specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)315{316cache.remove(&MainEntity::from(entity));317}318}319}320321for entity in entities_needing_specialization.iter() {322// Update the entity's specialization tick with this run's tick323entity_specialization_ticks.insert(324(*entity).into(),325EntitySpecializationTickPair {326system_tick: ticks.this_run(),327material_instances_tick: render_material_instances.current_change_tick,328},329);330}331}332333fn sweep_image_materials_needing_specialization(334mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,335mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,336mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,337render_material_instances: Res<RenderMaterialInstances>,338views: Query<&ExtractedView>,339) {340// Clean up any despawned entities, we do this first in case the removed material was re-added341// the same frame, thus will appear both in the removed components list and have been added to342// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter343for entity in removed_mesh_material_components.read() {344if entity_specialization_ticks345.get(&MainEntity::from(entity))346.is_some_and(|ticks| {347ticks.material_instances_tick == render_material_instances.current_change_tick348})349{350continue;351}352353entity_specialization_ticks.remove(&MainEntity::from(entity));354355for view in views {356if let Some(cache) =357specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)358{359cache.remove(&MainEntity::from(entity));360}361}362}363}364365366