bevy_light/
lib.rs

1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
2
3use bevy_app::{App, Plugin, PostUpdate};
4use bevy_camera::{
5    primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
6    visibility::{
7        CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling,
8        RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, VisibleEntityRanges,
9        VisibleMeshEntities,
10    },
11    CameraUpdateSystems,
12};
13use bevy_ecs::{entity::EntityHashSet, prelude::*};
14use bevy_math::Vec3A;
15use bevy_mesh::Mesh3d;
16use bevy_reflect::prelude::*;
17use bevy_transform::{components::GlobalTransform, TransformSystems};
18use bevy_utils::Parallel;
19use core::ops::DerefMut;
20
21pub mod cluster;
22pub use cluster::ClusteredDecal;
23use cluster::{
24    add_clusters, assign::assign_objects_to_clusters, GlobalVisibleClusterableObjects,
25    VisibleClusterableObjects,
26};
27mod ambient_light;
28pub use ambient_light::{AmbientLight, GlobalAmbientLight};
29use bevy_camera::visibility::SetViewVisibility;
30
31mod probe;
32pub use probe::{
33    AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight,
34    IrradianceVolume, LightProbe,
35};
36mod volumetric;
37pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};
38pub mod cascade;
39use cascade::{build_directional_light_cascades, clear_directional_light_cascades};
40pub use cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades};
41mod point_light;
42pub use point_light::{
43    update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture,
44};
45mod spot_light;
46pub use spot_light::{
47    orthonormalize, spot_light_clip_from_view, spot_light_world_from_view,
48    update_spot_light_frusta, SpotLight, SpotLightTexture,
49};
50mod directional_light;
51pub use directional_light::{
52    update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap,
53    DirectionalLightTexture, SunDisk,
54};
55
56/// The light prelude.
57///
58/// This includes the most common types in this crate, re-exported for your convenience.
59pub mod prelude {
60    #[doc(hidden)]
61    pub use crate::{
62        light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight,
63        GeneratedEnvironmentMapLight, GlobalAmbientLight, LightProbe, PointLight, SpotLight,
64    };
65}
66
67use crate::directional_light::validate_shadow_map_size;
68
69/// Constants for operating with the light units: lumens, and lux.
70pub mod light_consts {
71    /// Approximations for converting the wattage of lamps to lumens.
72    ///
73    /// The **lumen** (symbol: **lm**) is the unit of [luminous flux], a measure
74    /// of the total quantity of [visible light] emitted by a source per unit of
75    /// time, in the [International System of Units] (SI).
76    ///
77    /// For more information, see [wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit))
78    ///
79    /// [luminous flux]: https://en.wikipedia.org/wiki/Luminous_flux
80    /// [visible light]: https://en.wikipedia.org/wiki/Visible_light
81    /// [International System of Units]: https://en.wikipedia.org/wiki/International_System_of_Units
82    pub mod lumens {
83        pub const LUMENS_PER_LED_WATTS: f32 = 90.0;
84        pub const LUMENS_PER_INCANDESCENT_WATTS: f32 = 13.8;
85        pub const LUMENS_PER_HALOGEN_WATTS: f32 = 19.8;
86        /// 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's
87        /// default "very overcast day" exposure level. For "indoor lighting" with a lower exposure,
88        /// this would be way too bright.
89        pub const VERY_LARGE_CINEMA_LIGHT: f32 = 1_000_000.0;
90    }
91
92    /// Predefined for lux values in several locations.
93    ///
94    /// The **lux** (symbol: **lx**) is the unit of [illuminance], or [luminous flux] per unit area,
95    /// in the [International System of Units] (SI). It is equal to one lumen per square meter.
96    ///
97    /// For more information, see [wikipedia](https://en.wikipedia.org/wiki/Lux)
98    ///
99    /// [illuminance]: https://en.wikipedia.org/wiki/Illuminance
100    /// [luminous flux]: https://en.wikipedia.org/wiki/Luminous_flux
101    /// [International System of Units]: https://en.wikipedia.org/wiki/International_System_of_Units
102    pub mod lux {
103        /// The amount of light (lux) in a moonless, overcast night sky. (starlight)
104        pub const MOONLESS_NIGHT: f32 = 0.0001;
105        /// The amount of light (lux) during a full moon on a clear night.
106        pub const FULL_MOON_NIGHT: f32 = 0.05;
107        /// The amount of light (lux) during the dark limit of civil twilight under a clear sky.
108        pub const CIVIL_TWILIGHT: f32 = 3.4;
109        /// The amount of light (lux) in family living room lights.
110        pub const LIVING_ROOM: f32 = 50.;
111        /// The amount of light (lux) in an office building's hallway/toilet lighting.
112        pub const HALLWAY: f32 = 80.;
113        /// The amount of light (lux) in very dark overcast day
114        pub const DARK_OVERCAST_DAY: f32 = 100.;
115        /// The amount of light (lux) in an office.
116        pub const OFFICE: f32 = 320.;
117        /// The amount of light (lux) during sunrise or sunset on a clear day.
118        pub const CLEAR_SUNRISE: f32 = 400.;
119        /// The amount of light (lux) on an overcast day; typical TV studio lighting
120        pub const OVERCAST_DAY: f32 = 1000.;
121        /// The amount of light (lux) from ambient daylight (not direct sunlight).
122        pub const AMBIENT_DAYLIGHT: f32 = 10_000.;
123        /// The amount of light (lux) in full daylight (not direct sun).
124        pub const FULL_DAYLIGHT: f32 = 20_000.;
125        /// The amount of light (lux) in direct sunlight.
126        pub const DIRECT_SUNLIGHT: f32 = 100_000.;
127        /// The amount of light (lux) of raw sunlight, not filtered by the atmosphere.
128        pub const RAW_SUNLIGHT: f32 = 130_000.;
129    }
130}
131
132#[derive(Default)]
133pub struct LightPlugin;
134
135impl Plugin for LightPlugin {
136    fn build(&self, app: &mut App) {
137        app.init_resource::<GlobalVisibleClusterableObjects>()
138            .init_resource::<GlobalAmbientLight>()
139            .init_resource::<DirectionalLightShadowMap>()
140            .init_resource::<PointLightShadowMap>()
141            .configure_sets(
142                PostUpdate,
143                SimulationLightSystems::UpdateDirectionalLightCascades
144                    .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades),
145            )
146            .configure_sets(
147                PostUpdate,
148                SimulationLightSystems::CheckLightVisibility
149                    .ambiguous_with(SimulationLightSystems::CheckLightVisibility),
150            )
151            .add_systems(
152                PostUpdate,
153                (
154                    validate_shadow_map_size.before(build_directional_light_cascades),
155                    add_clusters
156                        .in_set(SimulationLightSystems::AddClusters)
157                        .after(CameraUpdateSystems),
158                    assign_objects_to_clusters
159                        .in_set(SimulationLightSystems::AssignLightsToClusters)
160                        .after(TransformSystems::Propagate)
161                        .after(VisibilitySystems::CheckVisibility)
162                        .after(CameraUpdateSystems),
163                    clear_directional_light_cascades
164                        .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
165                        .after(TransformSystems::Propagate)
166                        .after(CameraUpdateSystems),
167                    update_directional_light_frusta
168                        .in_set(SimulationLightSystems::UpdateLightFrusta)
169                        // This must run after CheckVisibility because it relies on `ViewVisibility`
170                        .after(VisibilitySystems::CheckVisibility)
171                        .after(TransformSystems::Propagate)
172                        .after(SimulationLightSystems::UpdateDirectionalLightCascades)
173                        // We assume that no entity will be both a directional light and a spot light,
174                        // so these systems will run independently of one another.
175                        // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
176                        .ambiguous_with(update_spot_light_frusta),
177                    update_point_light_frusta
178                        .in_set(SimulationLightSystems::UpdateLightFrusta)
179                        .after(TransformSystems::Propagate)
180                        .after(SimulationLightSystems::AssignLightsToClusters),
181                    update_spot_light_frusta
182                        .in_set(SimulationLightSystems::UpdateLightFrusta)
183                        .after(TransformSystems::Propagate)
184                        .after(SimulationLightSystems::AssignLightsToClusters),
185                    (
186                        check_dir_light_mesh_visibility,
187                        check_point_light_mesh_visibility,
188                    )
189                        .in_set(SimulationLightSystems::CheckLightVisibility)
190                        .after(VisibilitySystems::CalculateBounds)
191                        .after(TransformSystems::Propagate)
192                        .after(SimulationLightSystems::UpdateLightFrusta)
193                        // Lights can "see" entities and mark them as visible. This is done to
194                        // correctly render shadows for entities that are not in view of a camera,
195                        // but must be renderable to cast shadows. Because of this, we need to check
196                        // entity visibility and mark as visible before they can be hidden.
197                        .after(VisibilitySystems::CheckVisibility)
198                        .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
199                    build_directional_light_cascades
200                        .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
201                        .after(clear_directional_light_cascades),
202                ),
203            );
204    }
205}
206
207/// A convenient alias for `Or<(With<PointLight>, With<SpotLight>,
208/// With<DirectionalLight>)>`, for use with [`bevy_camera::visibility::VisibleEntities`].
209pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
210
211/// Add this component to make a [`Mesh3d`] not cast shadows.
212#[derive(Debug, Component, Reflect, Default, Clone, PartialEq)]
213#[reflect(Component, Default, Debug, Clone, PartialEq)]
214pub struct NotShadowCaster;
215/// Add this component to make a [`Mesh3d`] not receive shadows.
216///
217/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will
218/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled,
219/// even when [`TransmittedShadowReceiver`] is being used.
220#[derive(Debug, Component, Reflect, Default)]
221#[reflect(Component, Default, Debug)]
222pub struct NotShadowReceiver;
223/// Add this component to make a [`Mesh3d`] using a PBR material with `StandardMaterial::diffuse_transmission > 0.0`
224/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”)
225///
226/// Not enabled by default, as it requires carefully setting up `StandardMaterial::thickness`
227/// (and potentially even baking a thickness texture!) to match the geometry of the mesh, in order to avoid self-shadow artifacts.
228///
229/// **Note:** Using [`NotShadowReceiver`] overrides this component.
230#[derive(Debug, Component, Reflect, Default)]
231#[reflect(Component, Default, Debug)]
232pub struct TransmittedShadowReceiver;
233
234/// Add this component to a [`Camera3d`](bevy_camera::Camera3d)
235/// to control how to anti-alias shadow edges.
236///
237/// The different modes use different approaches to
238/// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing).
239#[derive(Debug, Component, Reflect, Clone, Copy, PartialEq, Eq, Default)]
240#[reflect(Component, Default, Debug, PartialEq, Clone)]
241pub enum ShadowFilteringMethod {
242    /// Hardware 2x2.
243    ///
244    /// Fast but poor quality.
245    Hardware2x2,
246    /// Approximates a fixed Gaussian blur, good when TAA isn't in use.
247    ///
248    /// Good quality, good performance.
249    ///
250    /// For directional and spot lights, this uses a [method by Ignacio Castaño
251    /// for *The Witness*] using 9 samples and smart filtering to achieve the same
252    /// as a regular 5x5 filter kernel.
253    ///
254    /// [method by Ignacio Castaño for *The Witness*]: https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/
255    #[default]
256    Gaussian,
257    /// A randomized filter that varies over time, good when TAA is in use.
258    ///
259    /// Good quality when used with `TemporalAntiAliasing`
260    /// and good performance.
261    ///
262    /// For directional and spot lights, this uses a [method by Jorge Jimenez for
263    /// *Call of Duty: Advanced Warfare*] using 8 samples in spiral pattern,
264    /// randomly-rotated by interleaved gradient noise with spatial variation.
265    ///
266    /// [method by Jorge Jimenez for *Call of Duty: Advanced Warfare*]: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
267    Temporal,
268}
269
270/// System sets used to run light-related systems.
271#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
272pub enum SimulationLightSystems {
273    AddClusters,
274    AssignLightsToClusters,
275    /// System order ambiguities between systems in this set are ignored:
276    /// each [`build_directional_light_cascades`] system is independent of the others,
277    /// and should operate on distinct sets of entities.
278    UpdateDirectionalLightCascades,
279    UpdateLightFrusta,
280    /// System order ambiguities between systems in this set are ignored:
281    /// the order of systems within this set is irrelevant, as the various visibility-checking systems
282    /// assumes that their operations are irreversible during the frame.
283    CheckLightVisibility,
284}
285
286fn shrink_entities(visible_entities: &mut Vec<Entity>) {
287    // Check that visible entities capacity() is no more than two times greater than len()
288    let capacity = visible_entities.capacity();
289    let reserved = capacity
290        .checked_div(visible_entities.len())
291        .map_or(0, |reserve| {
292            if reserve > 2 {
293                capacity / (reserve / 2)
294            } else {
295                capacity
296            }
297        });
298
299    visible_entities.shrink_to(reserved);
300}
301
302pub fn check_dir_light_mesh_visibility(
303    mut commands: Commands,
304    mut directional_lights: Query<
305        (
306            &DirectionalLight,
307            &CascadesFrusta,
308            &mut CascadesVisibleEntities,
309            Option<&RenderLayers>,
310            &ViewVisibility,
311        ),
312        Without<SpotLight>,
313    >,
314    visible_entity_query: Query<
315        (
316            Entity,
317            &InheritedVisibility,
318            Option<&RenderLayers>,
319            Option<&Aabb>,
320            Option<&GlobalTransform>,
321            Has<VisibilityRange>,
322            Has<NoFrustumCulling>,
323        ),
324        (
325            Without<NotShadowCaster>,
326            Without<DirectionalLight>,
327            With<Mesh3d>,
328        ),
329    >,
330    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
331    mut defer_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
332    mut view_visible_entities_queue: Local<Parallel<Vec<Vec<Entity>>>>,
333) {
334    let visible_entity_ranges = visible_entity_ranges.as_deref();
335
336    for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in
337        &mut directional_lights
338    {
339        let mut views_to_remove = Vec::new();
340        for (view, cascade_view_entities) in &mut visible_entities.entities {
341            match frusta.frusta.get(view) {
342                Some(view_frusta) => {
343                    cascade_view_entities.resize(view_frusta.len(), Default::default());
344                    cascade_view_entities.iter_mut().for_each(|x| x.clear());
345                }
346                None => views_to_remove.push(*view),
347            };
348        }
349        for (view, frusta) in &frusta.frusta {
350            visible_entities
351                .entities
352                .entry(*view)
353                .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]);
354        }
355
356        for v in views_to_remove {
357            visible_entities.entities.remove(&v);
358        }
359
360        // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
361        if !directional_light.shadows_enabled || !light_view_visibility.get() {
362            continue;
363        }
364
365        let view_mask = maybe_view_mask.unwrap_or_default();
366
367        for (view, view_frusta) in &frusta.frusta {
368            visible_entity_query.par_iter().for_each_init(
369                || {
370                    let mut entities = view_visible_entities_queue.borrow_local_mut();
371                    entities.resize(view_frusta.len(), Vec::default());
372                    (defer_visible_entities_queue.borrow_local_mut(), entities)
373                },
374                |(defer_visible_entities_local_queue, view_visible_entities_local_queue),
375                 (
376                    entity,
377                    inherited_visibility,
378                    maybe_entity_mask,
379                    maybe_aabb,
380                    maybe_transform,
381                    has_visibility_range,
382                    has_no_frustum_culling,
383                )| {
384                    if !inherited_visibility.get() {
385                        return;
386                    }
387
388                    let entity_mask = maybe_entity_mask.unwrap_or_default();
389                    if !view_mask.intersects(entity_mask) {
390                        return;
391                    }
392
393                    // Check visibility ranges.
394                    if has_visibility_range
395                        && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
396                            !visible_entity_ranges.entity_is_in_range_of_view(entity, *view)
397                        })
398                    {
399                        return;
400                    }
401
402                    if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
403                        let mut visible = false;
404                        for (frustum, frustum_visible_entities) in view_frusta
405                            .iter()
406                            .zip(view_visible_entities_local_queue.iter_mut())
407                        {
408                            // Disable near-plane culling, as a shadow caster could lie before the near plane.
409                            if !has_no_frustum_culling
410                                && !frustum.intersects_obb(aabb, &transform.affine(), false, true)
411                            {
412                                continue;
413                            }
414                            visible = true;
415
416                            frustum_visible_entities.push(entity);
417                        }
418                        if visible {
419                            defer_visible_entities_local_queue.push(entity);
420                        }
421                    } else {
422                        defer_visible_entities_local_queue.push(entity);
423                        for frustum_visible_entities in view_visible_entities_local_queue.iter_mut()
424                        {
425                            frustum_visible_entities.push(entity);
426                        }
427                    }
428                },
429            );
430            // collect entities from parallel queue
431            for entities in view_visible_entities_queue.iter_mut() {
432                visible_entities
433                    .entities
434                    .get_mut(view)
435                    .unwrap()
436                    .iter_mut()
437                    .zip(entities.iter_mut())
438                    .for_each(|(dst, source)| {
439                        dst.append(source);
440                    });
441            }
442        }
443
444        for (_, cascade_view_entities) in &mut visible_entities.entities {
445            cascade_view_entities
446                .iter_mut()
447                .map(DerefMut::deref_mut)
448                .for_each(shrink_entities);
449        }
450    }
451
452    // Defer marking view visibility so this system can run in parallel with check_point_light_mesh_visibility
453    // TODO: use resource to avoid unnecessary memory alloc
454    let mut defer_queue = core::mem::take(defer_visible_entities_queue.deref_mut());
455    commands.queue(move |world: &mut World| {
456        let mut query = world.query::<&mut ViewVisibility>();
457        for entities in defer_queue.iter_mut() {
458            let mut iter = query.iter_many_mut(world, entities.iter());
459            while let Some(mut view_visibility) = iter.fetch_next() {
460                view_visibility.set_visible();
461            }
462        }
463    });
464}
465
466pub fn check_point_light_mesh_visibility(
467    visible_point_lights: Query<&VisibleClusterableObjects>,
468    mut point_lights: Query<(
469        &PointLight,
470        &GlobalTransform,
471        &CubemapFrusta,
472        &mut CubemapVisibleEntities,
473        Option<&RenderLayers>,
474    )>,
475    mut spot_lights: Query<(
476        &SpotLight,
477        &GlobalTransform,
478        &Frustum,
479        &mut VisibleMeshEntities,
480        Option<&RenderLayers>,
481    )>,
482    mut visible_entity_query: Query<
483        (
484            Entity,
485            &InheritedVisibility,
486            &mut ViewVisibility,
487            Option<&RenderLayers>,
488            Option<&Aabb>,
489            Option<&GlobalTransform>,
490            Has<VisibilityRange>,
491            Has<NoFrustumCulling>,
492        ),
493        (
494            Without<NotShadowCaster>,
495            Without<DirectionalLight>,
496            With<Mesh3d>,
497        ),
498    >,
499    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
500    mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
501    mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
502    mut checked_lights: Local<EntityHashSet>,
503) {
504    checked_lights.clear();
505
506    let visible_entity_ranges = visible_entity_ranges.as_deref();
507    for visible_lights in &visible_point_lights {
508        for light_entity in visible_lights.entities.iter().copied() {
509            if !checked_lights.insert(light_entity) {
510                continue;
511            }
512
513            // Point lights
514            if let Ok((
515                point_light,
516                transform,
517                cubemap_frusta,
518                mut cubemap_visible_entities,
519                maybe_view_mask,
520            )) = point_lights.get_mut(light_entity)
521            {
522                for visible_entities in cubemap_visible_entities.iter_mut() {
523                    visible_entities.entities.clear();
524                }
525
526                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
527                if !point_light.shadows_enabled {
528                    continue;
529                }
530
531                let view_mask = maybe_view_mask.unwrap_or_default();
532                let light_sphere = Sphere {
533                    center: Vec3A::from(transform.translation()),
534                    radius: point_light.range,
535                };
536
537                visible_entity_query.par_iter_mut().for_each_init(
538                    || cubemap_visible_entities_queue.borrow_local_mut(),
539                    |cubemap_visible_entities_local_queue,
540                     (
541                        entity,
542                        inherited_visibility,
543                        mut view_visibility,
544                        maybe_entity_mask,
545                        maybe_aabb,
546                        maybe_transform,
547                        has_visibility_range,
548                        has_no_frustum_culling,
549                    )| {
550                        if !inherited_visibility.get() {
551                            return;
552                        }
553                        let entity_mask = maybe_entity_mask.unwrap_or_default();
554                        if !view_mask.intersects(entity_mask) {
555                            return;
556                        }
557                        if has_visibility_range
558                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
559                                !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
560                            })
561                        {
562                            return;
563                        }
564
565                        // If we have an aabb and transform, do frustum culling
566                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
567                            let model_to_world = transform.affine();
568                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
569                            if !has_no_frustum_culling
570                                && !light_sphere.intersects_obb(aabb, &model_to_world)
571                            {
572                                return;
573                            }
574
575                            for (frustum, visible_entities) in cubemap_frusta
576                                .iter()
577                                .zip(cubemap_visible_entities_local_queue.iter_mut())
578                            {
579                                if has_no_frustum_culling
580                                    || frustum.intersects_obb(aabb, &model_to_world, true, true)
581                                {
582                                    view_visibility.set_visible();
583                                    visible_entities.push(entity);
584                                }
585                            }
586                        } else {
587                            view_visibility.set_visible();
588                            for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
589                            {
590                                visible_entities.push(entity);
591                            }
592                        }
593                    },
594                );
595
596                for entities in cubemap_visible_entities_queue.iter_mut() {
597                    for (dst, source) in
598                        cubemap_visible_entities.iter_mut().zip(entities.iter_mut())
599                    {
600                        dst.entities.append(source);
601                    }
602                }
603
604                for visible_entities in cubemap_visible_entities.iter_mut() {
605                    shrink_entities(visible_entities);
606                }
607            }
608
609            // Spot lights
610            if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
611                spot_lights.get_mut(light_entity)
612            {
613                visible_entities.clear();
614
615                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
616                if !point_light.shadows_enabled {
617                    continue;
618                }
619
620                let view_mask = maybe_view_mask.unwrap_or_default();
621                let light_sphere = Sphere {
622                    center: Vec3A::from(transform.translation()),
623                    radius: point_light.range,
624                };
625
626                visible_entity_query.par_iter_mut().for_each_init(
627                    || spot_visible_entities_queue.borrow_local_mut(),
628                    |spot_visible_entities_local_queue,
629                     (
630                        entity,
631                        inherited_visibility,
632                        mut view_visibility,
633                        maybe_entity_mask,
634                        maybe_aabb,
635                        maybe_transform,
636                        has_visibility_range,
637                        has_no_frustum_culling,
638                    )| {
639                        if !inherited_visibility.get() {
640                            return;
641                        }
642
643                        let entity_mask = maybe_entity_mask.unwrap_or_default();
644                        if !view_mask.intersects(entity_mask) {
645                            return;
646                        }
647                        // Check visibility ranges.
648                        if has_visibility_range
649                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
650                                !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
651                            })
652                        {
653                            return;
654                        }
655
656                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
657                            let model_to_world = transform.affine();
658                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
659                            if !has_no_frustum_culling
660                                && !light_sphere.intersects_obb(aabb, &model_to_world)
661                            {
662                                return;
663                            }
664
665                            if has_no_frustum_culling
666                                || frustum.intersects_obb(aabb, &model_to_world, true, true)
667                            {
668                                view_visibility.set_visible();
669                                spot_visible_entities_local_queue.push(entity);
670                            }
671                        } else {
672                            view_visibility.set_visible();
673                            spot_visible_entities_local_queue.push(entity);
674                        }
675                    },
676                );
677
678                for entities in spot_visible_entities_queue.iter_mut() {
679                    visible_entities.append(entities);
680                }
681
682                shrink_entities(visible_entities.deref_mut());
683            }
684        }
685    }
686}