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