manual_material/
manual_material.rs

1//! A simple 3D scene with light shining over a cube sitting on a plane.
2
3use bevy::{
4    asset::{AsAssetId, AssetEventSystems},
5    core_pipeline::core_3d::Opaque3d,
6    ecs::system::{
7        lifetimeless::{SRes, SResMut},
8        SystemChangeTick, SystemParamItem,
9    },
10    pbr::{
11        late_sweep_material_instances, DrawMaterial, EntitiesNeedingSpecialization,
12        EntitySpecializationTickPair, EntitySpecializationTicks, MaterialBindGroupAllocator,
13        MaterialBindGroupAllocators, MaterialDrawFunction,
14        MaterialExtractEntitiesNeedingSpecializationSystems, MaterialExtractionSystems,
15        MaterialFragmentShader, MaterialProperties, PreparedMaterial, RenderMaterialBindings,
16        RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache,
17    },
18    platform::collections::hash_map::Entry,
19    prelude::*,
20    render::{
21        erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError},
22        render_asset::RenderAssets,
23        render_phase::DrawFunctions,
24        render_resource::{
25            binding_types::{sampler, texture_2d},
26            AsBindGroup, BindGroupLayout, BindGroupLayoutEntries, BindingResources,
27            OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
28            TextureSampleType, TextureViewDimension, UnpreparedBindGroup,
29        },
30        renderer::RenderDevice,
31        sync_world::MainEntity,
32        texture::GpuImage,
33        view::ExtractedView,
34        Extract, RenderApp, RenderStartup,
35    },
36    utils::Parallel,
37};
38use std::{any::TypeId, sync::Arc};
39
40const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl";
41
42fn main() {
43    App::new()
44        .add_plugins((DefaultPlugins, ImageMaterialPlugin))
45        .add_systems(Startup, setup)
46        .run();
47}
48
49struct ImageMaterialPlugin;
50
51impl Plugin for ImageMaterialPlugin {
52    fn build(&self, app: &mut App) {
53        app.init_asset::<ImageMaterial>()
54            .add_plugins(ErasedRenderAssetPlugin::<ImageMaterial>::default())
55            .add_systems(
56                PostUpdate,
57                check_entities_needing_specialization.after(AssetEventSystems),
58            )
59            .init_resource::<EntitiesNeedingSpecialization<ImageMaterial>>();
60
61        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
62            return;
63        };
64
65        render_app
66            .add_systems(RenderStartup, init_image_material_resources)
67            .add_systems(
68                ExtractSchedule,
69                (
70                    extract_image_materials,
71                    extract_image_materials_needing_specialization
72                        .in_set(MaterialExtractEntitiesNeedingSpecializationSystems),
73                    sweep_image_materials_needing_specialization
74                        .after(MaterialExtractEntitiesNeedingSpecializationSystems)
75                        .after(MaterialExtractionSystems)
76                        .before(late_sweep_material_instances),
77                ),
78            );
79    }
80}
81
82fn init_image_material_resources(
83    mut commands: Commands,
84    render_device: Res<RenderDevice>,
85    mut bind_group_allocators: ResMut<MaterialBindGroupAllocators>,
86) {
87    let bind_group_layout = render_device.create_bind_group_layout(
88        "image_material_layout",
89        &BindGroupLayoutEntries::sequential(
90            ShaderStages::FRAGMENT,
91            (
92                texture_2d(TextureSampleType::Float { filterable: false }),
93                sampler(SamplerBindingType::NonFiltering),
94            ),
95        ),
96    );
97    let sampler = render_device.create_sampler(&SamplerDescriptor::default());
98    commands.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone()));
99    commands.insert_resource(ImageMaterialBindGroupSampler(sampler));
100
101    bind_group_allocators.insert(
102        TypeId::of::<ImageMaterial>(),
103        MaterialBindGroupAllocator::new(&render_device, None, None, bind_group_layout, None),
104    );
105}
106
107#[derive(Resource)]
108struct ImageMaterialBindGroupLayout(BindGroupLayout);
109
110#[derive(Resource)]
111struct ImageMaterialBindGroupSampler(Sampler);
112
113#[derive(Component)]
114struct ImageMaterial3d(Handle<ImageMaterial>);
115
116impl AsAssetId for ImageMaterial3d {
117    type Asset = ImageMaterial;
118
119    fn as_asset_id(&self) -> AssetId<Self::Asset> {
120        self.0.id()
121    }
122}
123
124#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
125struct ImageMaterial {
126    image: Handle<Image>,
127}
128
129impl ErasedRenderAsset for ImageMaterial {
130    type SourceAsset = ImageMaterial;
131    type ErasedAsset = PreparedMaterial;
132    type Param = (
133        SRes<DrawFunctions<Opaque3d>>,
134        SRes<ImageMaterialBindGroupLayout>,
135        SRes<AssetServer>,
136        SResMut<MaterialBindGroupAllocators>,
137        SResMut<RenderMaterialBindings>,
138        SRes<RenderAssets<GpuImage>>,
139        SRes<ImageMaterialBindGroupSampler>,
140    );
141
142    fn prepare_asset(
143        source_asset: Self::SourceAsset,
144        asset_id: AssetId<Self::SourceAsset>,
145        (
146            opaque_draw_functions,
147            material_layout,
148            asset_server,
149            bind_group_allocators,
150            render_material_bindings,
151            gpu_images,
152            image_material_sampler,
153        ): &mut SystemParamItem<Self::Param>,
154    ) -> std::result::Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>> {
155        let material_layout = material_layout.0.clone();
156        let draw_function_id = opaque_draw_functions.read().id::<DrawMaterial>();
157        let bind_group_allocator = bind_group_allocators
158            .get_mut(&TypeId::of::<ImageMaterial>())
159            .unwrap();
160        let Some(image) = gpu_images.get(&source_asset.image) else {
161            return Err(PrepareAssetError::RetryNextUpdate(source_asset));
162        };
163        let unprepared = UnpreparedBindGroup {
164            bindings: BindingResources(vec![
165                (
166                    0,
167                    OwnedBindingResource::TextureView(
168                        TextureViewDimension::D2,
169                        image.texture_view.clone(),
170                    ),
171                ),
172                (
173                    1,
174                    OwnedBindingResource::Sampler(
175                        SamplerBindingType::NonFiltering,
176                        image_material_sampler.0.clone(),
177                    ),
178                ),
179            ]),
180        };
181        let binding = match render_material_bindings.entry(asset_id.into()) {
182            Entry::Occupied(mut occupied_entry) => {
183                bind_group_allocator.free(*occupied_entry.get());
184                let new_binding =
185                    bind_group_allocator.allocate_unprepared(unprepared, &material_layout);
186                *occupied_entry.get_mut() = new_binding;
187                new_binding
188            }
189            Entry::Vacant(vacant_entry) => *vacant_entry
190                .insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)),
191        };
192
193        let mut properties = MaterialProperties {
194            material_layout: Some(material_layout),
195            ..Default::default()
196        };
197        properties.add_draw_function(MaterialDrawFunction, draw_function_id);
198        properties.add_shader(MaterialFragmentShader, asset_server.load(SHADER_ASSET_PATH));
199
200        Ok(PreparedMaterial {
201            binding,
202            properties: Arc::new(properties),
203        })
204    }
205}
206
207/// set up a simple 3D scene
208fn setup(
209    mut commands: Commands,
210    mut meshes: ResMut<Assets<Mesh>>,
211    mut materials: ResMut<Assets<ImageMaterial>>,
212    asset_server: Res<AssetServer>,
213) {
214    // cube
215    commands.spawn((
216        Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
217        ImageMaterial3d(materials.add(ImageMaterial {
218            image: asset_server.load("branding/icon.png"),
219        })),
220        Transform::from_xyz(0.0, 0.5, 0.0),
221    ));
222    // light
223    commands.spawn((
224        PointLight {
225            shadows_enabled: true,
226            ..default()
227        },
228        Transform::from_xyz(4.0, 8.0, 4.0),
229    ));
230    // camera
231    commands.spawn((
232        Camera3d::default(),
233        Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
234    ));
235}
236
237fn extract_image_materials(
238    mut material_instances: ResMut<RenderMaterialInstances>,
239    changed_meshes_query: Extract<
240        Query<
241            (Entity, &ViewVisibility, &ImageMaterial3d),
242            Or<(Changed<ViewVisibility>, Changed<ImageMaterial3d>)>,
243        >,
244    >,
245) {
246    let last_change_tick = material_instances.current_change_tick;
247
248    for (entity, view_visibility, material) in &changed_meshes_query {
249        if view_visibility.get() {
250            material_instances.instances.insert(
251                entity.into(),
252                RenderMaterialInstance {
253                    asset_id: material.0.id().untyped(),
254                    last_change_tick,
255                },
256            );
257        } else {
258            material_instances
259                .instances
260                .remove(&MainEntity::from(entity));
261        }
262    }
263}
264
265fn check_entities_needing_specialization(
266    needs_specialization: Query<
267        Entity,
268        (
269            Or<(
270                Changed<Mesh3d>,
271                AssetChanged<Mesh3d>,
272                Changed<ImageMaterial3d>,
273                AssetChanged<ImageMaterial3d>,
274            )>,
275            With<ImageMaterial3d>,
276        ),
277    >,
278    mut par_local: Local<Parallel<Vec<Entity>>>,
279    mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<ImageMaterial>>,
280) {
281    entities_needing_specialization.clear();
282
283    needs_specialization
284        .par_iter()
285        .for_each(|entity| par_local.borrow_local_mut().push(entity));
286
287    par_local.drain_into(&mut entities_needing_specialization);
288}
289
290fn extract_image_materials_needing_specialization(
291    entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<ImageMaterial>>>,
292    mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
293    mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
294    mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
295    render_material_instances: Res<RenderMaterialInstances>,
296    views: Query<&ExtractedView>,
297    ticks: SystemChangeTick,
298) {
299    // Clean up any despawned entities, we do this first in case the removed material was re-added
300    // the same frame, thus will appear both in the removed components list and have been added to
301    // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
302    for entity in removed_mesh_material_components.read() {
303        entity_specialization_ticks.remove(&MainEntity::from(entity));
304        for view in views {
305            if let Some(cache) =
306                specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
307            {
308                cache.remove(&MainEntity::from(entity));
309            }
310        }
311    }
312
313    for entity in entities_needing_specialization.iter() {
314        // Update the entity's specialization tick with this run's tick
315        entity_specialization_ticks.insert(
316            (*entity).into(),
317            EntitySpecializationTickPair {
318                system_tick: ticks.this_run(),
319                material_instances_tick: render_material_instances.current_change_tick,
320            },
321        );
322    }
323}
324
325fn sweep_image_materials_needing_specialization(
326    mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
327    mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
328    mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
329    render_material_instances: Res<RenderMaterialInstances>,
330    views: Query<&ExtractedView>,
331) {
332    // Clean up any despawned entities, we do this first in case the removed material was re-added
333    // the same frame, thus will appear both in the removed components list and have been added to
334    // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
335    for entity in removed_mesh_material_components.read() {
336        if entity_specialization_ticks
337            .get(&MainEntity::from(entity))
338            .is_some_and(|ticks| {
339                ticks.material_instances_tick == render_material_instances.current_change_tick
340            })
341        {
342            continue;
343        }
344
345        entity_specialization_ticks.remove(&MainEntity::from(entity));
346
347        for view in views {
348            if let Some(cache) =
349                specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
350            {
351                cache.remove(&MainEntity::from(entity));
352            }
353        }
354    }
355}