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 rectangle lights in the cluster.
206    pub rect_lights: u32,
207    /// The number of reflection probes in the cluster.
208    pub reflection_probes: u32,
209    /// The number of irradiance volumes in the cluster.
210    pub irradiance_volumes: u32,
211    /// The number of decals in the cluster.
212    pub decals: u32,
213}
214
215/// An object that projects a decal onto surfaces within its bounds.
216///
217/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
218/// projects its images onto surfaces in the -Z direction (thus you may find
219/// [`Transform::looking_at`] useful).
220///
221/// Each decal may project any of a base color texture, a normal map, a
222/// metallic/roughness map, and/or a texture that specifies emissive light. In
223/// addition, you may associate an arbitrary integer [`Self::tag`] with each
224/// clustered decal, which Bevy doesn't use, but that you can use in your
225/// shaders in order to associate application-specific data with your decals.
226///
227/// Clustered decals are the highest-quality types of decals that Bevy supports,
228/// but they require bindless textures. This means that they presently can't be
229/// used on WebGL 2 or WebGPU. Bevy's clustered decals can be used with forward
230/// or deferred rendering and don't require a prepass.
231#[derive(Component, Debug, Clone, Default, Reflect)]
232#[reflect(Component, Debug, Clone, Default)]
233#[require(Transform, ViewVisibility, Visibility, VisibilityClass)]
234#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
235pub struct ClusteredDecal {
236    /// The image that the clustered decal projects onto the base color of the
237    /// surface material.
238    ///
239    /// This must be a 2D image. If it has an alpha channel, it'll be alpha
240    /// blended with the underlying surface and/or other decals. All decal
241    /// images in the scene must use the same sampler.
242    pub base_color_texture: Option<Handle<Image>>,
243
244    /// The normal map that the clustered decal projects onto surfaces.
245    ///
246    /// Bevy uses the *Whiteout* method to combine normal maps from decals with
247    /// any normal map that the surface has, as described in the
248    /// [*Blending in Detail* article].
249    ///
250    /// Note that the normal map must be three-channel and must be in OpenGL
251    /// format, not DirectX format. That is, the green channel must point up,
252    /// not down.
253    ///
254    /// [*Blending in Detail* article]: https://blog.selfshadow.com/publications/blending-in-detail/
255    pub normal_map_texture: Option<Handle<Image>>,
256
257    /// The metallic-roughness map that the clustered decal projects onto
258    /// surfaces.
259    ///
260    /// Metallic and roughness PBR parameters are blended onto the base surface
261    /// using the alpha channel of the base color.
262    ///
263    /// Metallic is expected to be in the blue channel, while roughness is
264    /// expected to be in the green channel, following glTF conventions.
265    pub metallic_roughness_texture: Option<Handle<Image>>,
266
267    /// The emissive map that the clustered decal projects onto surfaces.
268    ///
269    /// Including this texture effectively causes the decal to glow. The
270    /// emissive component is blended onto the surface according to the alpha
271    /// channel.
272    pub emissive_texture: Option<Handle<Image>>,
273
274    /// An application-specific tag you can use for any purpose you want, in
275    /// conjunction with a custom shader.
276    ///
277    /// This value is exposed to the shader via the iterator API
278    /// (`bevy_pbr::decal::clustered::clustered_decal_iterator_new` and
279    /// `bevy_pbr::decal::clustered::clustered_decal_iterator_next`).
280    ///
281    /// For example, you might use the tag to restrict the set of surfaces to
282    /// which a decal can be rendered.
283    ///
284    /// See the `clustered_decals` example for an example of use.
285    pub tag: u32,
286}
287
288impl Default for ClusterZConfig {
289    fn default() -> Self {
290        Self {
291            first_slice_depth: 5.0,
292            far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
293        }
294    }
295}
296
297impl Default for ClusterConfig {
298    fn default() -> Self {
299        // 24 depth slices, square clusters with at most 4096 total clusters
300        // use max light distance as clusters max `Z`-depth, first slice extends to 5.0
301        Self::FixedZ {
302            total: 4096,
303            z_slices: 24,
304            z_config: ClusterZConfig::default(),
305            dynamic_resizing: true,
306        }
307    }
308}
309
310impl ClusterConfig {
311    fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
312        match &self {
313            ClusterConfig::None => UVec3::ZERO,
314            ClusterConfig::Single => UVec3::ONE,
315            ClusterConfig::XYZ { dimensions, .. } => *dimensions,
316            ClusterConfig::FixedZ {
317                total, z_slices, ..
318            } => {
319                let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
320                    .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
321                    .ratio();
322                let mut z_slices = *z_slices;
323                if *total < z_slices {
324                    warn!("ClusterConfig has more z-slices than total clusters!");
325                    z_slices = *total;
326                }
327                let per_layer = *total as f32 / z_slices as f32;
328
329                let y = f32::sqrt(per_layer / aspect_ratio);
330
331                let mut x = (y * aspect_ratio) as u32;
332                let mut y = y as u32;
333
334                // check extremes
335                if x == 0 {
336                    x = 1;
337                    y = per_layer as u32;
338                }
339                if y == 0 {
340                    x = per_layer as u32;
341                    y = 1;
342                }
343
344                UVec3::new(x, y, z_slices)
345            }
346        }
347    }
348
349    fn first_slice_depth(&self) -> f32 {
350        match self {
351            ClusterConfig::None | ClusterConfig::Single => 0.0,
352            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
353                z_config.first_slice_depth
354            }
355        }
356    }
357
358    fn far_z_mode(&self) -> ClusterFarZMode {
359        match self {
360            ClusterConfig::None => ClusterFarZMode::Constant(0.0),
361            ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
362            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
363                z_config.far_z_mode
364            }
365        }
366    }
367
368    fn dynamic_resizing(&self) -> bool {
369        match self {
370            ClusterConfig::None | ClusterConfig::Single => false,
371            ClusterConfig::XYZ {
372                dynamic_resizing, ..
373            }
374            | ClusterConfig::FixedZ {
375                dynamic_resizing, ..
376            } => *dynamic_resizing,
377        }
378    }
379}
380
381impl Default for Clusters {
382    fn default() -> Clusters {
383        Clusters {
384            tile_size: UVec2::ZERO,
385            dimensions: UVec3::ZERO,
386            near: 0.0,
387            far: 0.0,
388            last_frame_farthest_z: None,
389            last_frame_total_cluster_index_count: None,
390            // Although we start with CPU clustering, this will be switched to
391            // GPU clustering if that's enabled.
392            clusterable_objects: ClusterableObjects::Cpu(vec![]),
393        }
394    }
395}
396
397impl Clusters {
398    fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
399        debug_assert!(
400            requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
401        );
402
403        let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
404            .ceil()
405            .as_uvec2()
406            .max(UVec2::ONE);
407        self.tile_size = tile_size;
408        self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
409            .ceil()
410            .as_uvec2()
411            .extend(requested_dimensions.z)
412            .max(UVec3::ONE);
413
414        // NOTE: Maximum 4096 clusters due to uniform buffer size constraints
415        debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
416    }
417
418    fn clear(&mut self, global_cluster_settings: &GlobalClusterSettings) {
419        self.tile_size = UVec2::ONE;
420        self.dimensions = UVec3::ZERO;
421        self.near = 0.0;
422        self.far = 0.0;
423
424        match (
425            &mut self.clusterable_objects,
426            &global_cluster_settings.gpu_clustering,
427        ) {
428            (ClusterableObjects::Cpu(_), Some(_)) => {
429                self.clusterable_objects = ClusterableObjects::Gpu;
430            }
431            (ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
432                objects_in_cluster_cpu.clear();
433            }
434            (ClusterableObjects::Gpu, Some(_)) => {
435                self.clusterable_objects = ClusterableObjects::Cpu(vec![]);
436            }
437            (ClusterableObjects::Gpu, None) => {}
438        }
439    }
440
441    fn reset_for_new_frame(
442        &mut self,
443        cluster_count: usize,
444        global_cluster_settings: &GlobalClusterSettings,
445    ) {
446        match (
447            &mut self.clusterable_objects,
448            &global_cluster_settings.gpu_clustering,
449        ) {
450            (ClusterableObjects::Cpu(_), Some(_)) => {
451                self.clusterable_objects = ClusterableObjects::Gpu;
452            }
453
454            (ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
455                for clusterable_objects in objects_in_cluster_cpu.iter_mut() {
456                    clusterable_objects.clear();
457                }
458                objects_in_cluster_cpu.resize_with(cluster_count, ObjectsInClusterCpu::default);
459            }
460
461            (ClusterableObjects::Gpu, Some(_)) => {}
462
463            (ClusterableObjects::Gpu, None) => {
464                self.clusterable_objects =
465                    ClusterableObjects::Cpu(vec![ObjectsInClusterCpu::default(); cluster_count]);
466            }
467        }
468    }
469}
470
471impl ObjectsInClusterCpu {
472    /// Clears out all objects in this cluster in preparation for a new frame.
473    pub fn clear(&mut self) {
474        self.clusterables.clear();
475        self.counts = ClusterableObjectCounts::default();
476    }
477
478    /// Adds a spot light to the list.
479    pub fn add_spot_light(&mut self, entity: Entity) {
480        self.clusterables.push(entity);
481        self.counts.spot_lights += 1;
482    }
483
484    /// Adds a point light to the list.
485    pub fn add_point_light(&mut self, entity: Entity) {
486        self.clusterables.push(entity);
487        self.counts.point_lights += 1;
488    }
489
490    /// Adds a rectangle light to the list.
491    pub fn add_rect_light(&mut self, entity: Entity) {
492        self.clusterables.push(entity);
493        self.counts.rect_lights += 1;
494    }
495
496    /// Adds a reflection probe to the list.
497    pub fn add_reflection_probe(&mut self, entity: Entity) {
498        self.clusterables.push(entity);
499        self.counts.reflection_probes += 1;
500    }
501
502    /// Adds an irradiance volume to the list.
503    pub fn add_irradiance_volume(&mut self, entity: Entity) {
504        self.clusterables.push(entity);
505        self.counts.irradiance_volumes += 1;
506    }
507
508    /// Adds a decal to the list.
509    pub fn add_decal(&mut self, entity: Entity) {
510        self.clusterables.push(entity);
511        self.counts.decals += 1;
512    }
513
514    /// Iterates through all objects in this cluster.
515    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
516        self.clusterables.iter()
517    }
518}
519
520/// A system that adds AABBs to light probes and decals so that the visibility
521/// determination works for them.
522pub fn add_light_probe_and_decal_aabbs(
523    mut commands: Commands,
524    light_probes_and_decals_query: Query<
525        Entity,
526        (Or<(With<ClusteredDecal>, With<LightProbe>)>, Without<Aabb>),
527    >,
528) {
529    for entity in &light_probes_and_decals_query {
530        commands.entity(entity).insert(Aabb {
531            center: Vec3A::ZERO,
532            // Light probes are always unit-cube sized, the transform scale is what gives them their size.
533            // Scale should not be included in the Aabb because it gets transformed by the GlobalTransform.
534            half_extents: Vec3A::splat(0.5),
535        });
536    }
537}