Skip to main content

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