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, 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
17use core::{num::NonZero, ops::Deref};
18
19use bevy_app::{App, Plugin};
20use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
21use bevy_derive::{Deref, DerefMut};
22use bevy_ecs::{
23    component::Component,
24    entity::{Entity, EntityHashMap},
25    prelude::ReflectComponent,
26    query::With,
27    resource::Resource,
28    schedule::IntoScheduleConfigs as _,
29    system::{Query, Res, ResMut},
30};
31use bevy_image::Image;
32use bevy_math::Mat4;
33use bevy_platform::collections::HashMap;
34use bevy_reflect::Reflect;
35use bevy_render::{
36    extract_component::{ExtractComponent, ExtractComponentPlugin},
37    render_asset::RenderAssets,
38    render_resource::{
39        binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler,
40        SamplerBindingType, Shader, ShaderType, TextureSampleType, TextureView,
41    },
42    renderer::{RenderAdapter, RenderDevice, RenderQueue},
43    sync_world::RenderEntity,
44    texture::{FallbackImage, GpuImage},
45    view::{self, ViewVisibility, Visibility, VisibilityClass},
46    Extract, ExtractSchedule, Render, RenderApp, RenderSet,
47};
48use bevy_transform::{components::GlobalTransform, prelude::Transform};
49use bytemuck::{Pod, Zeroable};
50
51use crate::{
52    binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass,
53};
54
55/// The handle to the `clustered.wgsl` shader.
56pub(crate) const CLUSTERED_DECAL_SHADER_HANDLE: Handle<Shader> =
57    weak_handle!("87929002-3509-42f1-8279-2d2765dd145c");
58
59/// The maximum number of decals that can be present in a view.
60///
61/// This number is currently relatively low in order to work around the lack of
62/// first-class binding arrays in `wgpu`. When that feature is implemented, this
63/// limit can be increased.
64pub(crate) const MAX_VIEW_DECALS: usize = 8;
65
66/// A plugin that adds support for clustered decals.
67///
68/// In environments where bindless textures aren't available, clustered decals
69/// can still be added to a scene, but they won't project any decals.
70pub struct ClusteredDecalPlugin;
71
72/// An object that projects a decal onto surfaces within its bounds.
73///
74/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
75/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus
76/// you may find [`Transform::looking_at`] useful).
77///
78/// Clustered decals are the highest-quality types of decals that Bevy supports,
79/// but they require bindless textures. This means that they presently can't be
80/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
81/// with forward or deferred rendering and don't require a prepass.
82#[derive(Component, Debug, Clone, Reflect, ExtractComponent)]
83#[reflect(Component, Debug, Clone)]
84#[require(Transform, Visibility, VisibilityClass)]
85#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
86pub struct ClusteredDecal {
87    /// The image that the clustered decal projects.
88    ///
89    /// This must be a 2D image. If it has an alpha channel, it'll be alpha
90    /// blended with the underlying surface and/or other decals. All decal
91    /// images in the scene must use the same sampler.
92    pub image: Handle<Image>,
93
94    /// An application-specific tag you can use for any purpose you want.
95    ///
96    /// See the `clustered_decals` example for an example of use.
97    pub tag: u32,
98}
99
100/// Stores information about all the clustered decals in the scene.
101#[derive(Resource, Default)]
102pub struct RenderClusteredDecals {
103    /// Maps an index in the shader binding array to the associated decal image.
104    ///
105    /// [`Self::texture_to_binding_index`] holds the inverse mapping.
106    binding_index_to_textures: Vec<AssetId<Image>>,
107    /// Maps a decal image to the shader binding array.
108    ///
109    /// [`Self::binding_index_to_textures`] holds the inverse mapping.
110    texture_to_binding_index: HashMap<AssetId<Image>, u32>,
111    /// The information concerning each decal that we provide to the shader.
112    decals: Vec<RenderClusteredDecal>,
113    /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
114    /// index of that decal in the [`Self::decals`] list.
115    entity_to_decal_index: EntityHashMap<usize>,
116}
117
118impl RenderClusteredDecals {
119    /// Clears out this [`RenderClusteredDecals`] in preparation for a new
120    /// frame.
121    fn clear(&mut self) {
122        self.binding_index_to_textures.clear();
123        self.texture_to_binding_index.clear();
124        self.decals.clear();
125        self.entity_to_decal_index.clear();
126    }
127}
128
129/// The per-view bind group entries pertaining to decals.
130pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> {
131    /// The list of decals, corresponding to `mesh_view_bindings::decals` in the
132    /// shader.
133    pub(crate) decals: &'a Buffer,
134    /// The list of textures, corresponding to
135    /// `mesh_view_bindings::decal_textures` in the shader.
136    pub(crate) texture_views: Vec<&'a <TextureView as Deref>::Target>,
137    /// The sampler that the shader uses to sample decals, corresponding to
138    /// `mesh_view_bindings::decal_sampler` in the shader.
139    pub(crate) sampler: &'a Sampler,
140}
141
142/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready
143/// to upload to the GPU.
144#[derive(Resource, Deref, DerefMut)]
145pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
146
147impl Default for DecalsBuffer {
148    fn default() -> Self {
149        DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
150    }
151}
152
153impl Plugin for ClusteredDecalPlugin {
154    fn build(&self, app: &mut App) {
155        load_internal_asset!(
156            app,
157            CLUSTERED_DECAL_SHADER_HANDLE,
158            "clustered.wgsl",
159            Shader::from_wgsl
160        );
161
162        app.add_plugins(ExtractComponentPlugin::<ClusteredDecal>::default())
163            .register_type::<ClusteredDecal>();
164
165        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
166            return;
167        };
168
169        render_app
170            .init_resource::<DecalsBuffer>()
171            .init_resource::<RenderClusteredDecals>()
172            .add_systems(ExtractSchedule, extract_decals)
173            .add_systems(
174                Render,
175                prepare_decals
176                    .in_set(RenderSet::ManageViews)
177                    .after(prepare_lights),
178            )
179            .add_systems(Render, upload_decals.in_set(RenderSet::PrepareResources));
180    }
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    mut render_decals: ResMut<RenderClusteredDecals>,
213) {
214    // Clear out the `RenderDecals` in preparation for a new frame.
215    render_decals.clear();
216
217    // Loop over each decal.
218    for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals {
219        // If the decal is invisible, skip it.
220        if !view_visibility.get() {
221            continue;
222        }
223
224        // Insert or add the image.
225        let image_index = render_decals.get_or_insert_image(&clustered_decal.image.id());
226
227        // Record the decal.
228        let decal_index = render_decals.decals.len();
229        render_decals
230            .entity_to_decal_index
231            .insert(decal_entity, decal_index);
232
233        render_decals.decals.push(RenderClusteredDecal {
234            local_from_world: global_transform.affine().inverse().into(),
235            image_index,
236            tag: clustered_decal.tag,
237            pad_a: 0,
238            pad_b: 0,
239        });
240    }
241}
242
243/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table.
244fn prepare_decals(
245    decals: Query<Entity, With<ClusteredDecal>>,
246    mut global_clusterable_object_meta: ResMut<GlobalClusterableObjectMeta>,
247    render_decals: Res<RenderClusteredDecals>,
248) {
249    for decal_entity in &decals {
250        if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) {
251            global_clusterable_object_meta
252                .entity_to_index
253                .insert(decal_entity, *index);
254        }
255    }
256}
257
258/// Returns the layout for the clustered-decal-related bind group entries for a
259/// single view.
260pub(crate) fn get_bind_group_layout_entries(
261    render_device: &RenderDevice,
262    render_adapter: &RenderAdapter,
263) -> Option<[BindGroupLayoutEntryBuilder; 3]> {
264    // If binding arrays aren't supported on the current platform, we have no
265    // bind group layout entries.
266    if !clustered_decals_are_usable(render_device, render_adapter) {
267        return None;
268    }
269
270    Some([
271        // `decals`
272        binding_types::storage_buffer_read_only::<RenderClusteredDecal>(false),
273        // `decal_textures`
274        binding_types::texture_2d(TextureSampleType::Float { filterable: true })
275            .count(NonZero::<u32>::new(MAX_VIEW_DECALS as u32).unwrap()),
276        // `decal_sampler`
277        binding_types::sampler(SamplerBindingType::Filtering),
278    ])
279}
280
281impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
282    /// Creates and returns the bind group entries for clustered decals for a
283    /// single view.
284    pub(crate) fn get(
285        render_decals: &RenderClusteredDecals,
286        decals_buffer: &'a DecalsBuffer,
287        images: &'a RenderAssets<GpuImage>,
288        fallback_image: &'a FallbackImage,
289        render_device: &RenderDevice,
290        render_adapter: &RenderAdapter,
291    ) -> Option<RenderViewClusteredDecalBindGroupEntries<'a>> {
292        // Skip the entries if decals are unsupported on the current platform.
293        if !clustered_decals_are_usable(render_device, render_adapter) {
294            return None;
295        }
296
297        // We use the first sampler among all the images. This assumes that all
298        // images use the same sampler, which is a documented restriction. If
299        // there's no sampler, we just use the one from the fallback image.
300        let sampler = match render_decals
301            .binding_index_to_textures
302            .iter()
303            .filter_map(|image_id| images.get(*image_id))
304            .next()
305        {
306            Some(gpu_image) => &gpu_image.sampler,
307            None => &fallback_image.d2.sampler,
308        };
309
310        // Gather up the decal textures.
311        let mut texture_views = vec![];
312        for image_id in &render_decals.binding_index_to_textures {
313            match images.get(*image_id) {
314                None => texture_views.push(&*fallback_image.d2.texture_view),
315                Some(gpu_image) => texture_views.push(&*gpu_image.texture_view),
316            }
317        }
318
319        // Pad out the binding array to its maximum length, which is
320        // required on some platforms.
321        while texture_views.len() < MAX_VIEW_DECALS {
322            texture_views.push(&*fallback_image.d2.texture_view);
323        }
324
325        Some(RenderViewClusteredDecalBindGroupEntries {
326            decals: decals_buffer.buffer()?,
327            texture_views,
328            sampler,
329        })
330    }
331}
332
333impl RenderClusteredDecals {
334    /// Returns the index of the given image in the decal texture binding array,
335    /// adding it to the list if necessary.
336    fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> u32 {
337        *self
338            .texture_to_binding_index
339            .entry(*image_id)
340            .or_insert_with(|| {
341                let index = self.binding_index_to_textures.len() as u32;
342                self.binding_index_to_textures.push(*image_id);
343                index
344            })
345    }
346}
347
348/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the
349/// GPU.
350fn upload_decals(
351    render_decals: Res<RenderClusteredDecals>,
352    mut decals_buffer: ResMut<DecalsBuffer>,
353    render_device: Res<RenderDevice>,
354    render_queue: Res<RenderQueue>,
355) {
356    decals_buffer.clear();
357
358    for &decal in &render_decals.decals {
359        decals_buffer.push(decal);
360    }
361
362    // Make sure the buffer is non-empty.
363    // Otherwise there won't be a buffer to bind.
364    if decals_buffer.is_empty() {
365        decals_buffer.push(RenderClusteredDecal::default());
366    }
367
368    decals_buffer.write_buffer(&render_device, &render_queue);
369}
370
371/// Returns true if clustered decals are usable on the current platform or false
372/// otherwise.
373///
374/// Clustered decals are currently disabled on macOS and iOS due to insufficient
375/// texture bindings and limited bindless support in `wgpu`.
376pub fn clustered_decals_are_usable(
377    render_device: &RenderDevice,
378    render_adapter: &RenderAdapter,
379) -> bool {
380    // Disable binding arrays on Metal. There aren't enough texture bindings available.
381    // See issue #17553.
382    // Re-enable this when `wgpu` has first-class bindless.
383    binding_arrays_are_usable(render_device, render_adapter)
384        && cfg!(not(any(target_os = "macos", target_os = "ios")))
385}