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}