Skip to main content

bevy_light/cluster/
assign.rs

1//! Assigning objects to clusters.
2
3use bevy_camera::{
4    primitives::{Aabb, Frustum, Sphere},
5    visibility::{RenderLayers, ViewVisibility},
6    Camera,
7};
8use bevy_ecs::{
9    entity::Entity,
10    query::{Has, With},
11    system::{Local, Query, Res},
12};
13use bevy_math::{
14    ops::{self, sin_cos},
15    Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _,
16};
17use bevy_shape::HalfSpace;
18use bevy_transform::components::GlobalTransform;
19use tracing::{error, warn};
20
21use super::{ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings};
22use crate::{
23    cluster::ClusterableObjects, EnvironmentMapLight, LightProbe, PointLight, RectLight, SpotLight,
24    VolumetricLight,
25};
26
27const NDC_MIN: Vec2 = Vec2::NEG_ONE;
28const NDC_MAX: Vec2 = Vec2::ONE;
29
30const VEC2_HALF: Vec2 = Vec2::splat(0.5);
31const VEC2_HALF_NEGATIVE_Y: Vec2 = Vec2::new(0.5, -0.5);
32
33/// The depth of the farthest cluster that we use for the first frame if
34/// [`ClusterFarZMode::MaxClusterableObjectRange`] is in use.
35///
36/// We use this arbitrary value because, on the first frame, we have no
37/// clustering from the previous frame to refer to.
38const DEFAULT_FAR_DEPTH: f32 = 1000.0;
39
40/// Data required for assigning objects to clusters.
41#[derive(Clone, Debug)]
42pub(crate) struct ClusterableObjectAssignmentData {
43    entity: Entity,
44    // TODO: We currently ignore the scale on the transform. This is confusing.
45    // Replace with an `Isometry3d`.
46    transform: GlobalTransform,
47    range: f32,
48    object_type: ClusterableObjectType,
49    render_layers: RenderLayers,
50}
51
52impl ClusterableObjectAssignmentData {
53    pub fn sphere(&self) -> Sphere {
54        Sphere {
55            center: self.transform.translation_vec3a(),
56            radius: self.range,
57        }
58    }
59}
60
61/// Data needed to assign objects to clusters that's specific to the type of
62/// clusterable object.
63#[derive(Clone, Copy, Debug)]
64pub enum ClusterableObjectType {
65    /// Data needed to assign point lights to clusters.
66    PointLight {
67        /// Whether shadows are enabled for this point light.
68        ///
69        /// This is used for sorting the light list.
70        shadow_maps_enabled: bool,
71
72        /// Whether this light interacts with volumetrics.
73        ///
74        /// This is used for sorting the light list.
75        volumetric: bool,
76    },
77
78    /// Data needed to assign spot lights to clusters.
79    SpotLight {
80        /// Whether shadows are enabled for this spot light.
81        ///
82        /// This is used for sorting the light list.
83        shadow_maps_enabled: bool,
84
85        /// Whether this light interacts with volumetrics.
86        ///
87        /// This is used for sorting the light list.
88        volumetric: bool,
89
90        /// The outer angle of the light cone in radians.
91        outer_angle: f32,
92    },
93
94    /// Marks that the clusterable object is a rectangle light.
95    RectLight,
96
97    /// Marks that the clusterable object is a reflection probe.
98    ReflectionProbe,
99
100    /// Marks that the clusterable object is an irradiance volume.
101    IrradianceVolume,
102
103    /// Marks that the clusterable object is a decal.
104    Decal,
105}
106
107impl ClusterableObjectType {
108    /// Returns a tuple that can be sorted to obtain the order in which indices
109    /// to clusterable objects must be stored in the cluster offsets and counts
110    /// list.
111    ///
112    /// Generally, we sort first by type, then, for lights, by whether shadows
113    /// are enabled (enabled before disabled), and then whether volumetrics are
114    /// enabled (enabled before disabled).
115    pub fn ordering(&self) -> (u8, bool, bool) {
116        match *self {
117            ClusterableObjectType::PointLight {
118                shadow_maps_enabled,
119                volumetric,
120            } => (0, !shadow_maps_enabled, !volumetric),
121            ClusterableObjectType::SpotLight {
122                shadow_maps_enabled,
123                volumetric,
124                ..
125            } => (1, !shadow_maps_enabled, !volumetric),
126            ClusterableObjectType::RectLight => (2, false, false),
127            ClusterableObjectType::ReflectionProbe => (3, false, false),
128            ClusterableObjectType::IrradianceVolume => (4, false, false),
129            ClusterableObjectType::Decal => (5, false, false),
130        }
131    }
132}
133
134/// Clusters point lights, spot lights, light probes, and decals.
135///
136/// NOTE: Run this before `update_point_light_frusta`!
137pub(crate) fn assign_objects_to_clusters(
138    mut views: Query<(
139        &GlobalTransform,
140        &Camera,
141        &Frustum,
142        Option<&ClusterConfig>,
143        &mut Clusters,
144        Option<&RenderLayers>,
145    )>,
146    point_lights_query: Query<(
147        Entity,
148        &GlobalTransform,
149        &ViewVisibility,
150        &PointLight,
151        Option<&RenderLayers>,
152        Option<&VolumetricLight>,
153    )>,
154    spot_lights_query: Query<(
155        Entity,
156        &GlobalTransform,
157        &ViewVisibility,
158        &SpotLight,
159        Option<&RenderLayers>,
160        Option<&VolumetricLight>,
161    )>,
162    rect_lights_query: Query<(
163        Entity,
164        &GlobalTransform,
165        &ViewVisibility,
166        &RectLight,
167        Option<&RenderLayers>,
168    )>,
169    light_probes_query: Query<
170        (
171            Entity,
172            &GlobalTransform,
173            &ViewVisibility,
174            Has<EnvironmentMapLight>,
175        ),
176        With<LightProbe>,
177    >,
178    decals_query: Query<(Entity, &GlobalTransform, &ViewVisibility), With<ClusteredDecal>>,
179    mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
180    mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
181    mut max_clusterable_objects_warning_emitted: Local<bool>,
182    global_cluster_settings: Option<Res<GlobalClusterSettings>>,
183) {
184    let Some(global_cluster_settings) = global_cluster_settings else {
185        return;
186    };
187
188    clusterable_objects.clear();
189
190    // Collect clusterable objects if GPU clustering is disabled.
191    if global_cluster_settings.gpu_clustering.is_none() {
192        // collect just the relevant query data into a persisted vec to avoid reallocating each frame
193        clusterable_objects.extend(point_lights_query.iter().filter_map(
194            |(entity, transform, view_visibility, point_light, maybe_layers, volumetric)| {
195                if view_visibility.get() {
196                    Some(ClusterableObjectAssignmentData {
197                        entity,
198                        transform: GlobalTransform::from_translation(transform.translation()),
199                        range: point_light.range,
200                        object_type: ClusterableObjectType::PointLight {
201                            shadow_maps_enabled: point_light.shadow_maps_enabled,
202                            volumetric: volumetric.is_some(),
203                        },
204                        render_layers: maybe_layers.unwrap_or_default().clone(),
205                    })
206                } else {
207                    None
208                }
209            },
210        ));
211        clusterable_objects.extend(spot_lights_query.iter().filter_map(
212            |(entity, transform, view_visibility, spot_light, maybe_layers, volumetric)| {
213                if view_visibility.get() {
214                    Some(ClusterableObjectAssignmentData {
215                        entity,
216                        transform: *transform,
217                        range: spot_light.range,
218                        object_type: ClusterableObjectType::SpotLight {
219                            outer_angle: spot_light.outer_angle,
220                            shadow_maps_enabled: spot_light.shadow_maps_enabled,
221                            volumetric: volumetric.is_some(),
222                        },
223                        render_layers: maybe_layers.unwrap_or_default().clone(),
224                    })
225                } else {
226                    None
227                }
228            },
229        ));
230
231        // Gather up rect lights if we're clustering them.
232        if global_cluster_settings.supports_storage_buffers {
233            clusterable_objects.extend(rect_lights_query.iter().filter_map(
234                |(entity, transform, view_visibility, rect_light, maybe_layers)| {
235                    if view_visibility.get() {
236                        Some(ClusterableObjectAssignmentData {
237                            entity,
238                            transform: *transform,
239                            range: rect_light.range,
240                            object_type: ClusterableObjectType::RectLight,
241                            render_layers: maybe_layers.unwrap_or_default().clone(),
242                        })
243                    } else {
244                        None
245                    }
246                },
247            ));
248        }
249
250        // Gather up light probes, but only if we're clustering them.
251        //
252        // UBOs aren't large enough to hold indices for light probes, so we can't
253        // cluster light probes on such platforms (mainly WebGL 2). Besides, those
254        // platforms typically lack bindless textures, so multiple light probes
255        // wouldn't be supported anyhow.
256        if global_cluster_settings.supports_storage_buffers {
257            clusterable_objects.extend(light_probes_query.iter().filter_map(
258                |(entity, transform, view_visibility, is_reflection_probe)| {
259                    if view_visibility.get() {
260                        Some(ClusterableObjectAssignmentData {
261                            entity,
262                            transform: *transform,
263                            range: transform.radius_vec3a(Vec3A::ONE),
264                            object_type: if is_reflection_probe {
265                                ClusterableObjectType::ReflectionProbe
266                            } else {
267                                ClusterableObjectType::IrradianceVolume
268                            },
269                            render_layers: RenderLayers::default(),
270                        })
271                    } else {
272                        None
273                    }
274                },
275            ));
276        }
277
278        // Add decals if the current platform supports them.
279        if global_cluster_settings.clustered_decals_are_usable {
280            clusterable_objects.extend(decals_query.iter().filter_map(
281                |(entity, transform, view_visibility)| {
282                    if view_visibility.get() {
283                        Some(ClusterableObjectAssignmentData {
284                            entity,
285                            transform: *transform,
286                            range: transform.scale().length(),
287                            object_type: ClusterableObjectType::Decal,
288                            render_layers: RenderLayers::default(),
289                        })
290                    } else {
291                        None
292                    }
293                },
294            ));
295        }
296
297        if clusterable_objects.len()
298            > global_cluster_settings.max_uniform_buffer_clusterable_objects
299            && !global_cluster_settings.supports_storage_buffers
300        {
301            clusterable_objects.sort_by_cached_key(|clusterable_object| {
302                (
303                    clusterable_object.object_type.ordering(),
304                    clusterable_object.entity,
305                )
306            });
307
308            if clusterable_objects.len()
309                > global_cluster_settings.max_uniform_buffer_clusterable_objects
310                && !*max_clusterable_objects_warning_emitted
311            {
312                warn!(
313                    "max_uniform_buffer_clusterable_objects ({}) exceeded",
314                    global_cluster_settings.max_uniform_buffer_clusterable_objects
315                );
316                *max_clusterable_objects_warning_emitted = true;
317            }
318
319            clusterable_objects
320                .truncate(global_cluster_settings.max_uniform_buffer_clusterable_objects);
321        }
322    }
323
324    for (camera_transform, camera, frustum, config, clusters, maybe_layers) in &mut views {
325        let view_layers = maybe_layers.unwrap_or_default();
326        let clusters = clusters.into_inner();
327        let config = config.copied().unwrap_or_default();
328
329        if matches!(config, ClusterConfig::None) {
330            clusters.clear(&global_cluster_settings);
331            continue;
332        }
333
334        let screen_size = match camera.physical_viewport_size() {
335            Some(screen_size) if screen_size.x != 0 && screen_size.y != 0 => screen_size,
336            _ => {
337                clusters.clear(&global_cluster_settings);
338                continue;
339            }
340        };
341
342        let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size);
343
344        let world_from_view = camera_transform.affine();
345        let view_from_world_scale = camera_transform.compute_transform().scale.recip();
346        let view_from_world_scale_max = view_from_world_scale.abs().max_element();
347        let view_from_world = Mat4::from(world_from_view.inverse());
348        let is_orthographic = camera.clip_from_view().w_axis.w == 1.0;
349
350        let far_z = match config.far_z_mode() {
351            ClusterFarZMode::MaxClusterableObjectRange => {
352                clusters.last_frame_farthest_z.unwrap_or(DEFAULT_FAR_DEPTH)
353            }
354            ClusterFarZMode::Constant(far) => far,
355        };
356        let first_slice_depth = match (is_orthographic, requested_cluster_dimensions.z) {
357            (true, _) => {
358                // NOTE: Based on glam's Mat4::orthographic_rh(), as used to calculate the orthographic projection
359                // matrix, we can calculate the projection's view-space near plane as follows:
360                // component 3,2 = r * near and 2,2 = r where r = 1.0 / (near - far)
361                // There is a caveat here that when calculating the projection matrix, near and far were swapped to give
362                // reversed z, consistent with the perspective projection. So,
363                // 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near)
364                // rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r
365                // = (3,2 - 1.0) / 2,2
366                (camera.clip_from_view().w_axis.z - 1.0) / camera.clip_from_view().z_axis.z
367            }
368            (false, 1) => config.first_slice_depth().max(far_z),
369            _ => config.first_slice_depth(),
370        };
371        let first_slice_depth = first_slice_depth * view_from_world_scale.z;
372
373        // NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems.
374        let far_z = far_z.max(first_slice_depth);
375        let cluster_factors = calculate_cluster_factors(
376            first_slice_depth,
377            far_z,
378            requested_cluster_dimensions.z as f32,
379            is_orthographic,
380        );
381
382        // If the dynamic resizing feature is on, use the last frame's cluster
383        // index count to determine the new number of clusters.
384        // We don’t need to do this if storage buffers are available, because we have plenty of space in that case.
385        if config.dynamic_resizing()
386            && !global_cluster_settings.supports_storage_buffers
387            && let Some(last_frame_cluster_index_count) =
388                clusters.last_frame_total_cluster_index_count
389            && last_frame_cluster_index_count
390                > global_cluster_settings.view_cluster_bindings_max_indices
391        {
392            // scale x and y cluster count to be able to fit all our indices
393
394            // we take the ratio of the actual indices over the index estimate.
395            // this is not guaranteed to be small enough due to overlapped tiles, but
396            // the conservative estimate is more than sufficient to cover the
397            // difference
398            let index_ratio = global_cluster_settings.view_cluster_bindings_max_indices as f32
399                / last_frame_cluster_index_count as f32;
400            let xy_ratio = index_ratio.sqrt();
401
402            requested_cluster_dimensions.x =
403                ((requested_cluster_dimensions.x as f32 * xy_ratio).floor() as u32).max(1);
404            requested_cluster_dimensions.y =
405                ((requested_cluster_dimensions.y as f32 * xy_ratio).floor() as u32).max(1);
406        }
407
408        clusters.update(screen_size, requested_cluster_dimensions);
409        clusters.near = first_slice_depth;
410        clusters.far = far_z;
411
412        // NOTE: Maximum 4096 clusters due to uniform buffer size constraints
413        debug_assert!(
414            clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096
415        );
416
417        let view_from_clip = camera.clip_from_view().inverse();
418
419        let cluster_count =
420            (clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize;
421        clusters.reset_for_new_frame(cluster_count, &global_cluster_settings);
422
423        let (mut total_cluster_index_count, mut farthest_z) = (0, 0.0f32);
424        let view_from_world_row_2 = view_from_world.row(2);
425
426        if matches!(clusters.clusterable_objects, ClusterableObjects::Cpu(..)) {
427            // initialize empty cluster bounding spheres
428            cluster_aabb_spheres.clear();
429            cluster_aabb_spheres.extend(core::iter::repeat_n(None, cluster_count));
430
431            // Calculate the x/y/z cluster frustum planes in view space
432            let mut x_planes = Vec::with_capacity(clusters.dimensions.x as usize + 1);
433            let mut y_planes = Vec::with_capacity(clusters.dimensions.y as usize + 1);
434            let mut z_planes = Vec::with_capacity(clusters.dimensions.z as usize + 1);
435
436            if is_orthographic {
437                let x_slices = clusters.dimensions.x as f32;
438                for x in 0..=clusters.dimensions.x {
439                    let x_proportion = x as f32 / x_slices;
440                    let x_pos = x_proportion * 2.0 - 1.0;
441                    let view_x = clip_to_view(view_from_clip, Vec4::new(x_pos, 0.0, 1.0, 1.0)).x;
442                    let normal = Vec3::X;
443                    let d = view_x * normal.x;
444                    x_planes.push(HalfSpace::new(normal.extend(d)));
445                }
446
447                let y_slices = clusters.dimensions.y as f32;
448                for y in 0..=clusters.dimensions.y {
449                    let y_proportion = 1.0 - y as f32 / y_slices;
450                    let y_pos = y_proportion * 2.0 - 1.0;
451                    let view_y = clip_to_view(view_from_clip, Vec4::new(0.0, y_pos, 1.0, 1.0)).y;
452                    let normal = Vec3::Y;
453                    let d = view_y * normal.y;
454                    y_planes.push(HalfSpace::new(normal.extend(d)));
455                }
456            } else {
457                let x_slices = clusters.dimensions.x as f32;
458                for x in 0..=clusters.dimensions.x {
459                    let x_proportion = x as f32 / x_slices;
460                    let x_pos = x_proportion * 2.0 - 1.0;
461                    let nb = clip_to_view(view_from_clip, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz();
462                    let nt = clip_to_view(view_from_clip, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz();
463                    let normal = nb.cross(nt);
464                    let d = nb.dot(normal);
465                    x_planes.push(HalfSpace::new(normal.extend(d)));
466                }
467
468                let y_slices = clusters.dimensions.y as f32;
469                for y in 0..=clusters.dimensions.y {
470                    let y_proportion = 1.0 - y as f32 / y_slices;
471                    let y_pos = y_proportion * 2.0 - 1.0;
472                    let nl = clip_to_view(view_from_clip, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz();
473                    let nr = clip_to_view(view_from_clip, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz();
474                    let normal = nr.cross(nl);
475                    let d = nr.dot(normal);
476                    y_planes.push(HalfSpace::new(normal.extend(d)));
477                }
478            }
479
480            let z_slices = clusters.dimensions.z;
481            for z in 0..=z_slices {
482                let view_z =
483                    z_slice_to_view_z(first_slice_depth, far_z, z_slices, z, is_orthographic);
484                let normal = -Vec3::Z;
485                let d = view_z * normal.z;
486                z_planes.push(HalfSpace::new(normal.extend(d)));
487            }
488
489            for clusterable_object in &clusterable_objects {
490                // check if the clusterable light layers overlap the view layers
491                if !view_layers.intersects(&clusterable_object.render_layers) {
492                    continue;
493                }
494
495                let clusterable_object_sphere = clusterable_object.sphere();
496
497                // Check if the clusterable object is within the view frustum
498                if !frustum.intersects_sphere(&clusterable_object_sphere, true) {
499                    continue;
500                }
501
502                // note: caching seems to be slower than calling twice for this aabb calculation
503                let (
504                    clusterable_object_aabb_xy_ndc_z_view_min,
505                    clusterable_object_aabb_xy_ndc_z_view_max,
506                ) = cluster_space_clusterable_object_aabb(
507                    view_from_world,
508                    view_from_world_scale,
509                    camera.clip_from_view(),
510                    &clusterable_object_sphere,
511                );
512
513                let min_cluster = ndc_position_to_cluster(
514                    clusters.dimensions,
515                    cluster_factors,
516                    is_orthographic,
517                    clusterable_object_aabb_xy_ndc_z_view_min,
518                    clusterable_object_aabb_xy_ndc_z_view_min.z,
519                );
520                let max_cluster = ndc_position_to_cluster(
521                    clusters.dimensions,
522                    cluster_factors,
523                    is_orthographic,
524                    clusterable_object_aabb_xy_ndc_z_view_max,
525                    clusterable_object_aabb_xy_ndc_z_view_max.z,
526                );
527                let (min_cluster, max_cluster) =
528                    (min_cluster.min(max_cluster), min_cluster.max(max_cluster));
529
530                // If we're doing GPU clustering, then all we have to do is
531                // to push the Z range we computed. Otherwise, proceed to
532                // assign to individual clusters.
533                let clusterable_objects = match clusters.clusterable_objects {
534                    ClusterableObjects::Gpu => {
535                        error!(
536                            "We shouldn't be clustering objects on CPU if we're in GPU clustering \
537                             mode"
538                        );
539                        return;
540                    }
541                    ClusterableObjects::Cpu(ref mut clusterable_objects_cpu) => {
542                        clusterable_objects_cpu
543                    }
544                };
545
546                // What follows is the Iterative Sphere Refinement algorithm from Just Cause 3
547                // Persson et al, Practical Clustered Shading
548                // http://newq.net/dl/pub/s2015_practical.pdf
549                // NOTE: A sphere under perspective projection is no longer a sphere. It gets
550                // stretched and warped, which prevents simpler algorithms from being correct
551                // as they often assume that the widest part of the sphere under projection is the
552                // center point on the axis of interest plus the radius, and that is not true!
553                let view_clusterable_object_sphere = Sphere {
554                    center: Vec3A::from_vec4(
555                        view_from_world * clusterable_object_sphere.center.extend(1.0),
556                    ),
557                    radius: clusterable_object_sphere.radius * view_from_world_scale_max,
558                };
559
560                let this_object_far_z = -view_from_world_row_2
561                    .dot(clusterable_object.transform.translation().extend(1.0))
562                    + clusterable_object.range * view_from_world_scale.z;
563                farthest_z = farthest_z.max(this_object_far_z);
564
565                let spot_light_dir_sin_cos = match clusterable_object.object_type {
566                    ClusterableObjectType::SpotLight { outer_angle, .. } => {
567                        let (angle_sin, angle_cos) = sin_cos(outer_angle);
568                        Some((
569                            (view_from_world * clusterable_object.transform.back().extend(0.0))
570                                .truncate()
571                                .normalize(),
572                            angle_sin,
573                            angle_cos,
574                        ))
575                    }
576                    ClusterableObjectType::Decal => {
577                        // TODO: cull via a frustum
578                        None
579                    }
580                    ClusterableObjectType::PointLight { .. }
581                    | ClusterableObjectType::RectLight
582                    | ClusterableObjectType::ReflectionProbe
583                    | ClusterableObjectType::IrradianceVolume => None,
584                };
585                let clusterable_object_center_clip =
586                    camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0);
587                let object_center_ndc =
588                    clusterable_object_center_clip.xyz() / clusterable_object_center_clip.w;
589                let cluster_coordinates = ndc_position_to_cluster(
590                    clusters.dimensions,
591                    cluster_factors,
592                    is_orthographic,
593                    object_center_ndc,
594                    view_clusterable_object_sphere.center.z,
595                );
596                let z_center = if object_center_ndc.z <= 1.0 {
597                    Some(cluster_coordinates.z)
598                } else {
599                    None
600                };
601                let y_center = if object_center_ndc.y > 1.0 {
602                    None
603                } else if object_center_ndc.y < -1.0 {
604                    Some(clusters.dimensions.y + 1)
605                } else {
606                    Some(cluster_coordinates.y)
607                };
608                for z in min_cluster.z..=max_cluster.z {
609                    let mut z_object = view_clusterable_object_sphere;
610                    if z_center.is_none() || z != z_center.unwrap() {
611                        // The z plane closer to the clusterable object has the
612                        // larger radius circle where the light sphere
613                        // intersects the z plane.
614                        let z_plane = if z_center.is_some() && z < z_center.unwrap() {
615                            z_planes[(z + 1) as usize]
616                        } else {
617                            z_planes[z as usize]
618                        };
619                        // Project the sphere to this z plane and use its radius as the radius of a
620                        // new, refined sphere.
621                        if let Some(projected) = project_to_plane_z(z_object, z_plane) {
622                            z_object = projected;
623                        } else {
624                            continue;
625                        }
626                    }
627                    for y in min_cluster.y..=max_cluster.y {
628                        let mut y_object = z_object;
629                        if y_center.is_none() || y != y_center.unwrap() {
630                            // The y plane closer to the clusterable object has
631                            // the larger radius circle where the light sphere
632                            // intersects the y plane.
633                            let y_plane = if y_center.is_some() && y < y_center.unwrap() {
634                                y_planes[(y + 1) as usize]
635                            } else {
636                                y_planes[y as usize]
637                            };
638                            // Project the refined sphere to this y plane and use its radius as the
639                            // radius of a new, even more refined sphere.
640                            if let Some(projected) =
641                                project_to_plane_y(y_object, y_plane, is_orthographic)
642                            {
643                                y_object = projected;
644                            } else {
645                                continue;
646                            }
647                        }
648                        // Loop from the left to find the first affected cluster
649                        let mut min_x = min_cluster.x;
650                        loop {
651                            if min_x >= max_cluster.x
652                                || -get_distance_x(
653                                    x_planes[(min_x + 1) as usize],
654                                    y_object.center,
655                                    is_orthographic,
656                                ) + y_object.radius
657                                    > 0.0
658                            {
659                                break;
660                            }
661                            min_x += 1;
662                        }
663                        // Loop from the right to find the last affected cluster
664                        let mut max_x = max_cluster.x;
665                        loop {
666                            if max_x <= min_x
667                                || get_distance_x(
668                                    x_planes[max_x as usize],
669                                    y_object.center,
670                                    is_orthographic,
671                                ) + y_object.radius
672                                    > 0.0
673                            {
674                                break;
675                            }
676                            max_x -= 1;
677                        }
678                        let mut cluster_index = ((y * clusters.dimensions.x + min_x)
679                            * clusters.dimensions.z
680                            + z) as usize;
681
682                        match clusterable_object.object_type {
683                            ClusterableObjectType::SpotLight { .. } => {
684                                let (view_light_direction, angle_sin, angle_cos) =
685                                    spot_light_dir_sin_cos.unwrap();
686                                for x in min_x..=max_x {
687                                    // further culling for spot lights
688                                    // get or initialize cluster bounding sphere
689                                    let cluster_aabb_sphere =
690                                        &mut cluster_aabb_spheres[cluster_index];
691                                    let cluster_aabb_sphere =
692                                        if let Some(sphere) = cluster_aabb_sphere {
693                                            &*sphere
694                                        } else {
695                                            let aabb = compute_aabb_for_cluster(
696                                                first_slice_depth,
697                                                far_z,
698                                                clusters.tile_size.as_vec2(),
699                                                screen_size.as_vec2(),
700                                                view_from_clip,
701                                                is_orthographic,
702                                                clusters.dimensions,
703                                                UVec3::new(x, y, z),
704                                            );
705                                            let sphere = Sphere {
706                                                center: aabb.center,
707                                                radius: aabb.half_extents.length(),
708                                            };
709                                            *cluster_aabb_sphere = Some(sphere);
710                                            cluster_aabb_sphere.as_ref().unwrap()
711                                        };
712
713                                    // test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/
714                                    let spot_light_offset = Vec3::from(
715                                        view_clusterable_object_sphere.center
716                                            - cluster_aabb_sphere.center,
717                                    );
718                                    let spot_light_dist_sq = spot_light_offset.length_squared();
719                                    let v1_len = spot_light_offset.dot(view_light_direction);
720
721                                    let distance_closest_point = (angle_cos
722                                        * (spot_light_dist_sq - v1_len * v1_len).sqrt())
723                                        - v1_len * angle_sin;
724                                    let angle_cull =
725                                        distance_closest_point > cluster_aabb_sphere.radius;
726
727                                    let front_cull = v1_len
728                                        > cluster_aabb_sphere.radius
729                                            + clusterable_object.range * view_from_world_scale_max;
730                                    let back_cull = v1_len < -cluster_aabb_sphere.radius;
731
732                                    if !angle_cull && !front_cull && !back_cull {
733                                        // this cluster is affected by the spot light
734                                        clusterable_objects[cluster_index]
735                                            .add_spot_light(clusterable_object.entity);
736                                        total_cluster_index_count += 1;
737                                    }
738                                    cluster_index += clusters.dimensions.z as usize;
739                                }
740                            }
741
742                            ClusterableObjectType::PointLight { .. } => {
743                                for _ in min_x..=max_x {
744                                    // all clusters within range are affected by point lights
745                                    clusterable_objects[cluster_index]
746                                        .add_point_light(clusterable_object.entity);
747                                    cluster_index += clusters.dimensions.z as usize;
748                                }
749                                total_cluster_index_count += (max_x - min_x + 1) as usize;
750                            }
751
752                            ClusterableObjectType::RectLight => {
753                                // This might be worth refining to cull clusters behind the light
754                                for _ in min_x..=max_x {
755                                    clusterable_objects[cluster_index]
756                                        .add_rect_light(clusterable_object.entity);
757                                    cluster_index += clusters.dimensions.z as usize;
758                                }
759                                total_cluster_index_count += (max_x - min_x + 1) as usize;
760                            }
761
762                            ClusterableObjectType::ReflectionProbe => {
763                                // Reflection probes currently affect all
764                                // clusters in their bounding sphere.
765                                //
766                                // TODO: Cull more aggressively based on the
767                                // probe's OBB.
768                                for _ in min_x..=max_x {
769                                    clusterable_objects[cluster_index]
770                                        .add_reflection_probe(clusterable_object.entity);
771                                    cluster_index += clusters.dimensions.z as usize;
772                                }
773                                total_cluster_index_count += (max_x - min_x + 1) as usize;
774                            }
775
776                            ClusterableObjectType::IrradianceVolume => {
777                                // Irradiance volumes currently affect all
778                                // clusters in their bounding sphere.
779                                //
780                                // TODO: Cull more aggressively based on the
781                                // probe's OBB.
782                                for _ in min_x..=max_x {
783                                    clusterable_objects[cluster_index]
784                                        .add_irradiance_volume(clusterable_object.entity);
785                                    cluster_index += clusters.dimensions.z as usize;
786                                }
787                                total_cluster_index_count += (max_x - min_x + 1) as usize;
788                            }
789
790                            ClusterableObjectType::Decal => {
791                                // Decals currently affect all clusters in their
792                                // bounding sphere.
793                                //
794                                // TODO: Cull more aggressively based on the
795                                // decal's OBB.
796                                for _ in min_x..=max_x {
797                                    clusterable_objects[cluster_index]
798                                        .add_decal(clusterable_object.entity);
799                                    cluster_index += clusters.dimensions.z as usize;
800                                }
801                                total_cluster_index_count += (max_x - min_x + 1) as usize;
802                            }
803                        }
804                    }
805                }
806            }
807        }
808
809        // Save statistics for this frame so that the `dynamic_resizing` and
810        // `ClusterFarZMode::MaxClusterableObjectRange` features can use them as
811        // heuristics on the next frame.
812        clusters.last_frame_total_cluster_index_count = Some(total_cluster_index_count);
813        clusters.last_frame_farthest_z = Some(farthest_z);
814    }
815}
816
817// TODO: this probably shouldn't return a Vec2 and should probably be named better.
818#[expect(missing_docs, reason = "TODO")]
819pub fn calculate_cluster_factors(
820    near: f32,
821    far: f32,
822    z_slices: f32,
823    is_orthographic: bool,
824) -> Vec2 {
825    if is_orthographic {
826        Vec2::new(-near, z_slices / (-far - -near))
827    } else {
828        let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near);
829        Vec2::new(
830            z_slices_of_ln_zfar_over_znear,
831            ops::ln(near) * z_slices_of_ln_zfar_over_znear,
832        )
833    }
834}
835
836fn compute_aabb_for_cluster(
837    z_near: f32,
838    z_far: f32,
839    tile_size: Vec2,
840    screen_size: Vec2,
841    view_from_clip: Mat4,
842    is_orthographic: bool,
843    cluster_dimensions: UVec3,
844    ijk: UVec3,
845) -> Aabb {
846    let ijk = ijk.as_vec3();
847
848    // Calculate the minimum and maximum points in screen space
849    let p_min = ijk.xy() * tile_size;
850    let p_max = p_min + tile_size;
851
852    let (cluster_min, cluster_max) = if is_orthographic {
853        // Use linear depth slicing for orthographic
854
855        // Convert to view space at the cluster near and far planes
856        // NOTE: 1.0 is the near plane due to using reverse z projections
857        let mut p_min = screen_to_view(screen_size, view_from_clip, p_min, 0.0).xyz();
858        let mut p_max = screen_to_view(screen_size, view_from_clip, p_max, 0.0).xyz();
859
860        // calculate cluster depth using z_near and z_far
861        p_min.z = -z_near + (z_near - z_far) * ijk.z / cluster_dimensions.z as f32;
862        p_max.z = -z_near + (z_near - z_far) * (ijk.z + 1.0) / cluster_dimensions.z as f32;
863
864        (p_min.min(p_max), p_min.max(p_max))
865    } else {
866        // Convert to view space at the near plane
867        // NOTE: 1.0 is the near plane due to using reverse z projections
868        let p_min = screen_to_view(screen_size, view_from_clip, p_min, 1.0);
869        let p_max = screen_to_view(screen_size, view_from_clip, p_max, 1.0);
870
871        let z_far_over_z_near = -z_far / -z_near;
872        let cluster_near = if ijk.z == 0.0 {
873            0.0
874        } else {
875            -z_near
876                * ops::powf(
877                    z_far_over_z_near,
878                    (ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32,
879                )
880        };
881        // NOTE: This could be simplified to:
882        // cluster_far = cluster_near * z_far_over_z_near;
883        let cluster_far = if cluster_dimensions.z == 1 {
884            -z_far
885        } else {
886            -z_near * ops::powf(z_far_over_z_near, ijk.z / (cluster_dimensions.z - 1) as f32)
887        };
888
889        // Calculate the four intersection points of the min and max points with the cluster near and far planes
890        let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
891        let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
892        let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
893        let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
894
895        (
896            p_min_near.min(p_min_far).min(p_max_near.min(p_max_far)),
897            p_min_near.max(p_min_far).max(p_max_near.max(p_max_far)),
898        )
899    };
900
901    Aabb::from_min_max(cluster_min, cluster_max)
902}
903
904// NOTE: Keep in sync as the inverse of view_z_to_z_slice above
905fn z_slice_to_view_z(
906    near: f32,
907    far: f32,
908    z_slices: u32,
909    z_slice: u32,
910    is_orthographic: bool,
911) -> f32 {
912    if is_orthographic {
913        return -near - (far - near) * z_slice as f32 / z_slices as f32;
914    }
915
916    // Perspective
917    if z_slice == 0 {
918        0.0
919    } else {
920        -near * ops::powf(far / near, (z_slice - 1) as f32 / (z_slices - 1) as f32)
921    }
922}
923
924fn ndc_position_to_cluster(
925    cluster_dimensions: UVec3,
926    cluster_factors: Vec2,
927    is_orthographic: bool,
928    ndc_p: Vec3,
929    view_z: f32,
930) -> UVec3 {
931    let cluster_dimensions_f32 = cluster_dimensions.as_vec3();
932    let frag_coord = (ndc_p.xy() * VEC2_HALF_NEGATIVE_Y + VEC2_HALF).clamp(Vec2::ZERO, Vec2::ONE);
933    let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();
934    let z_slice = view_z_to_z_slice(
935        cluster_factors,
936        cluster_dimensions.z,
937        view_z,
938        is_orthographic,
939    );
940    xy.as_uvec2()
941        .extend(z_slice)
942        .clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
943}
944
945/// Calculate bounds for the clusterable object using a view space aabb.
946///
947/// Returns a `(Vec3, Vec3)` containing minimum and maximum with
948///     `X` and `Y` in normalized device coordinates with range `[-1, 1]`
949///     `Z` in view space, with range `[-inf, -f32::MIN_POSITIVE]`
950fn cluster_space_clusterable_object_aabb(
951    view_from_world: Mat4,
952    view_from_world_scale: Vec3,
953    clip_from_view: Mat4,
954    clusterable_object_sphere: &Sphere,
955) -> (Vec3, Vec3) {
956    let clusterable_object_aabb_view = Aabb {
957        center: Vec3A::from_vec4(view_from_world * clusterable_object_sphere.center.extend(1.0)),
958        half_extents: Vec3A::from(clusterable_object_sphere.radius * view_from_world_scale.abs()),
959    };
960    let (mut clusterable_object_aabb_view_min, mut clusterable_object_aabb_view_max) = (
961        clusterable_object_aabb_view.min(),
962        clusterable_object_aabb_view.max(),
963    );
964
965    // Constrain view z to be negative - i.e. in front of the camera
966    // When view z is >= 0.0 and we're using a perspective projection, bad things happen.
967    // At view z == 0.0, ndc x,y are mathematically undefined. At view z > 0.0, i.e. behind the camera,
968    // the perspective projection flips the directions of the axes. This breaks assumptions about
969    // use of min/max operations as something that was to the left in view space is now returning a
970    // coordinate that for view z in front of the camera would be on the right, but at view z behind the
971    // camera is on the left. So, we just constrain view z to be < 0.0 and necessarily in front of the camera.
972    clusterable_object_aabb_view_min.z = clusterable_object_aabb_view_min.z.min(-f32::MIN_POSITIVE);
973    clusterable_object_aabb_view_max.z = clusterable_object_aabb_view_max.z.min(-f32::MIN_POSITIVE);
974
975    // Is there a cheaper way to do this? The problem is that because of perspective
976    // the point at max z but min xy may be less xy in screenspace, and similar. As
977    // such, projecting the min and max xy at both the closer and further z and taking
978    // the min and max of those projected points addresses this.
979    let (
980        clusterable_object_aabb_view_xymin_near,
981        clusterable_object_aabb_view_xymin_far,
982        clusterable_object_aabb_view_xymax_near,
983        clusterable_object_aabb_view_xymax_far,
984    ) = (
985        clusterable_object_aabb_view_min,
986        clusterable_object_aabb_view_min
987            .xy()
988            .extend(clusterable_object_aabb_view_max.z),
989        clusterable_object_aabb_view_max
990            .xy()
991            .extend(clusterable_object_aabb_view_min.z),
992        clusterable_object_aabb_view_max,
993    );
994    let (
995        clusterable_object_aabb_clip_xymin_near,
996        clusterable_object_aabb_clip_xymin_far,
997        clusterable_object_aabb_clip_xymax_near,
998        clusterable_object_aabb_clip_xymax_far,
999    ) = (
1000        clip_from_view * clusterable_object_aabb_view_xymin_near.extend(1.0),
1001        clip_from_view * clusterable_object_aabb_view_xymin_far.extend(1.0),
1002        clip_from_view * clusterable_object_aabb_view_xymax_near.extend(1.0),
1003        clip_from_view * clusterable_object_aabb_view_xymax_far.extend(1.0),
1004    );
1005    let (
1006        clusterable_object_aabb_ndc_xymin_near,
1007        clusterable_object_aabb_ndc_xymin_far,
1008        clusterable_object_aabb_ndc_xymax_near,
1009        clusterable_object_aabb_ndc_xymax_far,
1010    ) = (
1011        clusterable_object_aabb_clip_xymin_near.xyz() / clusterable_object_aabb_clip_xymin_near.w,
1012        clusterable_object_aabb_clip_xymin_far.xyz() / clusterable_object_aabb_clip_xymin_far.w,
1013        clusterable_object_aabb_clip_xymax_near.xyz() / clusterable_object_aabb_clip_xymax_near.w,
1014        clusterable_object_aabb_clip_xymax_far.xyz() / clusterable_object_aabb_clip_xymax_far.w,
1015    );
1016    let (clusterable_object_aabb_ndc_min, clusterable_object_aabb_ndc_max) = (
1017        clusterable_object_aabb_ndc_xymin_near
1018            .min(clusterable_object_aabb_ndc_xymin_far)
1019            .min(clusterable_object_aabb_ndc_xymax_near)
1020            .min(clusterable_object_aabb_ndc_xymax_far),
1021        clusterable_object_aabb_ndc_xymin_near
1022            .max(clusterable_object_aabb_ndc_xymin_far)
1023            .max(clusterable_object_aabb_ndc_xymax_near)
1024            .max(clusterable_object_aabb_ndc_xymax_far),
1025    );
1026
1027    // clamp to ndc coords without depth
1028    let (aabb_min_ndc, aabb_max_ndc) = (
1029        clusterable_object_aabb_ndc_min.xy().clamp(NDC_MIN, NDC_MAX),
1030        clusterable_object_aabb_ndc_max.xy().clamp(NDC_MIN, NDC_MAX),
1031    );
1032
1033    // pack unadjusted z depth into the vecs
1034    (
1035        aabb_min_ndc.extend(clusterable_object_aabb_view_min.z),
1036        aabb_max_ndc.extend(clusterable_object_aabb_view_max.z),
1037    )
1038}
1039
1040// Calculate the intersection of a ray from the eye through the view space position to a z plane
1041fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {
1042    let v = p - origin;
1043    let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v);
1044    origin + t * v
1045}
1046
1047// NOTE: Keep in sync with bevy_pbr/src/render/pbr.wgsl
1048fn view_z_to_z_slice(
1049    cluster_factors: Vec2,
1050    z_slices: u32,
1051    view_z: f32,
1052    is_orthographic: bool,
1053) -> u32 {
1054    let z_slice = if is_orthographic {
1055        // NOTE: view_z is correct in the orthographic case
1056        ((view_z - cluster_factors.x) * cluster_factors.y).floor() as u32
1057    } else {
1058        // NOTE: had to use -view_z to make it positive else log(negative) is nan
1059        (ops::ln(-view_z) * cluster_factors.x - cluster_factors.y + 1.0) as u32
1060    };
1061    // NOTE: We use min as we may limit the far z plane used for clustering to be closer than
1062    // the furthest thing being drawn. This means that we need to limit to the maximum cluster.
1063    z_slice.min(z_slices - 1)
1064}
1065
1066fn clip_to_view(view_from_clip: Mat4, clip: Vec4) -> Vec4 {
1067    let view = view_from_clip * clip;
1068    view / view.w
1069}
1070
1071fn screen_to_view(screen_size: Vec2, view_from_clip: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 {
1072    let tex_coord = screen / screen_size;
1073    let clip = Vec4::new(
1074        tex_coord.x * 2.0 - 1.0,
1075        (1.0 - tex_coord.y) * 2.0 - 1.0,
1076        ndc_z,
1077        1.0,
1078    );
1079    clip_to_view(view_from_clip, clip)
1080}
1081
1082// NOTE: This exploits the fact that a x-plane normal has only x and z components
1083fn get_distance_x(plane: HalfSpace, point: Vec3A, is_orthographic: bool) -> f32 {
1084    if is_orthographic {
1085        point.x - plane.d()
1086    } else {
1087        // Distance from a point to a plane:
1088        // signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length()
1089        // NOTE: For a x-plane, ny and d are 0 and we have a unit normal
1090        //                          = nx * px + nz * pz
1091        plane.normal_d().xz().dot(point.xz())
1092    }
1093}
1094
1095// NOTE: This exploits the fact that a z-plane normal has only a z component
1096fn project_to_plane_z(z_object: Sphere, z_plane: HalfSpace) -> Option<Sphere> {
1097    // p = sphere center
1098    // n = plane normal
1099    // d = n.p if p is in the plane
1100    // NOTE: For a z-plane, nx and ny are both 0
1101    // d = px * nx + py * ny + pz * nz
1102    //   = pz * nz
1103    // => pz = d / nz
1104    let z = z_plane.d() / z_plane.normal_d().z;
1105    let distance_to_plane = z - z_object.center.z;
1106    if distance_to_plane.abs() > z_object.radius {
1107        return None;
1108    }
1109    Some(Sphere {
1110        center: Vec3A::from(z_object.center.xy().extend(z)),
1111        // hypotenuse length = radius
1112        // pythagoras = (distance to plane)^2 + b^2 = radius^2
1113        radius: (z_object.radius * z_object.radius - distance_to_plane * distance_to_plane).sqrt(),
1114    })
1115}
1116
1117// NOTE: This exploits the fact that a y-plane normal has only y and z components
1118fn project_to_plane_y(
1119    y_object: Sphere,
1120    y_plane: HalfSpace,
1121    is_orthographic: bool,
1122) -> Option<Sphere> {
1123    let distance_to_plane = if is_orthographic {
1124        y_plane.d() - y_object.center.y
1125    } else {
1126        -y_object.center.yz().dot(y_plane.normal_d().yz())
1127    };
1128
1129    if distance_to_plane.abs() > y_object.radius {
1130        return None;
1131    }
1132    Some(Sphere {
1133        center: y_object.center + distance_to_plane * y_plane.normal(),
1134        radius: (y_object.radius * y_object.radius - distance_to_plane * distance_to_plane).sqrt(),
1135    })
1136}