Skip to main content

bevy_light/
lib.rs

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