1use 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
212fn setup(
214 mut commands: Commands,
215 mut meshes: ResMut<Assets<Mesh>>,
216 mut materials: ResMut<Assets<ImageMaterial>>,
217 asset_server: Res<AssetServer>,
218) {
219 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 commands.spawn((
229 PointLight {
230 shadow_maps_enabled: true,
231 ..default()
232 },
233 Transform::from_xyz(4.0, 8.0, 4.0),
234 ));
235 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 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 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 for entity in entities_needing_specialization.changed.iter() {
314 dirty_specializations
315 .changed_renderables
316 .insert(MainEntity::from(*entity));
317 }
318}
319
320fn 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}