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 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
200fn setup(
202 mut commands: Commands,
203 mut meshes: ResMut<Assets<Mesh>>,
204 mut materials: ResMut<Assets<ImageMaterial>>,
205 asset_server: Res<AssetServer>,
206) {
207 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 commands.spawn((
217 PointLight {
218 shadows_enabled: true,
219 ..default()
220 },
221 Transform::from_xyz(4.0, 8.0, 4.0),
222 ));
223 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 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 entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
308 }
309}