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}