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}