# Objective Closes #18075 In order to enable a number of patterns for dynamic materials in the engine, it's necessary to decouple the renderer from the `Material` trait. This opens the possibility for: - Materials that aren't coupled to `AsBindGroup`. - 2d using the underlying 3d bindless infrastructure. - Dynamic materials that can change their layout at runtime. - Materials that aren't even backed by a Rust struct at all. ## Solution In short, remove all trait bounds from render world material systems and resources. This means moving a bunch of stuff onto `MaterialProperties` and engaging in some hacks to make specialization work. Rather than storing the bind group data in `MaterialBindGroupAllocator`, right now we're storing it in a closure on `MaterialProperties`. TBD if this has bad performance characteristics. ## Benchmarks - `many_cubes`: `cargo run --example many_cubes --release --features=bevy/trace_tracy -- --vary-material-data-per-instance`:  - @DGriffin91's Caldera `cargo run --release --features=bevy/trace_tracy -- --random-materials`  - @DGriffin91's Caldera with 20 unique material types (i.e. `MaterialPlugin<M>`) and random materials per mesh `cargo run --release --features=bevy/trace_tracy -- --random-materials`  ### TODO - We almost certainly lost some parallelization from removing the type params that could be gained back from smarter iteration. - Test all the things that could have broken. - ~Fix meshlets~ ## Showcase See [the example](https://github.com/bevyengine/bevy/pull/19667/files#diff-9d768cfe1c3aa81eff365d250d3cbe5a63e8df63e81dd85f64c3c3cd993f6d94) for a custom material implemented without the use of the `Material` trait and thus `AsBindGroup`.  --------- Co-authored-by: IceSentry <IceSentry@users.noreply.github.com> Co-authored-by: IceSentry <c.giguere42@gmail.com>
		
			
				
	
	
		
			316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! A simple 3D scene with light shining over a cube sitting on a plane.
 | 
						|
 | 
						|
use bevy::{
 | 
						|
    asset::{AsAssetId, AssetEventSystems},
 | 
						|
    core_pipeline::core_3d::Opaque3d,
 | 
						|
    ecs::system::{
 | 
						|
        lifetimeless::{SRes, SResMut},
 | 
						|
        SystemChangeTick, SystemParamItem,
 | 
						|
    },
 | 
						|
    pbr::{
 | 
						|
        DrawMaterial, EntitiesNeedingSpecialization, EntitySpecializationTicks,
 | 
						|
        MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialDrawFunction,
 | 
						|
        MaterialFragmentShader, MaterialProperties, PreparedMaterial, RenderMaterialBindings,
 | 
						|
        RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache,
 | 
						|
    },
 | 
						|
    platform::collections::hash_map::Entry,
 | 
						|
    prelude::*,
 | 
						|
    render::{
 | 
						|
        erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError},
 | 
						|
        render_asset::RenderAssets,
 | 
						|
        render_phase::DrawFunctions,
 | 
						|
        render_resource::{
 | 
						|
            binding_types::{sampler, texture_2d},
 | 
						|
            AsBindGroup, BindGroupLayout, BindGroupLayoutEntries, BindingResources,
 | 
						|
            OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
 | 
						|
            TextureSampleType, TextureViewDimension, UnpreparedBindGroup,
 | 
						|
        },
 | 
						|
        renderer::RenderDevice,
 | 
						|
        sync_world::MainEntity,
 | 
						|
        texture::GpuImage,
 | 
						|
        view::ExtractedView,
 | 
						|
        Extract, RenderApp,
 | 
						|
    },
 | 
						|
    utils::Parallel,
 | 
						|
};
 | 
						|
use std::{any::TypeId, sync::Arc};
 | 
						|
 | 
						|
const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl";
 | 
						|
 | 
						|
fn main() {
 | 
						|
    App::new()
 | 
						|
        .add_plugins((DefaultPlugins, ImageMaterialPlugin))
 | 
						|
        .add_systems(Startup, setup)
 | 
						|
        .run();
 | 
						|
}
 | 
						|
 | 
						|
struct ImageMaterialPlugin;
 | 
						|
 | 
						|
impl Plugin for ImageMaterialPlugin {
 | 
						|
    fn build(&self, app: &mut App) {
 | 
						|
        app.init_asset::<ImageMaterial>()
 | 
						|
            .add_plugins(ErasedRenderAssetPlugin::<ImageMaterial>::default())
 | 
						|
            .add_systems(
 | 
						|
                PostUpdate,
 | 
						|
                check_entities_needing_specialization.after(AssetEventSystems),
 | 
						|
            )
 | 
						|
            .init_resource::<EntitiesNeedingSpecialization<ImageMaterial>>();
 | 
						|
    }
 | 
						|
 | 
						|
    fn finish(&self, app: &mut App) {
 | 
						|
        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
 | 
						|
            return;
 | 
						|
        };
 | 
						|
 | 
						|
        render_app.add_systems(
 | 
						|
            ExtractSchedule,
 | 
						|
            (
 | 
						|
                extract_image_materials,
 | 
						|
                extract_image_materials_needing_specialization,
 | 
						|
            ),
 | 
						|
        );
 | 
						|
 | 
						|
        render_app.world_mut().resource_scope(
 | 
						|
            |world: &mut World, mut bind_group_allocators: Mut<MaterialBindGroupAllocators>| {
 | 
						|
                world.resource_scope(|world: &mut World, render_device: Mut<RenderDevice>| {
 | 
						|
                    let bind_group_layout = render_device.create_bind_group_layout(
 | 
						|
                        "image_material_layout",
 | 
						|
                        &BindGroupLayoutEntries::sequential(
 | 
						|
                            ShaderStages::FRAGMENT,
 | 
						|
                            (
 | 
						|
                                texture_2d(TextureSampleType::Float { filterable: false }),
 | 
						|
                                sampler(SamplerBindingType::NonFiltering),
 | 
						|
                            ),
 | 
						|
                        ),
 | 
						|
                    );
 | 
						|
                    let sampler = render_device.create_sampler(&SamplerDescriptor::default());
 | 
						|
                    world.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone()));
 | 
						|
                    world.insert_resource(ImageMaterialBindGroupSampler(sampler));
 | 
						|
 | 
						|
                    bind_group_allocators.insert(
 | 
						|
                        TypeId::of::<ImageMaterial>(),
 | 
						|
                        MaterialBindGroupAllocator::new(
 | 
						|
                            &render_device,
 | 
						|
                            None,
 | 
						|
                            None,
 | 
						|
                            bind_group_layout,
 | 
						|
                            None,
 | 
						|
                        ),
 | 
						|
                    );
 | 
						|
                });
 | 
						|
            },
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Resource)]
 | 
						|
struct ImageMaterialBindGroupLayout(BindGroupLayout);
 | 
						|
 | 
						|
#[derive(Resource)]
 | 
						|
struct ImageMaterialBindGroupSampler(Sampler);
 | 
						|
 | 
						|
#[derive(Component)]
 | 
						|
struct ImageMaterial3d(Handle<ImageMaterial>);
 | 
						|
 | 
						|
impl AsAssetId for ImageMaterial3d {
 | 
						|
    type Asset = ImageMaterial;
 | 
						|
 | 
						|
    fn as_asset_id(&self) -> AssetId<Self::Asset> {
 | 
						|
        self.0.id()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
 | 
						|
struct ImageMaterial {
 | 
						|
    image: Handle<Image>,
 | 
						|
}
 | 
						|
 | 
						|
impl ErasedRenderAsset for ImageMaterial {
 | 
						|
    type SourceAsset = ImageMaterial;
 | 
						|
    type ErasedAsset = PreparedMaterial;
 | 
						|
    type Param = (
 | 
						|
        SRes<DrawFunctions<Opaque3d>>,
 | 
						|
        SRes<ImageMaterialBindGroupLayout>,
 | 
						|
        SRes<AssetServer>,
 | 
						|
        SResMut<MaterialBindGroupAllocators>,
 | 
						|
        SResMut<RenderMaterialBindings>,
 | 
						|
        SRes<RenderAssets<GpuImage>>,
 | 
						|
        SRes<ImageMaterialBindGroupSampler>,
 | 
						|
    );
 | 
						|
 | 
						|
    fn prepare_asset(
 | 
						|
        source_asset: Self::SourceAsset,
 | 
						|
        asset_id: AssetId<Self::SourceAsset>,
 | 
						|
        (
 | 
						|
            opaque_draw_functions,
 | 
						|
            material_layout,
 | 
						|
            asset_server,
 | 
						|
            bind_group_allocators,
 | 
						|
            render_material_bindings,
 | 
						|
            gpu_images,
 | 
						|
            image_material_sampler,
 | 
						|
        ): &mut SystemParamItem<Self::Param>,
 | 
						|
    ) -> std::result::Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>> {
 | 
						|
        let material_layout = material_layout.0.clone();
 | 
						|
        let draw_function_id = opaque_draw_functions.read().id::<DrawMaterial>();
 | 
						|
        let bind_group_allocator = bind_group_allocators
 | 
						|
            .get_mut(&TypeId::of::<ImageMaterial>())
 | 
						|
            .unwrap();
 | 
						|
        let Some(image) = gpu_images.get(&source_asset.image) else {
 | 
						|
            return Err(PrepareAssetError::RetryNextUpdate(source_asset));
 | 
						|
        };
 | 
						|
        let unprepared = UnpreparedBindGroup {
 | 
						|
            bindings: BindingResources(vec![
 | 
						|
                (
 | 
						|
                    0,
 | 
						|
                    OwnedBindingResource::TextureView(
 | 
						|
                        TextureViewDimension::D2,
 | 
						|
                        image.texture_view.clone(),
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
                (
 | 
						|
                    1,
 | 
						|
                    OwnedBindingResource::Sampler(
 | 
						|
                        SamplerBindingType::NonFiltering,
 | 
						|
                        image_material_sampler.0.clone(),
 | 
						|
                    ),
 | 
						|
                ),
 | 
						|
            ]),
 | 
						|
        };
 | 
						|
        let binding = match render_material_bindings.entry(asset_id.into()) {
 | 
						|
            Entry::Occupied(mut occupied_entry) => {
 | 
						|
                bind_group_allocator.free(*occupied_entry.get());
 | 
						|
                let new_binding =
 | 
						|
                    bind_group_allocator.allocate_unprepared(unprepared, &material_layout);
 | 
						|
                *occupied_entry.get_mut() = new_binding;
 | 
						|
                new_binding
 | 
						|
            }
 | 
						|
            Entry::Vacant(vacant_entry) => *vacant_entry
 | 
						|
                .insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)),
 | 
						|
        };
 | 
						|
 | 
						|
        let mut properties = MaterialProperties {
 | 
						|
            material_layout: Some(material_layout),
 | 
						|
            ..Default::default()
 | 
						|
        };
 | 
						|
        properties.add_draw_function(MaterialDrawFunction, draw_function_id);
 | 
						|
        properties.add_shader(MaterialFragmentShader, asset_server.load(SHADER_ASSET_PATH));
 | 
						|
 | 
						|
        Ok(PreparedMaterial {
 | 
						|
            binding,
 | 
						|
            properties: Arc::new(properties),
 | 
						|
        })
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// set up a simple 3D scene
 | 
						|
fn setup(
 | 
						|
    mut commands: Commands,
 | 
						|
    mut meshes: ResMut<Assets<Mesh>>,
 | 
						|
    mut materials: ResMut<Assets<ImageMaterial>>,
 | 
						|
    asset_server: Res<AssetServer>,
 | 
						|
) {
 | 
						|
    // cube
 | 
						|
    commands.spawn((
 | 
						|
        Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
 | 
						|
        ImageMaterial3d(materials.add(ImageMaterial {
 | 
						|
            image: asset_server.load("branding/icon.png"),
 | 
						|
        })),
 | 
						|
        Transform::from_xyz(0.0, 0.5, 0.0),
 | 
						|
    ));
 | 
						|
    // light
 | 
						|
    commands.spawn((
 | 
						|
        PointLight {
 | 
						|
            shadows_enabled: true,
 | 
						|
            ..default()
 | 
						|
        },
 | 
						|
        Transform::from_xyz(4.0, 8.0, 4.0),
 | 
						|
    ));
 | 
						|
    // camera
 | 
						|
    commands.spawn((
 | 
						|
        Camera3d::default(),
 | 
						|
        Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
 | 
						|
    ));
 | 
						|
}
 | 
						|
 | 
						|
fn extract_image_materials(
 | 
						|
    mut material_instances: ResMut<RenderMaterialInstances>,
 | 
						|
    changed_meshes_query: Extract<
 | 
						|
        Query<
 | 
						|
            (Entity, &ViewVisibility, &ImageMaterial3d),
 | 
						|
            Or<(Changed<ViewVisibility>, Changed<ImageMaterial3d>)>,
 | 
						|
        >,
 | 
						|
    >,
 | 
						|
) {
 | 
						|
    let last_change_tick = material_instances.current_change_tick;
 | 
						|
 | 
						|
    for (entity, view_visibility, material) in &changed_meshes_query {
 | 
						|
        if view_visibility.get() {
 | 
						|
            material_instances.instances.insert(
 | 
						|
                entity.into(),
 | 
						|
                RenderMaterialInstance {
 | 
						|
                    asset_id: material.0.id().untyped(),
 | 
						|
                    last_change_tick,
 | 
						|
                },
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            material_instances
 | 
						|
                .instances
 | 
						|
                .remove(&MainEntity::from(entity));
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
fn check_entities_needing_specialization(
 | 
						|
    needs_specialization: Query<
 | 
						|
        Entity,
 | 
						|
        (
 | 
						|
            Or<(
 | 
						|
                Changed<Mesh3d>,
 | 
						|
                AssetChanged<Mesh3d>,
 | 
						|
                Changed<ImageMaterial3d>,
 | 
						|
                AssetChanged<ImageMaterial3d>,
 | 
						|
            )>,
 | 
						|
            With<ImageMaterial3d>,
 | 
						|
        ),
 | 
						|
    >,
 | 
						|
    mut par_local: Local<Parallel<Vec<Entity>>>,
 | 
						|
    mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<ImageMaterial>>,
 | 
						|
) {
 | 
						|
    entities_needing_specialization.clear();
 | 
						|
 | 
						|
    needs_specialization
 | 
						|
        .par_iter()
 | 
						|
        .for_each(|entity| par_local.borrow_local_mut().push(entity));
 | 
						|
 | 
						|
    par_local.drain_into(&mut entities_needing_specialization);
 | 
						|
}
 | 
						|
 | 
						|
fn extract_image_materials_needing_specialization(
 | 
						|
    entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<ImageMaterial>>>,
 | 
						|
    mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
 | 
						|
    mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
 | 
						|
    mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
 | 
						|
    views: Query<&ExtractedView>,
 | 
						|
    ticks: SystemChangeTick,
 | 
						|
) {
 | 
						|
    // Clean up any despawned entities, we do this first in case the removed material was re-added
 | 
						|
    // the same frame, thus will appear both in the removed components list and have been added to
 | 
						|
    // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
 | 
						|
    for entity in removed_mesh_material_components.read() {
 | 
						|
        entity_specialization_ticks.remove(&MainEntity::from(entity));
 | 
						|
        for view in views {
 | 
						|
            if let Some(cache) =
 | 
						|
                specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
 | 
						|
            {
 | 
						|
                cache.remove(&MainEntity::from(entity));
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    for entity in entities_needing_specialization.iter() {
 | 
						|
        // Update the entity's specialization tick with this run's tick
 | 
						|
        entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
 | 
						|
    }
 | 
						|
}
 |