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            .configure_sets(
178                PostUpdate,
179                SimulationLightSystems::AssignLightsToClusters
180                    .before(bevy_app::TransformGizmoRenderStep),
181            )
182            .add_systems(Update, automatically_add_parallax_correction_components)
183            .add_systems(
184                PostUpdate,
185                (
186                    validate_shadow_map_size.before(build_directional_light_cascades),
187                    assign_objects_to_clusters
188                        .in_set(SimulationLightSystems::AssignLightsToClusters)
189                        .after(TransformSystems::Propagate)
190                        .after(VisibilitySystems::CheckVisibility)
191                        .after(CameraUpdateSystems),
192                    update_directional_light_frusta
193                        .in_set(SimulationLightSystems::UpdateLightFrusta)
194                        // This must run after CheckVisibility because it relies on `ViewVisibility`
195                        .after(VisibilitySystems::CheckVisibility)
196                        .after(TransformSystems::Propagate)
197                        .after(SimulationLightSystems::UpdateDirectionalLightCascades)
198                        // We assume that no entity will be both a directional light and a spot light,
199                        // so these systems will run independently of one another.
200                        // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
201                        .ambiguous_with(update_spot_light_frusta),
202                    update_point_light_frusta
203                        .in_set(SimulationLightSystems::UpdateLightFrusta)
204                        .after(TransformSystems::Propagate)
205                        .after(SimulationLightSystems::AssignLightsToClusters),
206                    #[cfg(feature = "bevy_gizmos")]
207                    update_spot_light_frusta
208                        .in_set(SimulationLightSystems::UpdateLightFrusta)
209                        .before(FrustumGizmoSystems)
210                        .after(TransformSystems::Propagate)
211                        .after(SimulationLightSystems::AssignLightsToClusters),
212                    #[cfg(not(feature = "bevy_gizmos"))]
213                    update_spot_light_frusta
214                        .in_set(SimulationLightSystems::UpdateLightFrusta)
215                        .after(TransformSystems::Propagate)
216                        .after(SimulationLightSystems::AssignLightsToClusters),
217                    (
218                        check_dir_light_mesh_visibility,
219                        check_point_light_mesh_visibility,
220                    )
221                        .in_set(SimulationLightSystems::CheckLightVisibility)
222                        .after(VisibilitySystems::CalculateBounds)
223                        .after(TransformSystems::Propagate)
224                        .after(SimulationLightSystems::UpdateLightFrusta)
225                        // Lights can "see" entities and mark them as visible. This is done to
226                        // correctly render shadows for entities that are not in view of a camera,
227                        // but must be renderable to cast shadows. Because of this, we need to check
228                        // entity visibility and mark as visible before they can be hidden.
229                        .after(VisibilitySystems::CheckVisibility)
230                        .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
231                    (
232                        update_point_light_bounding_spheres.after(TransformSystems::Propagate),
233                        update_spot_light_bounding_spheres.after(TransformSystems::Propagate),
234                        add_light_probe_and_decal_aabbs,
235                    )
236                        .in_set(SimulationLightSystems::UpdateBounds)
237                        .before(VisibilitySystems::UpdateFrusta),
238                    build_directional_light_cascades
239                        .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
240                        .after(TransformSystems::Propagate)
241                        .before(bevy_app::TransformGizmoRenderStep)
242                        .after(CameraUpdateSystems),
243                    extract_chromatic_phase_textures.after(AssetEventSystems),
244                ),
245            );
246
247        #[cfg(feature = "bevy_gizmos")]
248        app.add_plugins(gizmos::LightGizmoPlugin);
249    }
250}
251
252/// A convenient alias for `Or<(With<PointLight>, With<SpotLight>,
253/// With<DirectionalLight>, With<RectLight>)>`, for use with [`bevy_camera::visibility::VisibleEntities`].
254pub type WithLight = Or<(
255    With<PointLight>,
256    With<SpotLight>,
257    With<DirectionalLight>,
258    With<RectLight>,
259)>;
260
261/// Add this component to make a [`Mesh3d`] not cast shadows.
262#[derive(Debug, Component, Reflect, Default, Clone, PartialEq)]
263#[reflect(Component, Default, Debug, Clone, PartialEq)]
264pub struct NotShadowCaster;
265/// Add this component to make a [`Mesh3d`] not receive shadows.
266///
267/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will
268/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled,
269/// even when [`TransmittedShadowReceiver`] is being used.
270#[derive(Debug, Component, Reflect, Default, Clone)]
271#[reflect(Component, Default, Debug)]
272pub struct NotShadowReceiver;
273/// Add this component to make a [`Mesh3d`] using a PBR material with `StandardMaterial::diffuse_transmission > 0.0`
274/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”)
275///
276/// Not enabled by default, as it requires carefully setting up `StandardMaterial::thickness`
277/// (and potentially even baking a thickness texture!) to match the geometry of the mesh, in order to avoid self-shadow artifacts.
278///
279/// **Note:** Using [`NotShadowReceiver`] overrides this component.
280#[derive(Debug, Component, Reflect, Default, Clone)]
281#[reflect(Component, Default, Debug)]
282pub struct TransmittedShadowReceiver;
283
284/// Add this component to a [`Camera3d`]
285/// to control how to anti-alias shadow edges.
286///
287/// The different modes use different approaches to
288/// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing).
289#[derive(Debug, Component, Reflect, Clone, Copy, PartialEq, Eq, Default)]
290#[reflect(Component, Default, Debug, PartialEq, Clone)]
291pub enum ShadowFilteringMethod {
292    /// Hardware 2x2.
293    ///
294    /// Fast but poor quality.
295    Hardware2x2,
296    /// Approximates a fixed Gaussian blur, good when TAA isn't in use.
297    ///
298    /// Good quality, good performance.
299    ///
300    /// For directional and spot lights, this uses a [method by Ignacio Castaño
301    /// for *The Witness*] using 9 samples and smart filtering to achieve the same
302    /// as a regular 5x5 filter kernel.
303    ///
304    /// [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/
305    #[default]
306    Gaussian,
307    /// A randomized filter that varies over time, good when TAA is in use.
308    ///
309    /// Good quality when used with `TemporalAntiAliasing`
310    /// and good performance.
311    ///
312    /// For directional and spot lights, this uses a [method by Jorge Jimenez for
313    /// *Call of Duty: Advanced Warfare*] using 8 samples in spiral pattern,
314    /// randomly-rotated by interleaved gradient noise with spatial variation.
315    ///
316    /// [method by Jorge Jimenez for *Call of Duty: Advanced Warfare*]: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
317    Temporal,
318}
319
320/// System sets used to run light-related systems.
321#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
322pub enum SimulationLightSystems {
323    /// The set that adds AABBs and bounding spheres to clustered objects.
324    UpdateBounds,
325    /// After this set, all lights have been clustered.
326    AssignLightsToClusters,
327    /// After this set, all directional light cascades are up to date.
328    UpdateDirectionalLightCascades,
329    /// After this set, the frusta of shadow-casting point lights, spot lights, and directional lights are up to date.
330    UpdateLightFrusta,
331    /// System order ambiguities between systems in this set are ignored:
332    /// the order of systems within this set is irrelevant, as the various visibility-checking systems
333    /// assumes that their operations are irreversible during the frame.
334    CheckLightVisibility,
335}
336
337/// Updates the visibility for [`DirectionalLight`]s so that shadow map
338/// rendering can work.
339///
340/// This only processes entities without [`NoCpuCulling`]. Entities with
341/// [`NoCpuCulling`] receive no view-specific processing in the main world.
342pub fn check_dir_light_mesh_visibility(
343    mut commands: Commands,
344    mut directional_lights: Query<
345        (
346            &DirectionalLight,
347            &CascadesFrusta,
348            &mut CascadesVisibleEntities,
349            Option<&RenderLayers>,
350            &ViewVisibility,
351        ),
352        Without<SpotLight>,
353    >,
354    visible_entity_query: Query<
355        (
356            Entity,
357            &InheritedVisibility,
358            Option<&RenderLayers>,
359            Option<&Aabb>,
360            Option<&GlobalTransform>,
361            Has<VisibilityRange>,
362            Has<NoFrustumCulling>,
363        ),
364        (
365            Without<NotShadowCaster>,
366            Without<DirectionalLight>,
367            Without<NoCpuCulling>,
368            With<Mesh3d>,
369        ),
370    >,
371    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
372    mut defer_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
373    mut view_visible_entities_queue: Local<Parallel<Vec<Vec<Entity>>>>,
374) {
375    let visible_entity_ranges = visible_entity_ranges.as_deref();
376
377    for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in
378        &mut directional_lights
379    {
380        let mut views_to_remove = Vec::new();
381        for (view, cascade_view_entities) in &mut visible_entities.entities {
382            match frusta.frusta.get(view) {
383                Some(view_frusta) => {
384                    cascade_view_entities.resize(view_frusta.len(), Default::default());
385                }
386                None => views_to_remove.push(*view),
387            };
388        }
389        for (view, frusta) in &frusta.frusta {
390            visible_entities
391                .entities
392                .entry(*view)
393                .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]);
394        }
395
396        for v in views_to_remove {
397            visible_entities.entities.remove(&v);
398        }
399
400        // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
401        if !directional_light.shadow_maps_enabled || !light_view_visibility.get() {
402            visible_entities.entities.clear();
403            continue;
404        }
405
406        let view_mask = maybe_view_mask.unwrap_or_default();
407
408        for (view, view_frusta) in &frusta.frusta {
409            // Resize any per-thread buffer left at a stale state from a prior frame.
410            // Threads receiving no work this frame won't run the `init` closure from `par_iter`, and without this
411            // their buffer might be indexed out-of-bounds during collection if the number of cascades has increased.
412            for thread_queue in view_visible_entities_queue.iter_mut() {
413                thread_queue.resize(view_frusta.len(), Vec::default());
414            }
415
416            visible_entity_query.par_iter().for_each_init(
417                || {
418                    let mut entities = view_visible_entities_queue.borrow_local_mut();
419                    entities.resize(view_frusta.len(), Vec::default());
420                    (defer_visible_entities_queue.borrow_local_mut(), entities)
421                },
422                |(defer_visible_entities_local_queue, view_visible_entities_local_queue),
423                 (
424                    entity,
425                    inherited_visibility,
426                    maybe_entity_mask,
427                    maybe_aabb,
428                    maybe_transform,
429                    has_visibility_range,
430                    has_no_frustum_culling,
431                )| {
432                    if !inherited_visibility.get() {
433                        return;
434                    }
435
436                    let entity_mask = maybe_entity_mask.unwrap_or_default();
437                    if !view_mask.intersects(entity_mask) {
438                        return;
439                    }
440
441                    // Check visibility ranges.
442                    if has_visibility_range
443                        && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
444                            !visible_entity_ranges.entity_is_in_range_of_view(entity, *view)
445                        })
446                    {
447                        return;
448                    }
449
450                    if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
451                        let mut visible = false;
452                        for (frustum, frustum_visible_entities) in view_frusta
453                            .iter()
454                            .zip(view_visible_entities_local_queue.iter_mut())
455                        {
456                            // Disable near-plane culling, as a shadow caster could lie before the near plane.
457                            if !has_no_frustum_culling
458                                && !frustum.intersects_obb(aabb, &transform.affine(), false, true)
459                            {
460                                continue;
461                            }
462                            visible = true;
463
464                            frustum_visible_entities.push(entity);
465                        }
466                        if visible {
467                            defer_visible_entities_local_queue.push(entity);
468                        }
469                    } else {
470                        defer_visible_entities_local_queue.push(entity);
471                        for frustum_visible_entities in view_visible_entities_local_queue.iter_mut()
472                        {
473                            frustum_visible_entities.push(entity);
474                        }
475                    }
476                },
477            );
478            // collect entities from parallel queue
479            for (view_dest_index, view_dest) in visible_entities
480                .entities
481                .get_mut(view)
482                .unwrap()
483                .iter_mut()
484                .enumerate()
485            {
486                view_dest.entities.clear();
487                for thread_entity_queue in view_visible_entities_queue.iter_mut() {
488                    view_dest
489                        .entities
490                        .append(&mut thread_entity_queue[view_dest_index]);
491                }
492                view_dest.shrink();
493                view_dest.entities.sort_unstable();
494            }
495        }
496    }
497
498    // Defer marking view visibility so this system can run in parallel with check_point_light_mesh_visibility
499    // TODO: use resource to avoid unnecessary memory alloc
500    let mut defer_queue = mem::take(defer_visible_entities_queue.deref_mut());
501    commands.queue(move |world: &mut World| {
502        let mut query = world.query::<&mut ViewVisibility>();
503        for entities in defer_queue.iter_mut() {
504            let mut iter = query.iter_many_mut(world, entities.iter()).matched();
505            while let Some(mut view_visibility) = iter.fetch_next() {
506                view_visibility.set_visible();
507            }
508        }
509    });
510}
511
512/// Updates the visibility for [`PointLight`]s and [`SpotLight`]s so that shadow
513/// map rendering can work.
514///
515/// This only processes entities without [`NoCpuCulling`]. Entities with
516/// [`NoCpuCulling`] receive no view-specific processing in the main world.
517pub fn check_point_light_mesh_visibility(
518    visible_point_lights: Query<&VisibleEntities>,
519    mut point_lights: Query<(
520        &PointLight,
521        &GlobalTransform,
522        &CubemapFrusta,
523        &mut CubemapVisibleEntities,
524        Option<&RenderLayers>,
525    )>,
526    mut spot_lights: Query<(
527        &SpotLight,
528        &GlobalTransform,
529        &Frustum,
530        &mut VisibleMeshEntities,
531        Option<&RenderLayers>,
532    )>,
533    mut visible_entity_query: Query<
534        (
535            Entity,
536            &InheritedVisibility,
537            &mut ViewVisibility,
538            Option<&RenderLayers>,
539            Option<&Aabb>,
540            Option<&GlobalTransform>,
541            Has<VisibilityRange>,
542            Has<NoFrustumCulling>,
543        ),
544        (
545            Without<NotShadowCaster>,
546            Without<DirectionalLight>,
547            Without<NoCpuCulling>,
548            With<Mesh3d>,
549        ),
550    >,
551    mut camera_query: Query<(Entity, &RenderTarget), With<Camera>>,
552    mut shadow_lod_origin_query: Query<Entity, With<ShadowLodOrigin>>,
553    mut point_and_spot_light_query: Query<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
554    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
555    mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
556    mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
557    mut checked_lights: Local<EntityHashSet>,
558) {
559    checked_lights.clear();
560
561    let shadow_lod_origin = get_shadow_lod_origin(
562        camera_query.transmute_lens_filtered(),
563        shadow_lod_origin_query.transmute_lens_filtered(),
564        point_and_spot_light_query.transmute_lens_filtered(),
565    );
566
567    let visible_entity_ranges = visible_entity_ranges.as_deref();
568    for visible_lights in &visible_point_lights {
569        for &light_entity in visible_lights.get(TypeId::of::<ClusterVisibilityClass>()) {
570            if !checked_lights.insert(light_entity) {
571                continue;
572            }
573
574            // Point lights
575            if let Ok((
576                point_light,
577                transform,
578                cubemap_frusta,
579                mut cubemap_visible_entities,
580                maybe_view_mask,
581            )) = point_lights.get_mut(light_entity)
582            {
583                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
584                if !point_light.shadow_maps_enabled {
585                    continue;
586                }
587
588                let view_mask = maybe_view_mask.unwrap_or_default();
589                let light_sphere = Sphere {
590                    center: Vec3A::from(transform.translation()),
591                    radius: point_light.range,
592                };
593
594                visible_entity_query.par_iter_mut().for_each_init(
595                    || cubemap_visible_entities_queue.borrow_local_mut(),
596                    |cubemap_visible_entities_local_queue,
597                     (
598                        entity,
599                        inherited_visibility,
600                        mut view_visibility,
601                        maybe_entity_mask,
602                        maybe_aabb,
603                        maybe_transform,
604                        has_visibility_range,
605                        has_no_frustum_culling,
606                    )| {
607                        if !inherited_visibility.get() {
608                            return;
609                        }
610                        let entity_mask = maybe_entity_mask.unwrap_or_default();
611                        if !view_mask.intersects(entity_mask) {
612                            return;
613                        }
614                        if has_visibility_range
615                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
616                                shadow_lod_origin.is_none_or(|shadow_lod_origin| {
617                                    !visible_entity_ranges
618                                        .entity_is_in_range_of_view(entity, shadow_lod_origin)
619                                })
620                            })
621                        {
622                            return;
623                        }
624
625                        // If we have an aabb and transform, do frustum culling
626                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
627                            let model_to_world = transform.affine();
628                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
629                            if !has_no_frustum_culling
630                                && !light_sphere.intersects_obb(aabb, &model_to_world)
631                            {
632                                return;
633                            }
634
635                            for (frustum, visible_entities) in cubemap_frusta
636                                .iter()
637                                .zip(cubemap_visible_entities_local_queue.iter_mut())
638                            {
639                                if has_no_frustum_culling
640                                    || frustum.intersects_obb(aabb, &model_to_world, true, true)
641                                {
642                                    view_visibility.set_visible();
643                                    visible_entities.push(entity);
644                                }
645                            }
646                        } else {
647                            view_visibility.set_visible();
648                            for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
649                            {
650                                visible_entities.push(entity);
651                            }
652                        }
653                    },
654                );
655
656                // Collect the entities from each parallel queue.
657                for (view_dest_index, view_dest) in cubemap_visible_entities.iter_mut().enumerate()
658                {
659                    view_dest.entities.clear();
660                    for thread_entity_queue in cubemap_visible_entities_queue.iter_mut() {
661                        view_dest
662                            .entities
663                            .append(&mut thread_entity_queue[view_dest_index]);
664                    }
665                    view_dest.shrink();
666                    view_dest.entities.sort_unstable();
667                }
668            }
669
670            // Spot lights
671            if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
672                spot_lights.get_mut(light_entity)
673            {
674                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
675                if !point_light.shadow_maps_enabled {
676                    continue;
677                }
678
679                let view_mask = maybe_view_mask.unwrap_or_default();
680                let light_sphere = Sphere {
681                    center: Vec3A::from(transform.translation()),
682                    radius: point_light.range,
683                };
684
685                visible_entity_query.par_iter_mut().for_each_init(
686                    || spot_visible_entities_queue.borrow_local_mut(),
687                    |spot_visible_entities_local_queue,
688                     (
689                        entity,
690                        inherited_visibility,
691                        mut view_visibility,
692                        maybe_entity_mask,
693                        maybe_aabb,
694                        maybe_transform,
695                        has_visibility_range,
696                        has_no_frustum_culling,
697                    )| {
698                        if !inherited_visibility.get() {
699                            return;
700                        }
701
702                        let entity_mask = maybe_entity_mask.unwrap_or_default();
703                        if !view_mask.intersects(entity_mask) {
704                            return;
705                        }
706                        // Check visibility ranges.
707                        if has_visibility_range
708                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
709                                shadow_lod_origin.is_none_or(|shadow_lod_origin| {
710                                    !visible_entity_ranges
711                                        .entity_is_in_range_of_view(entity, shadow_lod_origin)
712                                })
713                            })
714                        {
715                            return;
716                        }
717
718                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
719                            let model_to_world = transform.affine();
720                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
721                            if !has_no_frustum_culling
722                                && !light_sphere.intersects_obb(aabb, &model_to_world)
723                            {
724                                return;
725                            }
726
727                            if has_no_frustum_culling
728                                || frustum.intersects_obb(aabb, &model_to_world, true, true)
729                            {
730                                view_visibility.set_visible();
731                                spot_visible_entities_local_queue.push(entity);
732                            }
733                        } else {
734                            view_visibility.set_visible();
735                            spot_visible_entities_local_queue.push(entity);
736                        }
737                    },
738                );
739
740                visible_entities.entities.clear();
741                for thread_entity_queue in spot_visible_entities_queue.iter_mut() {
742                    visible_entities.entities.append(thread_entity_queue);
743                }
744                visible_entities.shrink();
745                visible_entities.entities.sort_unstable();
746            }
747        }
748    }
749}
750
751/// Determines the LOD origin for spot and point light shadow maps.
752///
753/// The selection priority is, from highest to lowest:
754///
755/// 1. An entity explicitly marked with the [`ShadowLodOrigin`] component.
756///
757/// 2. A camera that renders to a window.
758///
759/// 3. Any camera.
760pub fn get_shadow_lod_origin(
761    mut camera_query: QueryLens<(Entity, &RenderTarget), With<Camera>>,
762    mut shadow_lod_origin_query: QueryLens<Entity, With<ShadowLodOrigin>>,
763    mut lights_query: QueryLens<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
764) -> Option<Entity> {
765    let (camera_query, shadow_lod_origin_query) =
766        (camera_query.query(), shadow_lod_origin_query.query());
767
768    let mut entities: SmallVec<[Entity; 4]> = smallvec![];
769    entities.extend(shadow_lod_origin_query.iter());
770    if let Some(lod_origin) = entities.iter().min() {
771        return Some(*lod_origin);
772    }
773
774    entities.extend(
775        camera_query
776            .iter()
777            .filter_map(|(main_entity, render_target)| match *render_target {
778                RenderTarget::Window(_) => Some(main_entity),
779                _ => None,
780            }),
781    );
782    if let Some(lod_origin) = entities.iter().min() {
783        return Some(*lod_origin);
784    };
785
786    entities.extend(camera_query.iter().map(|(main_entity, _)| main_entity));
787    if let Some(lod_origin) = entities.iter().min() {
788        if !lights_query.query().is_empty() {
789            warn_once!(
790                "Point lights and/or spot lights are present, but no entity has \
791                 `ShadowLodOrigin`, and no camera that renders to the window has been found. \
792                 Consider using the `ShadowLodOrigin` component to set a LOD origin."
793            );
794        }
795
796        return Some(*lod_origin);
797    };
798
799    None
800}