Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/decal/clustered.rs
6600 views
1
//! Clustered decals, bounding regions that project textures onto surfaces.
2
//!
3
//! A *clustered decal* is a bounding box that projects a texture onto any
4
//! surface within its bounds along the positive Z axis. In Bevy, clustered
5
//! decals use the *clustered forward* rendering technique.
6
//!
7
//! Clustered decals are the highest-quality types of decals that Bevy supports,
8
//! but they require bindless textures. This means that they presently can't be
9
//! used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
10
//! with forward or deferred rendering and don't require a prepass.
11
//!
12
//! On their own, clustered decals only project the base color of a texture. You
13
//! can, however, use the built-in *tag* field to customize the appearance of a
14
//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for
15
//! more information and the `clustered_decals` example for an example of use.
16
17
use core::{num::NonZero, ops::Deref};
18
19
use bevy_app::{App, Plugin};
20
use bevy_asset::AssetId;
21
use bevy_camera::visibility::ViewVisibility;
22
use bevy_derive::{Deref, DerefMut};
23
use bevy_ecs::{
24
entity::{Entity, EntityHashMap},
25
query::With,
26
resource::Resource,
27
schedule::IntoScheduleConfigs as _,
28
system::{Commands, Local, Query, Res, ResMut},
29
};
30
use bevy_image::Image;
31
use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture};
32
use bevy_math::Mat4;
33
use bevy_platform::collections::HashMap;
34
use bevy_render::{
35
render_asset::RenderAssets,
36
render_resource::{
37
binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler,
38
SamplerBindingType, ShaderType, TextureSampleType, TextureView,
39
},
40
renderer::{RenderAdapter, RenderDevice, RenderQueue},
41
sync_component::SyncComponentPlugin,
42
sync_world::RenderEntity,
43
texture::{FallbackImage, GpuImage},
44
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
45
};
46
use bevy_shader::load_shader_library;
47
use bevy_transform::components::GlobalTransform;
48
use bytemuck::{Pod, Zeroable};
49
50
use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta};
51
52
/// The maximum number of decals that can be present in a view.
53
///
54
/// This number is currently relatively low in order to work around the lack of
55
/// first-class binding arrays in `wgpu`. When that feature is implemented, this
56
/// limit can be increased.
57
pub(crate) const MAX_VIEW_DECALS: usize = 8;
58
59
/// A plugin that adds support for clustered decals.
60
///
61
/// In environments where bindless textures aren't available, clustered decals
62
/// can still be added to a scene, but they won't project any decals.
63
pub struct ClusteredDecalPlugin;
64
65
/// Stores information about all the clustered decals in the scene.
66
#[derive(Resource, Default)]
67
pub struct RenderClusteredDecals {
68
/// Maps an index in the shader binding array to the associated decal image.
69
///
70
/// [`Self::texture_to_binding_index`] holds the inverse mapping.
71
binding_index_to_textures: Vec<AssetId<Image>>,
72
/// Maps a decal image to the shader binding array.
73
///
74
/// [`Self::binding_index_to_textures`] holds the inverse mapping.
75
texture_to_binding_index: HashMap<AssetId<Image>, u32>,
76
/// The information concerning each decal that we provide to the shader.
77
decals: Vec<RenderClusteredDecal>,
78
/// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
79
/// index of that decal in the [`Self::decals`] list.
80
entity_to_decal_index: EntityHashMap<usize>,
81
}
82
83
impl RenderClusteredDecals {
84
/// Clears out this [`RenderClusteredDecals`] in preparation for a new
85
/// frame.
86
fn clear(&mut self) {
87
self.binding_index_to_textures.clear();
88
self.texture_to_binding_index.clear();
89
self.decals.clear();
90
self.entity_to_decal_index.clear();
91
}
92
93
pub fn insert_decal(
94
&mut self,
95
entity: Entity,
96
image: &AssetId<Image>,
97
local_from_world: Mat4,
98
tag: u32,
99
) {
100
let image_index = self.get_or_insert_image(image);
101
let decal_index = self.decals.len();
102
self.decals.push(RenderClusteredDecal {
103
local_from_world,
104
image_index,
105
tag,
106
pad_a: 0,
107
pad_b: 0,
108
});
109
self.entity_to_decal_index.insert(entity, decal_index);
110
}
111
112
pub fn get(&self, entity: Entity) -> Option<usize> {
113
self.entity_to_decal_index.get(&entity).copied()
114
}
115
}
116
117
/// The per-view bind group entries pertaining to decals.
118
pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> {
119
/// The list of decals, corresponding to `mesh_view_bindings::decals` in the
120
/// shader.
121
pub(crate) decals: &'a Buffer,
122
/// The list of textures, corresponding to
123
/// `mesh_view_bindings::decal_textures` in the shader.
124
pub(crate) texture_views: Vec<&'a <TextureView as Deref>::Target>,
125
/// The sampler that the shader uses to sample decals, corresponding to
126
/// `mesh_view_bindings::decal_sampler` in the shader.
127
pub(crate) sampler: &'a Sampler,
128
}
129
130
/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready
131
/// to upload to the GPU.
132
#[derive(Resource, Deref, DerefMut)]
133
pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
134
135
impl Default for DecalsBuffer {
136
fn default() -> Self {
137
DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
138
}
139
}
140
141
impl Plugin for ClusteredDecalPlugin {
142
fn build(&self, app: &mut App) {
143
load_shader_library!(app, "clustered.wgsl");
144
145
app.add_plugins(SyncComponentPlugin::<ClusteredDecal>::default());
146
147
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
148
return;
149
};
150
151
render_app
152
.init_resource::<DecalsBuffer>()
153
.init_resource::<RenderClusteredDecals>()
154
.add_systems(ExtractSchedule, (extract_decals, extract_clustered_decal))
155
.add_systems(
156
Render,
157
prepare_decals
158
.in_set(RenderSystems::ManageViews)
159
.after(prepare_lights),
160
)
161
.add_systems(
162
Render,
163
upload_decals.in_set(RenderSystems::PrepareResources),
164
);
165
}
166
}
167
168
// This is needed because of the orphan rule not allowing implementing
169
// foreign trait ExtractComponent on foreign type ClusteredDecal
170
fn extract_clustered_decal(
171
mut commands: Commands,
172
mut previous_len: Local<usize>,
173
query: Extract<Query<(RenderEntity, &ClusteredDecal)>>,
174
) {
175
let mut values = Vec::with_capacity(*previous_len);
176
for (entity, query_item) in &query {
177
values.push((entity, query_item.clone()));
178
}
179
*previous_len = values.len();
180
commands.try_insert_batch(values);
181
}
182
183
/// The GPU data structure that stores information about each decal.
184
#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)]
185
#[repr(C)]
186
pub struct RenderClusteredDecal {
187
/// The inverse of the model matrix.
188
///
189
/// The shader uses this in order to back-transform world positions into
190
/// model space.
191
local_from_world: Mat4,
192
/// The index of the decal texture in the binding array.
193
image_index: u32,
194
/// A custom tag available for application-defined purposes.
195
tag: u32,
196
/// Padding.
197
pad_a: u32,
198
/// Padding.
199
pad_b: u32,
200
}
201
202
/// Extracts decals from the main world into the render world.
203
pub fn extract_decals(
204
decals: Extract<
205
Query<(
206
RenderEntity,
207
&ClusteredDecal,
208
&GlobalTransform,
209
&ViewVisibility,
210
)>,
211
>,
212
spot_light_textures: Extract<
213
Query<(
214
RenderEntity,
215
&SpotLightTexture,
216
&GlobalTransform,
217
&ViewVisibility,
218
)>,
219
>,
220
point_light_textures: Extract<
221
Query<(
222
RenderEntity,
223
&PointLightTexture,
224
&GlobalTransform,
225
&ViewVisibility,
226
)>,
227
>,
228
directional_light_textures: Extract<
229
Query<(
230
RenderEntity,
231
&DirectionalLightTexture,
232
&GlobalTransform,
233
&ViewVisibility,
234
)>,
235
>,
236
mut render_decals: ResMut<RenderClusteredDecals>,
237
) {
238
// Clear out the `RenderDecals` in preparation for a new frame.
239
render_decals.clear();
240
241
// Loop over each decal.
242
for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals {
243
// If the decal is invisible, skip it.
244
if !view_visibility.get() {
245
continue;
246
}
247
248
render_decals.insert_decal(
249
decal_entity,
250
&clustered_decal.image.id(),
251
global_transform.affine().inverse().into(),
252
clustered_decal.tag,
253
);
254
}
255
256
for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures {
257
// If the decal is invisible, skip it.
258
if !view_visibility.get() {
259
continue;
260
}
261
262
render_decals.insert_decal(
263
decal_entity,
264
&texture.image.id(),
265
global_transform.affine().inverse().into(),
266
0,
267
);
268
}
269
270
for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures {
271
// If the decal is invisible, skip it.
272
if !view_visibility.get() {
273
continue;
274
}
275
276
render_decals.insert_decal(
277
decal_entity,
278
&texture.image.id(),
279
global_transform.affine().inverse().into(),
280
texture.cubemap_layout as u32,
281
);
282
}
283
284
for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures {
285
// If the decal is invisible, skip it.
286
if !view_visibility.get() {
287
continue;
288
}
289
290
render_decals.insert_decal(
291
decal_entity,
292
&texture.image.id(),
293
global_transform.affine().inverse().into(),
294
if texture.tiled { 1 } else { 0 },
295
);
296
}
297
}
298
299
/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table.
300
fn prepare_decals(
301
decals: Query<Entity, With<ClusteredDecal>>,
302
mut global_clusterable_object_meta: ResMut<GlobalClusterableObjectMeta>,
303
render_decals: Res<RenderClusteredDecals>,
304
) {
305
for decal_entity in &decals {
306
if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) {
307
global_clusterable_object_meta
308
.entity_to_index
309
.insert(decal_entity, *index);
310
}
311
}
312
}
313
314
/// Returns the layout for the clustered-decal-related bind group entries for a
315
/// single view.
316
pub(crate) fn get_bind_group_layout_entries(
317
render_device: &RenderDevice,
318
render_adapter: &RenderAdapter,
319
) -> Option<[BindGroupLayoutEntryBuilder; 3]> {
320
// If binding arrays aren't supported on the current platform, we have no
321
// bind group layout entries.
322
if !clustered_decals_are_usable(render_device, render_adapter) {
323
return None;
324
}
325
326
Some([
327
// `decals`
328
binding_types::storage_buffer_read_only::<RenderClusteredDecal>(false),
329
// `decal_textures`
330
binding_types::texture_2d(TextureSampleType::Float { filterable: true })
331
.count(NonZero::<u32>::new(MAX_VIEW_DECALS as u32).unwrap()),
332
// `decal_sampler`
333
binding_types::sampler(SamplerBindingType::Filtering),
334
])
335
}
336
337
impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
338
/// Creates and returns the bind group entries for clustered decals for a
339
/// single view.
340
pub(crate) fn get(
341
render_decals: &RenderClusteredDecals,
342
decals_buffer: &'a DecalsBuffer,
343
images: &'a RenderAssets<GpuImage>,
344
fallback_image: &'a FallbackImage,
345
render_device: &RenderDevice,
346
render_adapter: &RenderAdapter,
347
) -> Option<RenderViewClusteredDecalBindGroupEntries<'a>> {
348
// Skip the entries if decals are unsupported on the current platform.
349
if !clustered_decals_are_usable(render_device, render_adapter) {
350
return None;
351
}
352
353
// We use the first sampler among all the images. This assumes that all
354
// images use the same sampler, which is a documented restriction. If
355
// there's no sampler, we just use the one from the fallback image.
356
let sampler = match render_decals
357
.binding_index_to_textures
358
.iter()
359
.filter_map(|image_id| images.get(*image_id))
360
.next()
361
{
362
Some(gpu_image) => &gpu_image.sampler,
363
None => &fallback_image.d2.sampler,
364
};
365
366
// Gather up the decal textures.
367
let mut texture_views = vec![];
368
for image_id in &render_decals.binding_index_to_textures {
369
match images.get(*image_id) {
370
None => texture_views.push(&*fallback_image.d2.texture_view),
371
Some(gpu_image) => texture_views.push(&*gpu_image.texture_view),
372
}
373
}
374
375
// Pad out the binding array to its maximum length, which is
376
// required on some platforms.
377
while texture_views.len() < MAX_VIEW_DECALS {
378
texture_views.push(&*fallback_image.d2.texture_view);
379
}
380
381
Some(RenderViewClusteredDecalBindGroupEntries {
382
decals: decals_buffer.buffer()?,
383
texture_views,
384
sampler,
385
})
386
}
387
}
388
389
impl RenderClusteredDecals {
390
/// Returns the index of the given image in the decal texture binding array,
391
/// adding it to the list if necessary.
392
fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> u32 {
393
*self
394
.texture_to_binding_index
395
.entry(*image_id)
396
.or_insert_with(|| {
397
let index = self.binding_index_to_textures.len() as u32;
398
self.binding_index_to_textures.push(*image_id);
399
index
400
})
401
}
402
}
403
404
/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the
405
/// GPU.
406
fn upload_decals(
407
render_decals: Res<RenderClusteredDecals>,
408
mut decals_buffer: ResMut<DecalsBuffer>,
409
render_device: Res<RenderDevice>,
410
render_queue: Res<RenderQueue>,
411
) {
412
decals_buffer.clear();
413
414
for &decal in &render_decals.decals {
415
decals_buffer.push(decal);
416
}
417
418
// Make sure the buffer is non-empty.
419
// Otherwise there won't be a buffer to bind.
420
if decals_buffer.is_empty() {
421
decals_buffer.push(RenderClusteredDecal::default());
422
}
423
424
decals_buffer.write_buffer(&render_device, &render_queue);
425
}
426
427
/// Returns true if clustered decals are usable on the current platform or false
428
/// otherwise.
429
///
430
/// Clustered decals are currently disabled on macOS and iOS due to insufficient
431
/// texture bindings and limited bindless support in `wgpu`.
432
pub fn clustered_decals_are_usable(
433
render_device: &RenderDevice,
434
render_adapter: &RenderAdapter,
435
) -> bool {
436
// Disable binding arrays on Metal. There aren't enough texture bindings available.
437
// See issue #17553.
438
// Re-enable this when `wgpu` has first-class bindless.
439
binding_arrays_are_usable(render_device, render_adapter)
440
&& cfg!(not(any(target_os = "macos", target_os = "ios")))
441
&& cfg!(feature = "pbr_clustered_decals")
442
}
443
444