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