1use 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
207fn setup(
209 mut commands: Commands,
210 mut meshes: ResMut<Assets<Mesh>>,
211 mut materials: ResMut<Assets<ImageMaterial>>,
212 asset_server: Res<AssetServer>,
213) {
214 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 commands.spawn((
224 PointLight {
225 shadows_enabled: true,
226 ..default()
227 },
228 Transform::from_xyz(4.0, 8.0, 4.0),
229 ));
230 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 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 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 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}