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