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 its images onto surfaces in the -Z direction (thus you may find
152/// [`Transform::looking_at`] useful).
153///
154/// Each decal may project any of a base color texture, a normal map, a
155/// metallic/roughness map, and/or a texture that specifies emissive light. In
156/// addition, you may associate an arbitrary integer [`Self::tag`] with each
157/// clustered decal, which Bevy doesn't use, but that you can use in your
158/// shaders in order to associate application-specific data with your decals.
159///
160/// Clustered decals are the highest-quality types of decals that Bevy supports,
161/// but they require bindless textures. This means that they presently can't be
162/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
163/// with forward or deferred rendering and don't require a prepass.
164#[derive(Component, Debug, Clone, Default, Reflect)]
165#[reflect(Component, Debug, Clone, Default)]
166#[require(Transform, Visibility, VisibilityClass)]
167#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
168pub struct ClusteredDecal {
169    /// The image that the clustered decal projects onto the base color of the
170    /// surface material.
171    ///
172    /// This must be a 2D image. If it has an alpha channel, it'll be alpha
173    /// blended with the underlying surface and/or other decals. All decal
174    /// images in the scene must use the same sampler.
175    pub base_color_texture: Option<Handle<Image>>,
176
177    /// The normal map that the clustered decal projects onto surfaces.
178    ///
179    /// Bevy uses the *Whiteout* method to combine normal maps from decals with
180    /// any normal map that the surface has, as described in the
181    /// [*Blending in Detail* article].
182    ///
183    /// Note that the normal map must be three-channel and must be in OpenGL
184    /// format, not DirectX format. That is, the green channel must point up,
185    /// not down.
186    ///
187    /// [*Blending in Detail* article]: https://blog.selfshadow.com/publications/blending-in-detail/
188    pub normal_map_texture: Option<Handle<Image>>,
189
190    /// The metallic-roughness map that the clustered decal projects onto
191    /// surfaces.
192    ///
193    /// Metallic and roughness PBR parameters are blended onto the base surface
194    /// using the alpha channel of the base color.
195    ///
196    /// Metallic is expected to be in the blue channel, while roughness is
197    /// expected to be in the green channel, following glTF conventions.
198    pub metallic_roughness_texture: Option<Handle<Image>>,
199
200    /// The emissive map that the clustered decal projects onto surfaces.
201    ///
202    /// Including this texture effectively causes the decal to glow. The
203    /// emissive component is blended onto the surface according to the alpha
204    /// channel.
205    pub emissive_texture: Option<Handle<Image>>,
206
207    /// An application-specific tag you can use for any purpose you want, in
208    /// conjunction with a custom shader.
209    ///
210    /// This value is exposed to the shader via the iterator API
211    /// (`bevy_pbr::decal::clustered::clustered_decal_iterator_new` and
212    /// `bevy_pbr::decal::clustered::clustered_decal_iterator_next`).
213    ///
214    /// For example, you might use the tag to restrict the set of surfaces to
215    /// which a decal can be rendered.
216    ///
217    /// See the `clustered_decals` example for an example of use.
218    pub tag: u32,
219}
220
221impl Default for ClusterZConfig {
222    fn default() -> Self {
223        Self {
224            first_slice_depth: 5.0,
225            far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
226        }
227    }
228}
229
230impl Default for ClusterConfig {
231    fn default() -> Self {
232        // 24 depth slices, square clusters with at most 4096 total clusters
233        // use max light distance as clusters max `Z`-depth, first slice extends to 5.0
234        Self::FixedZ {
235            total: 4096,
236            z_slices: 24,
237            z_config: ClusterZConfig::default(),
238            dynamic_resizing: true,
239        }
240    }
241}
242
243impl ClusterConfig {
244    fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
245        match &self {
246            ClusterConfig::None => UVec3::ZERO,
247            ClusterConfig::Single => UVec3::ONE,
248            ClusterConfig::XYZ { dimensions, .. } => *dimensions,
249            ClusterConfig::FixedZ {
250                total, z_slices, ..
251            } => {
252                let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
253                    .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
254                    .ratio();
255                let mut z_slices = *z_slices;
256                if *total < z_slices {
257                    warn!("ClusterConfig has more z-slices than total clusters!");
258                    z_slices = *total;
259                }
260                let per_layer = *total as f32 / z_slices as f32;
261
262                let y = f32::sqrt(per_layer / aspect_ratio);
263
264                let mut x = (y * aspect_ratio) as u32;
265                let mut y = y as u32;
266
267                // check extremes
268                if x == 0 {
269                    x = 1;
270                    y = per_layer as u32;
271                }
272                if y == 0 {
273                    x = per_layer as u32;
274                    y = 1;
275                }
276
277                UVec3::new(x, y, z_slices)
278            }
279        }
280    }
281
282    fn first_slice_depth(&self) -> f32 {
283        match self {
284            ClusterConfig::None | ClusterConfig::Single => 0.0,
285            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
286                z_config.first_slice_depth
287            }
288        }
289    }
290
291    fn far_z_mode(&self) -> ClusterFarZMode {
292        match self {
293            ClusterConfig::None => ClusterFarZMode::Constant(0.0),
294            ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
295            ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
296                z_config.far_z_mode
297            }
298        }
299    }
300
301    fn dynamic_resizing(&self) -> bool {
302        match self {
303            ClusterConfig::None | ClusterConfig::Single => false,
304            ClusterConfig::XYZ {
305                dynamic_resizing, ..
306            }
307            | ClusterConfig::FixedZ {
308                dynamic_resizing, ..
309            } => *dynamic_resizing,
310        }
311    }
312}
313
314impl Clusters {
315    fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
316        debug_assert!(
317            requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
318        );
319
320        let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
321            .ceil()
322            .as_uvec2()
323            .max(UVec2::ONE);
324        self.tile_size = tile_size;
325        self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
326            .ceil()
327            .as_uvec2()
328            .extend(requested_dimensions.z)
329            .max(UVec3::ONE);
330
331        // NOTE: Maximum 4096 clusters due to uniform buffer size constraints
332        debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
333    }
334    fn clear(&mut self) {
335        self.tile_size = UVec2::ONE;
336        self.dimensions = UVec3::ZERO;
337        self.near = 0.0;
338        self.far = 0.0;
339        self.clusterable_objects.clear();
340    }
341}
342
343pub fn add_clusters(
344    mut commands: Commands,
345    cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without<Clusters>, With<Camera3d>)>,
346) {
347    for (entity, config, camera) in &cameras {
348        if !camera.is_active {
349            continue;
350        }
351
352        let config = config.copied().unwrap_or_default();
353        // actual settings here don't matter - they will be overwritten in
354        // `assign_objects_to_clusters``
355        commands
356            .entity(entity)
357            .insert((Clusters::default(), config));
358    }
359}
360
361impl VisibleClusterableObjects {
362    #[inline]
363    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
364        self.entities.iter()
365    }
366
367    #[inline]
368    pub fn len(&self) -> usize {
369        self.entities.len()
370    }
371
372    #[inline]
373    pub fn is_empty(&self) -> bool {
374        self.entities.is_empty()
375    }
376}
377
378impl GlobalVisibleClusterableObjects {
379    #[inline]
380    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
381        self.entities.iter()
382    }
383
384    #[inline]
385    pub fn contains(&self, entity: Entity) -> bool {
386        self.entities.contains(&entity)
387    }
388}