Skip to main content

bevy_pbr/decal/
clustered.rs

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 or WebGPU. Bevy's clustered decals can be used
10//! with forward or deferred rendering and don't require a prepass.
11//!
12//! Each clustered decal may contain up to 4 textures. By default, the 4
13//! textures correspond to the base color, a normal map, a metallic-roughness
14//! map, and an emissive map respectively. However, with a custom shader, you
15//! can use these 4 textures for whatever you wish. Additionally, you can use
16//! the built-in *tag* field to store additional application-specific data; by
17//! reading the tag in the shader, you can modify the appearance of a clustered
18//! decal arbitrarily. See the documentation in `clustered.wgsl` for more
19//! information and the `clustered_decals` example for an example of use.
20
21use core::{num::NonZero, ops::Deref};
22
23use bevy_app::{App, Plugin};
24use bevy_asset::{AssetId, Handle};
25use bevy_camera::visibility::ViewVisibility;
26use bevy_derive::{Deref, DerefMut};
27use bevy_ecs::{
28    entity::{Entity, EntityHashMap},
29    query::With,
30    resource::Resource,
31    schedule::IntoScheduleConfigs as _,
32    system::{Commands, Local, Query, Res, ResMut},
33};
34use bevy_image::Image;
35use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture};
36use bevy_math::{Mat4, Vec3};
37use bevy_platform::collections::HashMap;
38use bevy_render::{
39    render_asset::RenderAssets,
40    render_resource::{
41        binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler,
42        SamplerBindingType, ShaderType, TextureSampleType, TextureView,
43    },
44    renderer::{RenderAdapter, RenderDevice, RenderQueue},
45    settings::WgpuFeatures,
46    sync_component::{SyncComponent, SyncComponentPlugin},
47    sync_world::RenderEntity,
48    texture::{FallbackImage, GpuImage},
49    Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderSystems,
50};
51use bevy_shader::load_shader_library;
52use bevy_transform::components::GlobalTransform;
53use bytemuck::{Pod, Zeroable};
54
55use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta};
56
57/// The number of textures that can be associated with each clustered decal.
58const IMAGES_PER_DECAL: usize = 4;
59
60/// A plugin that adds support for clustered decals.
61///
62/// In environments where bindless textures aren't available, clustered decals
63/// can still be added to a scene, but they won't project any decals.
64pub struct ClusteredDecalPlugin;
65
66/// Stores information about all the clustered decals in the scene.
67#[derive(Resource, Default)]
68pub struct RenderClusteredDecals {
69    /// Maps an index in the shader binding array to the associated decal image.
70    ///
71    /// The `texture_to_binding_index` field holds the inverse mapping.
72    pub binding_index_to_textures: Vec<AssetId<Image>>,
73    /// Maps a decal image to the shader binding array.
74    ///
75    /// [`Self::binding_index_to_textures`] holds the inverse mapping.
76    texture_to_binding_index: HashMap<AssetId<Image>, i32>,
77    /// The information concerning each decal that we provide to the shader.
78    decals: Vec<RenderClusteredDecal>,
79    /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
80    /// index of that decal in the [`Self::decals`] list.
81    entity_to_decal_index: EntityHashMap<usize>,
82}
83
84impl RenderClusteredDecals {
85    /// Clears out this [`RenderClusteredDecals`] in preparation for a new
86    /// frame.
87    fn clear(&mut self) {
88        self.binding_index_to_textures.clear();
89        self.texture_to_binding_index.clear();
90        self.decals.clear();
91        self.entity_to_decal_index.clear();
92    }
93
94    pub fn insert_decal(
95        &mut self,
96        entity: Entity,
97        images: [Option<AssetId<Image>>; IMAGES_PER_DECAL],
98        local_from_world: Mat4,
99        world_position: Vec3,
100        bounding_sphere_radius: f32,
101        tag: u32,
102    ) {
103        let image_indices = images.map(|maybe_image_id| match maybe_image_id {
104            Some(ref image_id) => self.get_or_insert_image(image_id),
105            None => -1,
106        });
107        let decal_index = self.decals.len();
108        self.decals.push(RenderClusteredDecal {
109            local_from_world,
110            image_indices,
111            world_position,
112            bounding_sphere_radius,
113            tag,
114            pad_a: 0,
115            pad_b: 0,
116            pad_c: 0,
117        });
118        self.entity_to_decal_index.insert(entity, decal_index);
119    }
120
121    pub fn get(&self, entity: Entity) -> Option<usize> {
122        self.entity_to_decal_index.get(&entity).copied()
123    }
124
125    /// Returns the number of clustered decals in the scene.
126    pub fn len(&self) -> usize {
127        self.decals.len()
128    }
129
130    /// Returns true if there are no clustered decals in the scene.
131    pub fn is_empty(&self) -> bool {
132        self.decals.is_empty()
133    }
134}
135
136/// The per-view bind group entries pertaining to decals.
137pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> {
138    /// The list of decals, corresponding to `mesh_view_bindings::decals` in the
139    /// shader.
140    pub(crate) decals: &'a Buffer,
141    /// The list of textures, corresponding to
142    /// `mesh_view_bindings::decal_textures` in the shader.
143    pub(crate) texture_views: Vec<&'a <TextureView as Deref>::Target>,
144    /// The sampler that the shader uses to sample decals, corresponding to
145    /// `mesh_view_bindings::decal_sampler` in the shader.
146    pub(crate) sampler: &'a Sampler,
147}
148
149/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready
150/// to upload to the GPU.
151#[derive(Resource, Deref, DerefMut)]
152pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
153
154impl Default for DecalsBuffer {
155    fn default() -> Self {
156        DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
157    }
158}
159
160impl Plugin for ClusteredDecalPlugin {
161    fn build(&self, app: &mut App) {
162        load_shader_library!(app, "clustered.wgsl");
163
164        app.add_plugins(SyncComponentPlugin::<ClusteredDecal, Self>::default());
165
166        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
167            return;
168        };
169
170        render_app
171            .init_gpu_resource::<DecalsBuffer>()
172            .init_resource::<RenderClusteredDecals>()
173            .add_systems(ExtractSchedule, (extract_decals, extract_clustered_decal))
174            .add_systems(
175                Render,
176                prepare_decals
177                    .in_set(RenderSystems::PrepareViews)
178                    .after(prepare_lights),
179            )
180            .add_systems(
181                Render,
182                upload_decals.in_set(RenderSystems::PrepareResources),
183            );
184    }
185}
186
187impl SyncComponent<ClusteredDecalPlugin> for ClusteredDecal {
188    type Target = Self;
189}
190
191// This is needed because of the orphan rule not allowing implementing
192// foreign trait ExtractComponent on foreign type ClusteredDecal
193fn extract_clustered_decal(
194    mut commands: Commands,
195    mut previous_len: Local<usize>,
196    query: Extract<Query<(RenderEntity, &ClusteredDecal)>>,
197) {
198    let mut values = Vec::with_capacity(*previous_len);
199    for (entity, query_item) in &query {
200        values.push((entity, query_item.clone()));
201    }
202    *previous_len = values.len();
203    commands.try_insert_batch(values);
204}
205
206/// The GPU data structure that stores information about each decal.
207#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)]
208#[repr(C)]
209pub struct RenderClusteredDecal {
210    /// The inverse of the model matrix.
211    ///
212    /// The shader uses this in order to back-transform world positions into
213    /// model space.
214    local_from_world: Mat4,
215    /// The index of each decal texture in the binding array.
216    ///
217    /// These are in the order of the base color texture, the normal map
218    /// texture, the metallic-roughness map texture, and finally the emissive
219    /// texture.
220    ///
221    /// If the decal doesn't have a texture assigned to a slot, the index at
222    /// that slot will be -1.
223    image_indices: [i32; 4],
224    world_position: Vec3,
225    bounding_sphere_radius: f32,
226    /// A custom tag available for application-defined purposes.
227    tag: u32,
228    /// Padding.
229    pad_a: u32,
230    /// Padding.
231    pad_b: u32,
232    /// Padding.
233    pad_c: u32,
234}
235
236/// Extracts decals from the main world into the render world.
237pub fn extract_decals(
238    decals: Extract<
239        Query<(
240            RenderEntity,
241            &ClusteredDecal,
242            &GlobalTransform,
243            &ViewVisibility,
244        )>,
245    >,
246    spot_light_textures: Extract<
247        Query<(
248            RenderEntity,
249            &SpotLightTexture,
250            &GlobalTransform,
251            &ViewVisibility,
252        )>,
253    >,
254    point_light_textures: Extract<
255        Query<(
256            RenderEntity,
257            &PointLightTexture,
258            &GlobalTransform,
259            &ViewVisibility,
260        )>,
261    >,
262    directional_light_textures: Extract<
263        Query<(
264            RenderEntity,
265            &DirectionalLightTexture,
266            &GlobalTransform,
267            &ViewVisibility,
268        )>,
269    >,
270    mut render_decals: ResMut<RenderClusteredDecals>,
271) {
272    // Clear out the `RenderClusteredDecals` in preparation for a new frame.
273    render_decals.clear();
274
275    extract_clustered_decals(&decals, &mut render_decals);
276    extract_spot_light_textures(&spot_light_textures, &mut render_decals);
277    extract_point_light_textures(&point_light_textures, &mut render_decals);
278    extract_directional_light_textures(&directional_light_textures, &mut render_decals);
279}
280
281/// Extracts all clustered decals and light textures from the scene and transfers
282/// them to the render world.
283fn extract_clustered_decals(
284    decals: &Extract<
285        Query<(
286            RenderEntity,
287            &ClusteredDecal,
288            &GlobalTransform,
289            &ViewVisibility,
290        )>,
291    >,
292    render_decals: &mut RenderClusteredDecals,
293) {
294    // Loop over each decal.
295    for (decal_entity, clustered_decal, global_transform, view_visibility) in decals {
296        // If the decal is invisible, skip it.
297        if !view_visibility.get() {
298            continue;
299        }
300
301        // Insert the decal, grabbing the ID of every associated texture as we
302        // do.
303        render_decals.insert_decal(
304            decal_entity,
305            [
306                clustered_decal.base_color_texture.as_ref().map(Handle::id),
307                clustered_decal.normal_map_texture.as_ref().map(Handle::id),
308                clustered_decal
309                    .metallic_roughness_texture
310                    .as_ref()
311                    .map(Handle::id),
312                clustered_decal.emissive_texture.as_ref().map(Handle::id),
313            ],
314            global_transform.affine().inverse().into(),
315            global_transform.translation(),
316            (global_transform.scale() * Vec3::ONE).length(),
317            clustered_decal.tag,
318        );
319    }
320}
321
322/// Extracts all textures from spot lights from the main world to the render
323/// world as clustered decals.
324fn extract_spot_light_textures(
325    spot_light_textures: &Extract<
326        Query<(
327            RenderEntity,
328            &SpotLightTexture,
329            &GlobalTransform,
330            &ViewVisibility,
331        )>,
332    >,
333    render_decals: &mut RenderClusteredDecals,
334) {
335    for (decal_entity, texture, global_transform, view_visibility) in spot_light_textures {
336        // If the texture is invisible, skip it.
337        if !view_visibility.get() {
338            continue;
339        }
340
341        render_decals.insert_decal(
342            decal_entity,
343            [Some(texture.image.id()), None, None, None],
344            global_transform.affine().inverse().into(),
345            global_transform.translation(),
346            (global_transform.scale() * Vec3::ONE).length(),
347            0,
348        );
349    }
350}
351
352/// Extracts all textures from point lights from the main world to the render
353/// world as clustered decals.
354fn extract_point_light_textures(
355    point_light_textures: &Extract<
356        Query<(
357            RenderEntity,
358            &PointLightTexture,
359            &GlobalTransform,
360            &ViewVisibility,
361        )>,
362    >,
363    render_decals: &mut RenderClusteredDecals,
364) {
365    for (decal_entity, texture, global_transform, view_visibility) in point_light_textures {
366        // If the texture is invisible, skip it.
367        if !view_visibility.get() {
368            continue;
369        }
370
371        render_decals.insert_decal(
372            decal_entity,
373            [Some(texture.image.id()), None, None, None],
374            global_transform.affine().inverse().into(),
375            global_transform.translation(),
376            (global_transform.scale() * Vec3::ONE).length(),
377            texture.cubemap_layout as u32,
378        );
379    }
380}
381
382/// Extracts all textures from directional lights from the main world to the
383/// render world as clustered decals.
384fn extract_directional_light_textures(
385    directional_light_textures: &Extract<
386        Query<(
387            RenderEntity,
388            &DirectionalLightTexture,
389            &GlobalTransform,
390            &ViewVisibility,
391        )>,
392    >,
393    render_decals: &mut RenderClusteredDecals,
394) {
395    for (decal_entity, texture, global_transform, view_visibility) in directional_light_textures {
396        // If the texture is invisible, skip it.
397        if !view_visibility.get() {
398            continue;
399        }
400
401        render_decals.insert_decal(
402            decal_entity,
403            [Some(texture.image.id()), None, None, None],
404            global_transform.affine().inverse().into(),
405            global_transform.translation(),
406            (global_transform.scale() * Vec3::ONE).length(),
407            if texture.tiled { 1 } else { 0 },
408        );
409    }
410}
411
412/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table.
413fn prepare_decals(
414    decals: Query<Entity, With<ClusteredDecal>>,
415    mut global_clusterable_object_meta: ResMut<GlobalClusterableObjectMeta>,
416    render_decals: Res<RenderClusteredDecals>,
417) {
418    for decal_entity in &decals {
419        if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) {
420            global_clusterable_object_meta
421                .entity_to_index
422                .insert(decal_entity, *index);
423        }
424    }
425}
426
427/// Returns the layout for the clustered-decal-related bind group entries for a
428/// single view.
429pub(crate) fn get_bind_group_layout_entries(
430    render_device: &RenderDevice,
431    render_adapter: &RenderAdapter,
432) -> Option<[BindGroupLayoutEntryBuilder; 3]> {
433    // If binding arrays aren't supported on the current platform, we have no
434    // bind group layout entries.
435    if !clustered_decals_are_usable(render_device, render_adapter) {
436        return None;
437    }
438
439    Some([
440        // `decals`
441        binding_types::storage_buffer_read_only::<RenderClusteredDecal>(false),
442        // `decal_textures`
443        binding_types::texture_2d(TextureSampleType::Float { filterable: true })
444            .count(NonZero::<u32>::new(max_view_decals(render_device)).unwrap()),
445        // `decal_sampler`
446        binding_types::sampler(SamplerBindingType::Filtering),
447    ])
448}
449
450impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
451    /// Creates and returns the bind group entries for clustered decals for a
452    /// single view.
453    pub(crate) fn get(
454        render_decals: &RenderClusteredDecals,
455        decals_buffer: &'a DecalsBuffer,
456        images: &'a RenderAssets<GpuImage>,
457        fallback_image: &'a FallbackImage,
458        render_device: &RenderDevice,
459        render_adapter: &RenderAdapter,
460    ) -> Option<RenderViewClusteredDecalBindGroupEntries<'a>> {
461        // Skip the entries if decals are unsupported on the current platform.
462        if !clustered_decals_are_usable(render_device, render_adapter) {
463            return None;
464        }
465
466        // We use the first sampler among all the images. This assumes that all
467        // images use the same sampler, which is a documented restriction. If
468        // there's no sampler, we just use the one from the fallback image.
469        let sampler = match render_decals
470            .binding_index_to_textures
471            .iter()
472            .filter_map(|image_id| images.get(*image_id))
473            .next()
474        {
475            Some(gpu_image) => &gpu_image.sampler,
476            None => &fallback_image.d2.sampler,
477        };
478
479        // Gather up the decal textures.
480        let mut texture_views = vec![];
481        for image_id in &render_decals.binding_index_to_textures {
482            match images.get(*image_id) {
483                None => texture_views.push(&*fallback_image.d2.texture_view),
484                Some(gpu_image) => texture_views.push(&*gpu_image.texture_view),
485            }
486        }
487
488        // If required on this platform, pad out the binding array to its
489        // maximum length.
490        if !render_device
491            .features()
492            .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY)
493        {
494            let max_view_decals = max_view_decals(render_device);
495            while texture_views.len() < max_view_decals as usize {
496                texture_views.push(&*fallback_image.d2.texture_view);
497            }
498        } else if texture_views.is_empty() {
499            texture_views.push(&*fallback_image.d2.texture_view);
500        }
501
502        Some(RenderViewClusteredDecalBindGroupEntries {
503            decals: decals_buffer.buffer()?,
504            texture_views,
505            sampler,
506        })
507    }
508}
509
510impl RenderClusteredDecals {
511    /// Returns the index of the given image in the decal texture binding array,
512    /// adding it to the list if necessary.
513    fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> i32 {
514        *self
515            .texture_to_binding_index
516            .entry(*image_id)
517            .or_insert_with(|| {
518                let index = self.binding_index_to_textures.len() as i32;
519                self.binding_index_to_textures.push(*image_id);
520                index
521            })
522    }
523}
524
525/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the
526/// GPU.
527fn upload_decals(
528    render_decals: Res<RenderClusteredDecals>,
529    mut decals_buffer: ResMut<DecalsBuffer>,
530    render_device: Res<RenderDevice>,
531    render_queue: Res<RenderQueue>,
532) {
533    decals_buffer.clear();
534
535    for &decal in &render_decals.decals {
536        decals_buffer.push(decal);
537    }
538
539    // Make sure the buffer is non-empty.
540    // Otherwise there won't be a buffer to bind.
541    if decals_buffer.is_empty() {
542        decals_buffer.push(RenderClusteredDecal::default());
543    }
544
545    decals_buffer.write_buffer(&render_device, &render_queue);
546}
547
548/// Returns true if clustered decals are usable on the current platform or false
549/// otherwise.
550///
551/// Clustered decals are currently disabled on macOS and iOS due to insufficient
552/// texture bindings and limited bindless support in `wgpu`.
553pub fn clustered_decals_are_usable(
554    render_device: &RenderDevice,
555    render_adapter: &RenderAdapter,
556) -> bool {
557    // Disable binding arrays on Metal. There aren't enough texture bindings available.
558    // See issue #17553.
559    // Re-enable this when `wgpu` has first-class bindless.
560    binding_arrays_are_usable(render_device, render_adapter)
561        && cfg!(feature = "pbr_clustered_decals")
562}
563
564/// Returns the maximum number of decals that can be in the scene, taking
565/// platform limitations into account.
566fn max_view_decals(render_device: &RenderDevice) -> u32 {
567    // If the current `wgpu` platform doesn't support partially-bound binding
568    // arrays, limit the number of decals to a low number. If we didn't do this,
569    // then on such platforms we'd pay the maximum overhead even if there are no
570    // decals are in the scene.
571    if render_device
572        .features()
573        .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY)
574    {
575        // This number was determined arbitrarily as a reasonable value that
576        // would encompass most use cases (e.g. bullet holes in walls) while
577        // offering a failsafe to prevent shaders becoming too slow if there are
578        // extremely large numbers of decals.
579        1024
580    } else {
581        8
582    }
583}