Skip to main content

bevy_light/cluster/
mod.rs

1//! Spatial clustering of objects to accelerate rendering performance.
2
3use bevy_asset::Handle;
4use bevy_camera::{
5    prelude::ViewVisibility,
6    primitives::Aabb,
7    visibility::{self, Visibility, VisibilityClass},
8};
9use bevy_ecs::{
10    component::Component,
11    entity::Entity,
12    query::{Or, With, Without},
13    reflect::ReflectComponent,
14    resource::Resource,
15    system::{Commands, Query},
16};
17use bevy_image::Image;
18use bevy_math::{AspectRatio, UVec2, UVec3, Vec3A, Vec3Swizzles as _};
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_transform::components::Transform;
21use tracing::warn;
22
23use crate::LightProbe;
24
25pub mod assign;
26
27#[cfg(test)]
28mod test;
29
30// Clustered-forward rendering notes
31// The main initial reference material used was this rather accessible article:
32// http://www.aortiz.me/2018/12/21/CG.html
33// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of:
34// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/
35// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.)
36// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016:
37// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf
38
39/// Cluster configuration depends on rendering capabilities, these are exposed here.
40/// They are automatically set by `bevy_pbr`, but a custom renderer may configure these too.
41#[derive(Clone, Resource, Debug)]
42#[expect(missing_docs, reason = "self explanatory")]
43pub struct GlobalClusterSettings {
44    pub supports_storage_buffers: bool,
45    pub clustered_decals_are_usable: bool,
46    /// Settings relating to GPU clustering, if GPU clustering is enabled.
47    ///
48    /// To disable GPU light clustering, set this to `None`.
49    pub gpu_clustering: Option<GlobalClusterGpuSettings>,
50    pub max_uniform_buffer_clusterable_objects: usize,
51    pub view_cluster_bindings_max_indices: usize,
52}
53
54/// Settings relating to GPU clustering.
55#[derive(Clone, Copy, Debug)]
56pub struct GlobalClusterGpuSettings {
57    /// The initial capacity of the list of Z slices.
58    ///
59    /// If there are too many clusterable objects on screen, this can overflow.
60    /// Bevy will detect this situation and resize it, but you might see a few
61    /// incorrect frames before Bevy resizes the list. To avoid this issue, you
62    /// can set this to a higher value.
63    ///
64    /// The default value is
65    /// `bevy::pbr::cluster::GPU_CLUSTERING_INITIAL_Z_SLICE_LIST_SIZE`.
66    pub initial_z_slice_list_capacity: usize,
67
68    /// The initial capacity of the list of clusterable objects.
69    ///
70    /// If there are too many clusterable objects on screen, this can overflow.
71    /// Bevy will detect this situation and resize it, but you might see a few
72    /// incorrect frames before Bevy resizes the list. To avoid this issue, you
73    /// can set this to a higher value.
74    ///
75    /// The default value is
76    /// `bevy::pbr::cluster::GPU_CLUSTERING_INITIAL_INDEX_LIST_CAPACITY`.
77    pub initial_index_list_capacity: usize,
78}
79
80/// Configure the far z-plane mode used for the furthest depth slice for clustered forward
81/// rendering
82#[derive(Debug, Copy, Clone, Reflect)]
83#[reflect(Clone)]
84pub enum ClusterFarZMode {
85    /// Calculate the required maximum z-depth based on the clusterable objects
86    /// that were visible on the previous frame. Makes better use of available
87    /// clusters, speeding up GPU lighting operations at the expense of using
88    /// more indices in the clusterable object index lists.
89    MaxClusterableObjectRange,
90    /// Constant max z-depth
91    Constant(f32),
92}
93
94/// Configure the depth-slicing strategy for clustered forward rendering
95#[derive(Debug, Copy, Clone, Reflect)]
96#[reflect(Default, Clone)]
97pub struct ClusterZConfig {
98    /// Far `Z` plane of the first depth slice
99    pub first_slice_depth: f32,
100    /// Strategy for how to evaluate the far `Z` plane of the furthest depth slice
101    pub far_z_mode: ClusterFarZMode,
102}
103
104/// Configuration of the clustering strategy for clustered forward rendering
105#[derive(Debug, Copy, Clone, Component, Reflect)]
106#[reflect(Component, Debug, Default, Clone)]
107pub enum ClusterConfig {
108    /// Disable cluster calculations for this view
109    None,
110    /// One single cluster. Optimal for low-light complexity scenes or scenes where
111    /// most lights affect the entire scene.
112    Single,
113    /// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio)
114    XYZ {
115        /// The dimensions of the cluster grid.
116        dimensions: UVec3,
117        /// How to distribute the `Z` slices spatially.
118        z_config: ClusterZConfig,
119        /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
120        /// the available cluster-object index limit
121        dynamic_resizing: bool,
122    },
123    /// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters
124    /// with at most total clusters. For top-down games where lights will generally always be within a
125    /// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This
126    /// would reduce the number of lights per cluster by distributing more clusters in screen space
127    /// `X/Y` which matches how lights are distributed in the scene.
128    FixedZ {
129        /// The total number of clusters to distribute.
130        total: u32,
131        /// The number of `Z` slices to distribute the clusters over.
132        z_slices: u32,
133        /// How to distribute the `Z` slices spatially.
134        z_config: ClusterZConfig,
135        /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
136        /// the available clusterable object index limit
137        dynamic_resizing: bool,
138    },
139}
140
141/// The cluster geometry generated by [`ClusterConfig`].
142#[derive(Component, Debug)]
143pub struct Clusters {
144    /// The dimensions of the rectangle the cluster occupies in screen-space, rounded up to the nearest pixel.
145    pub tile_size: UVec2,
146    /// Number of clusters in `X` / `Y` / `Z` in the view frustum
147    pub dimensions: UVec3,
148    /// Distance to the far plane of the first depth slice. The first depth slice is special
149    /// and explicitly-configured to avoid having unnecessarily many slices close to the camera.
150    pub near: f32,
151    /// Distance to the far plane of the last depth slice. This may change depending on [`ClusterZConfig`] used.
152    pub far: f32,
153    /// The farthest Z value of any bounding sphere of any clusterable object on
154    /// the previous frame.
155    ///
156    /// This is used for the [`ClusterFarZMode::MaxClusterableObjectRange`]
157    /// feature.
158    pub last_frame_farthest_z: Option<f32>,
159    /// The sum of the number of objects that all clusters contained last frame.
160    ///
161    /// This is used for the `dynamic_resizing` feature, which automatically
162    /// grows the number of clusters if clusters have likely become too large.
163    pub last_frame_total_cluster_index_count: Option<usize>,
164    /// All objects within the cluster.
165    pub clusterable_objects: ClusterableObjects,
166}
167
168/// The list of objects within a cluster, if known to the CPU.
169#[derive(Debug)]
170pub enum ClusterableObjects {
171    /// The list of objects in the cluster is known to the CPU.
172    Cpu(Vec<ObjectsInClusterCpu>),
173    /// The list of objects in the cluster is unknown to the CPU, because GPU
174    /// clustering is being used.
175    Gpu,
176}
177
178/// The [`VisibilityClass`] used for clusterables (decals, point lights, spot
179/// lights, and light probes).
180///
181/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass
182pub struct ClusterVisibilityClass;
183
184/// All objects that potentially intersect a single cluster.
185#[derive(Clone, Default, Debug)]
186pub struct ObjectsInClusterCpu {
187    /// A list of all clusterable objects that are potentially visible from this
188    /// view.
189    clusterables: Vec<Entity>,
190
191    /// The number of each clusterable object type.
192    pub counts: ClusterableObjectCounts,
193}
194
195/// Stores the number of each type of clusterable object in a single cluster.
196///
197/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if
198/// fewer than 3 SSBOs are available, which usually means on WebGL 2.
199#[derive(Clone, Copy, Default, Debug)]
200pub struct ClusterableObjectCounts {
201    /// The number of point lights in the cluster.
202    pub point_lights: u32,
203    /// The number of spot lights in the cluster.
204    pub spot_lights: u32,
205    /// The number of reflection probes in the cluster.
206    pub reflection_probes: u32,
207    /// The number of irradiance volumes in the cluster.
208    pub irradiance_volumes: u32,
209    /// The number of decals in the cluster.
210    pub decals: u32,
211}
212
213/// An object that projects a decal onto surfaces within its bounds.
214///
215/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
216/// projects its images onto surfaces in the -Z direction (thus you may find
217/// [`Transform::looking_at`] useful).
218///
219/// Each decal may project any of a base color texture, a normal map, a
220/// metallic/roughness map, and/or a texture that specifies emissive light. In
221/// addition, you may associate an arbitrary integer [`Self::tag`] with each
222/// clustered decal, which Bevy doesn't use, but that you can use in your
223/// shaders in order to associate application-specific data with your decals.
224///
225/// Clustered decals are the highest-quality types of decals that Bevy supports,
226/// but they require bindless textures. This means that they presently can't be
227/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
228/// with forward or deferred rendering and don't require a prepass.
229#[derive(Component, Debug, Clone, Default, Reflect)]
230#[reflect(Component, Debug, Clone, Default)]
231#[require(Transform, ViewVisibility, Visibility, VisibilityClass)]
232#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
233pub struct ClusteredDecal {
234    /// The image that the clustered decal projects onto the base color of the
235    /// surface material.
236    ///
237    /// This must be a 2D image. If it has an alpha channel, it'll be alpha
238    /// blended with the underlying surface and/or other decals. All decal
239    /// images in the scene must use the same sampler.
240    pub base_color_texture: Option<Handle<Image>>,
241
242    /// The normal map that the clustered decal projects onto surfaces.
243    ///
244    /// Bevy uses the *Whiteout* method to combine normal maps from decals with
245    /// any normal map that the surface has, as described in the
246    /// [*Blending in Detail* article].
247    ///
248    /// Note that the normal map must be three-channel and must be in OpenGL
249    /// format, not DirectX format. That is, the green channel must point up,
250    /// not down.
251    ///
252    /// [*Blending in Detail* article]: https://blog.selfshadow.com/publications/blending-in-detail/
253    pub normal_map_texture: Option<Handle<Image>>,
254
255    /// The metallic-roughness map that the clustered decal projects onto
256    /// surfaces.
257    ///
258    /// Metallic and roughness PBR parameters are blended onto the base surface
259    /// using the alpha channel of the base color.
260    ///
261    /// Metallic is expected to be in the blue channel, while roughness is
262    /// expected to be in the green channel, following glTF conventions.
263    pub metallic_roughness_texture: Option<Handle<Image>>,
264
265    /// The emissive map that the clustered decal projects onto surfaces.
266    ///
267    /// Including this texture effectively causes the decal to glow. The
268    /// emissive component is blended onto the surface according to the alpha
269    /// channel.
270    pub emissive_texture: Option<Handle<Image>>,
271
272    /// An application-specific tag you can use for any purpose you want, in
273    /// conjunction with a custom shader.
274    ///
275    /// This value is exposed to the shader via the iterator API
276    /// (`bevy_pbr::decal::clustered::clustered_decal_iterator_new` and
277    /// `bevy_pbr::decal::clustered::clustered_decal_iterator_next`).
278    ///
279    /// For example, you might use the tag to restrict the set of surfaces to
280    /// which a decal can be rendered.
281    ///
282    /// See the `clustered_decals` example for an example of use.
283    pub tag: u32,
284}
285
286impl Default for ClusterZConfig {
287    fn default() -> Self {
288        Self {
289            first_slice_depth: 5.0,
290            far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
291        }
292    }
293}
294
295impl Default for ClusterConfig {
296    fn default() -> Self {
297        // 24 depth slices, square clusters with at most 4096 total clusters
298        // use max light distance as clusters max `Z`-depth, first slice extends to 5.0
299        Self::FixedZ {
300            total: 4096,
301            z_slices: 24,
302            z_config: ClusterZConfig::default(),
303            dynamic_resizing: true,
304        }
305    }
306}
307
308impl ClusterConfig {
309    fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
310        match &self {
311            ClusterConfig::None => UVec3::ZERO,
312            ClusterConfig::Single => UVec3::ONE,
313            ClusterConfig::XYZ { dimensions, .. } => *dimensions,
314            ClusterConfig::FixedZ {
315                total, z_slices, ..
316            } => {
317                let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
318                    .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
319                    .ratio();
320                let mut z_slices = *z_slices;
321                if *total < z_slices {
322                    warn!("ClusterConfig has more z-slices than total clusters!");
323                    z_slices = *total;
324                }
325                let per_layer = *total as f32 / z_slices as f32;
326
327                let y = f32::sqrt(per_layer / aspect_ratio);
328
329                let mut x = (y * aspect_ratio) as u32;
330                let mut y = y as u32;
331
332                // check extremes
333                if x == 0 {
334                    x = 1;
335                    y = per_layer as u32;
336                }
337                if y == 0 {
338                    x = per_layer as u32;
339                    y = 1;
340                }
341
342                UVec3::new(x, y, z_slices)
343            }
344        }
345    }
346
347    fn first_slice_depth(&self) -> f32 {
348        match self {
349            ClusterConfig::None | ClusterConfig::Single => 0.0,
350            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
351                z_config.first_slice_depth
352            }
353        }
354    }
355
356    fn far_z_mode(&self) -> ClusterFarZMode {
357        match self {
358            ClusterConfig::None => ClusterFarZMode::Constant(0.0),
359            ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
360            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
361                z_config.far_z_mode
362            }
363        }
364    }
365
366    fn dynamic_resizing(&self) -> bool {
367        match self {
368            ClusterConfig::None | ClusterConfig::Single => false,
369            ClusterConfig::XYZ {
370                dynamic_resizing, ..
371            }
372            | ClusterConfig::FixedZ {
373                dynamic_resizing, ..
374            } => *dynamic_resizing,
375        }
376    }
377}
378
379impl Default for Clusters {
380    fn default() -> Clusters {
381        Clusters {
382            tile_size: UVec2::ZERO,
383            dimensions: UVec3::ZERO,
384            near: 0.0,
385            far: 0.0,
386            last_frame_farthest_z: None,
387            last_frame_total_cluster_index_count: None,
388            // Although we start with CPU clustering, this will be switched to
389            // GPU clustering if that's enabled.
390            clusterable_objects: ClusterableObjects::Cpu(vec![]),
391        }
392    }
393}
394
395impl Clusters {
396    fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
397        debug_assert!(
398            requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
399        );
400
401        let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
402            .ceil()
403            .as_uvec2()
404            .max(UVec2::ONE);
405        self.tile_size = tile_size;
406        self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
407            .ceil()
408            .as_uvec2()
409            .extend(requested_dimensions.z)
410            .max(UVec3::ONE);
411
412        // NOTE: Maximum 4096 clusters due to uniform buffer size constraints
413        debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
414    }
415
416    fn clear(&mut self, global_cluster_settings: &GlobalClusterSettings) {
417        self.tile_size = UVec2::ONE;
418        self.dimensions = UVec3::ZERO;
419        self.near = 0.0;
420        self.far = 0.0;
421
422        match (
423            &mut self.clusterable_objects,
424            &global_cluster_settings.gpu_clustering,
425        ) {
426            (ClusterableObjects::Cpu(_), Some(_)) => {
427                self.clusterable_objects = ClusterableObjects::Gpu;
428            }
429            (ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
430                objects_in_cluster_cpu.clear();
431            }
432            (ClusterableObjects::Gpu, Some(_)) => {
433                self.clusterable_objects = ClusterableObjects::Cpu(vec![]);
434            }
435            (ClusterableObjects::Gpu, None) => {}
436        }
437    }
438
439    fn reset_for_new_frame(
440        &mut self,
441        cluster_count: usize,
442        global_cluster_settings: &GlobalClusterSettings,
443    ) {
444        match (
445            &mut self.clusterable_objects,
446            &global_cluster_settings.gpu_clustering,
447        ) {
448            (ClusterableObjects::Cpu(_), Some(_)) => {
449                self.clusterable_objects = ClusterableObjects::Gpu;
450            }
451
452            (ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
453                for clusterable_objects in objects_in_cluster_cpu.iter_mut() {
454                    clusterable_objects.clear();
455                }
456                objects_in_cluster_cpu.resize_with(cluster_count, ObjectsInClusterCpu::default);
457            }
458
459            (ClusterableObjects::Gpu, Some(_)) => {}
460
461            (ClusterableObjects::Gpu, None) => {
462                self.clusterable_objects =
463                    ClusterableObjects::Cpu(vec![ObjectsInClusterCpu::default(); cluster_count]);
464            }
465        }
466    }
467}
468
469impl ObjectsInClusterCpu {
470    /// Clears out all objects in this cluster in preparation for a new frame.
471    pub fn clear(&mut self) {
472        self.clusterables.clear();
473        self.counts = ClusterableObjectCounts::default();
474    }
475
476    /// Adds a spot light to the list.
477    pub fn add_spot_light(&mut self, entity: Entity) {
478        self.clusterables.push(entity);
479        self.counts.spot_lights += 1;
480    }
481
482    /// Adds a point light to the list.
483    pub fn add_point_light(&mut self, entity: Entity) {
484        self.clusterables.push(entity);
485        self.counts.point_lights += 1;
486    }
487
488    /// Adds a reflection probe to the list.
489    pub fn add_reflection_probe(&mut self, entity: Entity) {
490        self.clusterables.push(entity);
491        self.counts.reflection_probes += 1;
492    }
493
494    /// Adds an irradiance volume to the list.
495    pub fn add_irradiance_volume(&mut self, entity: Entity) {
496        self.clusterables.push(entity);
497        self.counts.irradiance_volumes += 1;
498    }
499
500    /// Adds a decal to the list.
501    pub fn add_decal(&mut self, entity: Entity) {
502        self.clusterables.push(entity);
503        self.counts.decals += 1;
504    }
505
506    /// Iterates through all objects in this cluster.
507    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
508        self.clusterables.iter()
509    }
510}
511
512/// A system that adds AABBs to light probes and decals so that the visibility
513/// determination works for them.
514pub fn add_light_probe_and_decal_aabbs(
515    mut commands: Commands,
516    light_probes_and_decals_query: Query<
517        Entity,
518        (Or<(With<ClusteredDecal>, With<LightProbe>)>, Without<Aabb>),
519    >,
520) {
521    for entity in &light_probes_and_decals_query {
522        commands.entity(entity).insert(Aabb {
523            center: Vec3A::ZERO,
524            // Light probes are always unit-cube sized, the transform scale is what gives them their size.
525            // Scale should not be included in the Aabb because it gets transformed by the GlobalTransform.
526            half_extents: Vec3A::splat(0.5),
527        });
528    }
529}