bevy_light/cluster/
mod.rs

1//! Spatial clustering of objects, currently just point and spot lights.
2
3use bevy_asset::Handle;
4use bevy_camera::{
5    visibility::{self, Visibility, VisibilityClass},
6    Camera, Camera3d,
7};
8use bevy_ecs::{
9    component::Component,
10    entity::Entity,
11    query::{With, Without},
12    reflect::ReflectComponent,
13    resource::Resource,
14    system::{Commands, Query},
15};
16use bevy_image::Image;
17use bevy_math::{AspectRatio, UVec2, UVec3, Vec3Swizzles as _};
18use bevy_platform::collections::HashSet;
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_transform::components::Transform;
21use tracing::warn;
22
23pub mod assign;
24
25#[cfg(test)]
26mod test;
27
28// Clustered-forward rendering notes
29// The main initial reference material used was this rather accessible article:
30// http://www.aortiz.me/2018/12/21/CG.html
31// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of:
32// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/
33// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.)
34// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016:
35// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf
36
37#[derive(Resource)]
38pub struct GlobalClusterSettings {
39    pub supports_storage_buffers: bool,
40    pub clustered_decals_are_usable: bool,
41    pub max_uniform_buffer_clusterable_objects: usize,
42    pub view_cluster_bindings_max_indices: usize,
43}
44
45/// Configure the far z-plane mode used for the furthest depth slice for clustered forward
46/// rendering
47#[derive(Debug, Copy, Clone, Reflect)]
48#[reflect(Clone)]
49pub enum ClusterFarZMode {
50    /// Calculate the required maximum z-depth based on currently visible
51    /// clusterable objects.  Makes better use of available clusters, speeding
52    /// up GPU lighting operations at the expense of some CPU time and using
53    /// more indices in the clusterable object index lists.
54    MaxClusterableObjectRange,
55    /// Constant max z-depth
56    Constant(f32),
57}
58
59/// Configure the depth-slicing strategy for clustered forward rendering
60#[derive(Debug, Copy, Clone, Reflect)]
61#[reflect(Default, Clone)]
62pub struct ClusterZConfig {
63    /// Far `Z` plane of the first depth slice
64    pub first_slice_depth: f32,
65    /// Strategy for how to evaluate the far `Z` plane of the furthest depth slice
66    pub far_z_mode: ClusterFarZMode,
67}
68
69/// Configuration of the clustering strategy for clustered forward rendering
70#[derive(Debug, Copy, Clone, Component, Reflect)]
71#[reflect(Component, Debug, Default, Clone)]
72pub enum ClusterConfig {
73    /// Disable cluster calculations for this view
74    None,
75    /// One single cluster. Optimal for low-light complexity scenes or scenes where
76    /// most lights affect the entire scene.
77    Single,
78    /// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio)
79    XYZ {
80        dimensions: UVec3,
81        z_config: ClusterZConfig,
82        /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
83        /// the available cluster-object index limit
84        dynamic_resizing: bool,
85    },
86    /// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters
87    /// with at most total clusters. For top-down games where lights will generally always be within a
88    /// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This
89    /// would reduce the number of lights per cluster by distributing more clusters in screen space
90    /// `X/Y` which matches how lights are distributed in the scene.
91    FixedZ {
92        total: u32,
93        z_slices: u32,
94        z_config: ClusterZConfig,
95        /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
96        /// the available clusterable object index limit
97        dynamic_resizing: bool,
98    },
99}
100
101#[derive(Component, Debug, Default)]
102pub struct Clusters {
103    /// Tile size
104    pub tile_size: UVec2,
105    /// Number of clusters in `X` / `Y` / `Z` in the view frustum
106    pub dimensions: UVec3,
107    /// Distance to the far plane of the first depth slice. The first depth slice is special
108    /// and explicitly-configured to avoid having unnecessarily many slices close to the camera.
109    pub near: f32,
110    pub far: f32,
111    pub clusterable_objects: Vec<VisibleClusterableObjects>,
112}
113
114/// The [`VisibilityClass`] used for clusterables (decals, point lights, directional lights, and spot lights).
115///
116/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass
117pub struct ClusterVisibilityClass;
118
119#[derive(Clone, Component, Debug, Default)]
120pub struct VisibleClusterableObjects {
121    pub entities: Vec<Entity>,
122    pub counts: ClusterableObjectCounts,
123}
124
125#[derive(Resource, Default)]
126pub struct GlobalVisibleClusterableObjects {
127    pub(crate) entities: HashSet<Entity>,
128}
129
130/// Stores the number of each type of clusterable object in a single cluster.
131///
132/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if
133/// fewer than 3 SSBOs are available, which usually means on WebGL 2.
134#[derive(Clone, Copy, Default, Debug)]
135pub struct ClusterableObjectCounts {
136    /// The number of point lights in the cluster.
137    pub point_lights: u32,
138    /// The number of spot lights in the cluster.
139    pub spot_lights: u32,
140    /// The number of reflection probes in the cluster.
141    pub reflection_probes: u32,
142    /// The number of irradiance volumes in the cluster.
143    pub irradiance_volumes: u32,
144    /// The number of decals in the cluster.
145    pub decals: u32,
146}
147
148/// An object that projects a decal onto surfaces within its bounds.
149///
150/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
151/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus
152/// you may find [`Transform::looking_at`] useful).
153///
154/// Clustered decals are the highest-quality types of decals that Bevy supports,
155/// but they require bindless textures. This means that they presently can't be
156/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
157/// with forward or deferred rendering and don't require a prepass.
158#[derive(Component, Debug, Clone, Reflect)]
159#[reflect(Component, Debug, Clone)]
160#[require(Transform, Visibility, VisibilityClass)]
161#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
162pub struct ClusteredDecal {
163    /// The image that the clustered decal projects.
164    ///
165    /// This must be a 2D image. If it has an alpha channel, it'll be alpha
166    /// blended with the underlying surface and/or other decals. All decal
167    /// images in the scene must use the same sampler.
168    pub image: Handle<Image>,
169
170    /// An application-specific tag you can use for any purpose you want.
171    ///
172    /// See the `clustered_decals` example for an example of use.
173    pub tag: u32,
174}
175
176impl Default for ClusterZConfig {
177    fn default() -> Self {
178        Self {
179            first_slice_depth: 5.0,
180            far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
181        }
182    }
183}
184
185impl Default for ClusterConfig {
186    fn default() -> Self {
187        // 24 depth slices, square clusters with at most 4096 total clusters
188        // use max light distance as clusters max `Z`-depth, first slice extends to 5.0
189        Self::FixedZ {
190            total: 4096,
191            z_slices: 24,
192            z_config: ClusterZConfig::default(),
193            dynamic_resizing: true,
194        }
195    }
196}
197
198impl ClusterConfig {
199    fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
200        match &self {
201            ClusterConfig::None => UVec3::ZERO,
202            ClusterConfig::Single => UVec3::ONE,
203            ClusterConfig::XYZ { dimensions, .. } => *dimensions,
204            ClusterConfig::FixedZ {
205                total, z_slices, ..
206            } => {
207                let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
208                    .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
209                    .ratio();
210                let mut z_slices = *z_slices;
211                if *total < z_slices {
212                    warn!("ClusterConfig has more z-slices than total clusters!");
213                    z_slices = *total;
214                }
215                let per_layer = *total as f32 / z_slices as f32;
216
217                let y = f32::sqrt(per_layer / aspect_ratio);
218
219                let mut x = (y * aspect_ratio) as u32;
220                let mut y = y as u32;
221
222                // check extremes
223                if x == 0 {
224                    x = 1;
225                    y = per_layer as u32;
226                }
227                if y == 0 {
228                    x = per_layer as u32;
229                    y = 1;
230                }
231
232                UVec3::new(x, y, z_slices)
233            }
234        }
235    }
236
237    fn first_slice_depth(&self) -> f32 {
238        match self {
239            ClusterConfig::None | ClusterConfig::Single => 0.0,
240            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
241                z_config.first_slice_depth
242            }
243        }
244    }
245
246    fn far_z_mode(&self) -> ClusterFarZMode {
247        match self {
248            ClusterConfig::None => ClusterFarZMode::Constant(0.0),
249            ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
250            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
251                z_config.far_z_mode
252            }
253        }
254    }
255
256    fn dynamic_resizing(&self) -> bool {
257        match self {
258            ClusterConfig::None | ClusterConfig::Single => false,
259            ClusterConfig::XYZ {
260                dynamic_resizing, ..
261            }
262            | ClusterConfig::FixedZ {
263                dynamic_resizing, ..
264            } => *dynamic_resizing,
265        }
266    }
267}
268
269impl Clusters {
270    fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
271        debug_assert!(
272            requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
273        );
274
275        let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
276            .ceil()
277            .as_uvec2()
278            .max(UVec2::ONE);
279        self.tile_size = tile_size;
280        self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
281            .ceil()
282            .as_uvec2()
283            .extend(requested_dimensions.z)
284            .max(UVec3::ONE);
285
286        // NOTE: Maximum 4096 clusters due to uniform buffer size constraints
287        debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
288    }
289    fn clear(&mut self) {
290        self.tile_size = UVec2::ONE;
291        self.dimensions = UVec3::ZERO;
292        self.near = 0.0;
293        self.far = 0.0;
294        self.clusterable_objects.clear();
295    }
296}
297
298pub fn add_clusters(
299    mut commands: Commands,
300    cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without<Clusters>, With<Camera3d>)>,
301) {
302    for (entity, config, camera) in &cameras {
303        if !camera.is_active {
304            continue;
305        }
306
307        let config = config.copied().unwrap_or_default();
308        // actual settings here don't matter - they will be overwritten in
309        // `assign_objects_to_clusters``
310        commands
311            .entity(entity)
312            .insert((Clusters::default(), config));
313    }
314}
315
316impl VisibleClusterableObjects {
317    #[inline]
318    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
319        self.entities.iter()
320    }
321
322    #[inline]
323    pub fn len(&self) -> usize {
324        self.entities.len()
325    }
326
327    #[inline]
328    pub fn is_empty(&self) -> bool {
329        self.entities.is_empty()
330    }
331}
332
333impl GlobalVisibleClusterableObjects {
334    #[inline]
335    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
336        self.entities.iter()
337    }
338
339    #[inline]
340    pub fn contains(&self, entity: Entity) -> bool {
341        self.entities.contains(&entity)
342    }
343}