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