bevy_light/cluster/
assign.rs

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