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