Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/shader_advanced/manual_material.rs
9354 views
1
//! A simple 3D scene with light shining over a cube sitting on a plane.
2
3
use bevy::{
4
asset::{AsAssetId, AssetEventSystems},
5
core_pipeline::core_3d::Opaque3d,
6
ecs::system::{
7
lifetimeless::{SRes, SResMut},
8
SystemChangeTick, SystemParamItem,
9
},
10
material::{key::ErasedMeshPipelineKey, MaterialProperties},
11
pbr::{
12
base_specialize, late_sweep_material_instances, DrawMaterial,
13
EntitiesNeedingSpecialization, EntitySpecializationTickPair, EntitySpecializationTicks,
14
MainPassOpaqueDrawFunction, MaterialBindGroupAllocator, MaterialBindGroupAllocators,
15
MaterialExtractEntitiesNeedingSpecializationSystems, MaterialExtractionSystems,
16
MaterialFragmentShader, MeshPipelineKey, PreparedMaterial, RenderMaterialBindings,
17
RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache,
18
},
19
platform::collections::hash_map::Entry,
20
prelude::*,
21
render::{
22
erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError},
23
render_asset::RenderAssets,
24
render_phase::DrawFunctions,
25
render_resource::{
26
binding_types::{sampler, texture_2d},
27
AsBindGroup, BindGroupLayoutDescriptor, BindGroupLayoutEntries, BindingResources,
28
OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
29
TextureSampleType, TextureViewDimension, UnpreparedBindGroup,
30
},
31
renderer::RenderDevice,
32
sync_world::MainEntity,
33
texture::GpuImage,
34
view::ExtractedView,
35
Extract, RenderApp, RenderStartup,
36
},
37
utils::Parallel,
38
};
39
use std::{any::TypeId, sync::Arc};
40
41
const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl";
42
43
fn main() {
44
App::new()
45
.add_plugins((DefaultPlugins, ImageMaterialPlugin))
46
.add_systems(Startup, setup)
47
.run();
48
}
49
50
struct ImageMaterialPlugin;
51
52
impl 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(MaterialExtractEntitiesNeedingSpecializationSystems),
74
sweep_image_materials_needing_specialization
75
.after(MaterialExtractEntitiesNeedingSpecializationSystems)
76
.after(MaterialExtractionSystems)
77
.before(late_sweep_material_instances),
78
),
79
);
80
}
81
}
82
83
fn init_image_material_resources(
84
mut commands: Commands,
85
render_device: Res<RenderDevice>,
86
mut bind_group_allocators: ResMut<MaterialBindGroupAllocators>,
87
) {
88
let bind_group_layout = BindGroupLayoutDescriptor::new(
89
"image_material_layout",
90
&BindGroupLayoutEntries::sequential(
91
ShaderStages::FRAGMENT,
92
(
93
texture_2d(TextureSampleType::Float { filterable: false }),
94
sampler(SamplerBindingType::NonFiltering),
95
),
96
),
97
);
98
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
99
commands.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone()));
100
commands.insert_resource(ImageMaterialBindGroupSampler(sampler));
101
102
bind_group_allocators.insert(
103
TypeId::of::<ImageMaterial>(),
104
MaterialBindGroupAllocator::new(
105
&render_device,
106
"image_material_allocator",
107
None,
108
bind_group_layout,
109
None,
110
),
111
);
112
}
113
114
#[derive(Resource)]
115
struct ImageMaterialBindGroupLayout(BindGroupLayoutDescriptor);
116
117
#[derive(Resource)]
118
struct ImageMaterialBindGroupSampler(Sampler);
119
120
#[derive(Component)]
121
struct ImageMaterial3d(Handle<ImageMaterial>);
122
123
impl AsAssetId for ImageMaterial3d {
124
type Asset = ImageMaterial;
125
126
fn as_asset_id(&self) -> AssetId<Self::Asset> {
127
self.0.id()
128
}
129
}
130
131
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
132
struct ImageMaterial {
133
image: Handle<Image>,
134
}
135
136
impl ErasedRenderAsset for ImageMaterial {
137
type SourceAsset = ImageMaterial;
138
type ErasedAsset = PreparedMaterial;
139
type Param = (
140
SRes<DrawFunctions<Opaque3d>>,
141
SRes<ImageMaterialBindGroupLayout>,
142
SRes<AssetServer>,
143
SResMut<MaterialBindGroupAllocators>,
144
SResMut<RenderMaterialBindings>,
145
SRes<RenderAssets<GpuImage>>,
146
SRes<ImageMaterialBindGroupSampler>,
147
);
148
149
fn prepare_asset(
150
source_asset: Self::SourceAsset,
151
asset_id: AssetId<Self::SourceAsset>,
152
(
153
opaque_draw_functions,
154
material_layout,
155
asset_server,
156
bind_group_allocators,
157
render_material_bindings,
158
gpu_images,
159
image_material_sampler,
160
): &mut SystemParamItem<Self::Param>,
161
) -> std::result::Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>> {
162
let material_layout = material_layout.0.clone();
163
let draw_function_id = opaque_draw_functions.read().id::<DrawMaterial>();
164
let bind_group_allocator = bind_group_allocators
165
.get_mut(&TypeId::of::<ImageMaterial>())
166
.unwrap();
167
let Some(image) = gpu_images.get(&source_asset.image) else {
168
return Err(PrepareAssetError::RetryNextUpdate(source_asset));
169
};
170
let unprepared = UnpreparedBindGroup {
171
bindings: BindingResources(vec![
172
(
173
0,
174
OwnedBindingResource::TextureView(
175
TextureViewDimension::D2,
176
image.texture_view.clone(),
177
),
178
),
179
(
180
1,
181
OwnedBindingResource::Sampler(
182
SamplerBindingType::NonFiltering,
183
image_material_sampler.0.clone(),
184
),
185
),
186
]),
187
};
188
let binding = match render_material_bindings.entry(asset_id.into()) {
189
Entry::Occupied(mut occupied_entry) => {
190
bind_group_allocator.free(*occupied_entry.get());
191
let new_binding =
192
bind_group_allocator.allocate_unprepared(unprepared, &material_layout);
193
*occupied_entry.get_mut() = new_binding;
194
new_binding
195
}
196
Entry::Vacant(vacant_entry) => *vacant_entry
197
.insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)),
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
216
/// set up a simple 3D scene
217
fn setup(
218
mut commands: Commands,
219
mut meshes: ResMut<Assets<Mesh>>,
220
mut materials: ResMut<Assets<ImageMaterial>>,
221
asset_server: Res<AssetServer>,
222
) {
223
// cube
224
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
// light
232
commands.spawn((
233
PointLight {
234
shadow_maps_enabled: true,
235
..default()
236
},
237
Transform::from_xyz(4.0, 8.0, 4.0),
238
));
239
// camera
240
commands.spawn((
241
Camera3d::default(),
242
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
243
));
244
}
245
246
fn 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
274
fn 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
) {
290
entities_needing_specialization.clear();
291
292
needs_specialization
293
.par_iter()
294
.for_each(|entity| par_local.borrow_local_mut().push(entity));
295
296
par_local.drain_into(&mut entities_needing_specialization);
297
}
298
299
fn extract_image_materials_needing_specialization(
300
entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<ImageMaterial>>>,
301
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
302
mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
303
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
304
render_material_instances: Res<RenderMaterialInstances>,
305
views: Query<&ExtractedView>,
306
ticks: SystemChangeTick,
307
) {
308
// Clean up any despawned entities, we do this first in case the removed material was re-added
309
// the same frame, thus will appear both in the removed components list and have been added to
310
// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
311
for entity in removed_mesh_material_components.read() {
312
entity_specialization_ticks.remove(&MainEntity::from(entity));
313
for view in views {
314
if let Some(cache) =
315
specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
316
{
317
cache.remove(&MainEntity::from(entity));
318
}
319
}
320
}
321
322
for entity in entities_needing_specialization.iter() {
323
// Update the entity's specialization tick with this run's tick
324
entity_specialization_ticks.insert(
325
(*entity).into(),
326
EntitySpecializationTickPair {
327
system_tick: ticks.this_run(),
328
material_instances_tick: render_material_instances.current_change_tick,
329
},
330
);
331
}
332
}
333
334
fn sweep_image_materials_needing_specialization(
335
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
336
mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
337
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
338
render_material_instances: Res<RenderMaterialInstances>,
339
views: Query<&ExtractedView>,
340
) {
341
// Clean up any despawned entities, we do this first in case the removed material was re-added
342
// the same frame, thus will appear both in the removed components list and have been added to
343
// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
344
for entity in removed_mesh_material_components.read() {
345
if entity_specialization_ticks
346
.get(&MainEntity::from(entity))
347
.is_some_and(|ticks| {
348
ticks.material_instances_tick == render_material_instances.current_change_tick
349
})
350
{
351
continue;
352
}
353
354
entity_specialization_ticks.remove(&MainEntity::from(entity));
355
356
for view in views {
357
if let Some(cache) =
358
specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
359
{
360
cache.remove(&MainEntity::from(entity));
361
}
362
}
363
}
364
}
365
366