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//! 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
17use core::{num::NonZero, ops::Deref};
18
19use bevy_app::{App, Plugin};
20use bevy_asset::AssetId;
21use bevy_camera::visibility::ViewVisibility;
22use bevy_derive::{Deref, DerefMut};
23use bevy_ecs::{
24    entity::{Entity, EntityHashMap},
25    query::With,
26    resource::Resource,
27    schedule::IntoScheduleConfigs as _,
28    system::{Commands, Local, Query, Res, ResMut},
29};
30use bevy_image::Image;
31use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture};
32use bevy_math::Mat4;
33use bevy_platform::collections::HashMap;
34use 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};
46use bevy_shader::load_shader_library;
47use bevy_transform::components::GlobalTransform;
48use bytemuck::{Pod, Zeroable};
49
50use 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.
57pub(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.
63pub struct ClusteredDecalPlugin;
64
65/// Stores information about all the clustered decals in the scene.
66#[derive(Resource, Default)]
67pub 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
83impl 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.
118pub(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)]
133pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
134
135impl Default for DecalsBuffer {
136    fn default() -> Self {
137        DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
138    }
139}
140
141impl 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
170fn 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)]
186pub 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.
203pub 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.
300fn 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.
316pub(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
337impl<'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
389impl 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.
406fn 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`.
432pub 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!(feature = "pbr_clustered_decals")
441}