bevy_pbr/light/
mod.rs

1use core::ops::DerefMut;
2
3use bevy_ecs::{
4    entity::{EntityHashMap, EntityHashSet},
5    prelude::*,
6};
7use bevy_math::{ops, Mat4, Vec3A, Vec4};
8use bevy_reflect::prelude::*;
9use bevy_render::{
10    camera::{Camera, CameraProjection},
11    extract_component::ExtractComponent,
12    extract_resource::ExtractResource,
13    mesh::Mesh3d,
14    primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
15    view::{
16        InheritedVisibility, NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange,
17        VisibleEntityRanges,
18    },
19};
20use bevy_transform::components::{GlobalTransform, Transform};
21use bevy_utils::Parallel;
22
23use crate::*;
24
25mod ambient_light;
26pub use ambient_light::AmbientLight;
27
28mod point_light;
29pub use point_light::PointLight;
30mod spot_light;
31pub use spot_light::SpotLight;
32mod directional_light;
33pub use directional_light::DirectionalLight;
34
35/// Constants for operating with the light units: lumens, and lux.
36pub mod light_consts {
37    /// Approximations for converting the wattage of lamps to lumens.
38    ///
39    /// The **lumen** (symbol: **lm**) is the unit of [luminous flux], a measure
40    /// of the total quantity of [visible light] emitted by a source per unit of
41    /// time, in the [International System of Units] (SI).
42    ///
43    /// For more information, see [wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit))
44    ///
45    /// [luminous flux]: https://en.wikipedia.org/wiki/Luminous_flux
46    /// [visible light]: https://en.wikipedia.org/wiki/Visible_light
47    /// [International System of Units]: https://en.wikipedia.org/wiki/International_System_of_Units
48    pub mod lumens {
49        pub const LUMENS_PER_LED_WATTS: f32 = 90.0;
50        pub const LUMENS_PER_INCANDESCENT_WATTS: f32 = 13.8;
51        pub const LUMENS_PER_HALOGEN_WATTS: f32 = 19.8;
52    }
53
54    /// Predefined for lux values in several locations.
55    ///
56    /// The **lux** (symbol: **lx**) is the unit of [illuminance], or [luminous flux] per unit area,
57    /// in the [International System of Units] (SI). It is equal to one lumen per square meter.
58    ///
59    /// For more information, see [wikipedia](https://en.wikipedia.org/wiki/Lux)
60    ///
61    /// [illuminance]: https://en.wikipedia.org/wiki/Illuminance
62    /// [luminous flux]: https://en.wikipedia.org/wiki/Luminous_flux
63    /// [International System of Units]: https://en.wikipedia.org/wiki/International_System_of_Units
64    pub mod lux {
65        /// The amount of light (lux) in a moonless, overcast night sky. (starlight)
66        pub const MOONLESS_NIGHT: f32 = 0.0001;
67        /// The amount of light (lux) during a full moon on a clear night.
68        pub const FULL_MOON_NIGHT: f32 = 0.05;
69        /// The amount of light (lux) during the dark limit of civil twilight under a clear sky.
70        pub const CIVIL_TWILIGHT: f32 = 3.4;
71        /// The amount of light (lux) in family living room lights.
72        pub const LIVING_ROOM: f32 = 50.;
73        /// The amount of light (lux) in an office building's hallway/toilet lighting.
74        pub const HALLWAY: f32 = 80.;
75        /// The amount of light (lux) in very dark overcast day
76        pub const DARK_OVERCAST_DAY: f32 = 100.;
77        /// The amount of light (lux) in an office.
78        pub const OFFICE: f32 = 320.;
79        /// The amount of light (lux) during sunrise or sunset on a clear day.
80        pub const CLEAR_SUNRISE: f32 = 400.;
81        /// The amount of light (lux) on a overcast day; typical TV studio lighting
82        pub const OVERCAST_DAY: f32 = 1000.;
83        /// The amount of light (lux) from ambient daylight (not direct sunlight).
84        pub const AMBIENT_DAYLIGHT: f32 = 10_000.;
85        /// The amount of light (lux) in full daylight (not direct sun).
86        pub const FULL_DAYLIGHT: f32 = 20_000.;
87        /// The amount of light (lux) in direct sunlight.
88        pub const DIRECT_SUNLIGHT: f32 = 100_000.;
89    }
90}
91
92#[derive(Resource, Clone, Debug, Reflect)]
93#[reflect(Resource, Debug, Default)]
94pub struct PointLightShadowMap {
95    pub size: usize,
96}
97
98impl Default for PointLightShadowMap {
99    fn default() -> Self {
100        Self { size: 1024 }
101    }
102}
103
104/// A convenient alias for `Or<(With<PointLight>, With<SpotLight>,
105/// With<DirectionalLight>)>`, for use with [`bevy_render::view::VisibleEntities`].
106pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
107
108/// Controls the resolution of [`DirectionalLight`] shadow maps.
109#[derive(Resource, Clone, Debug, Reflect)]
110#[reflect(Resource, Debug, Default)]
111pub struct DirectionalLightShadowMap {
112    pub size: usize,
113}
114
115impl Default for DirectionalLightShadowMap {
116    fn default() -> Self {
117        Self { size: 2048 }
118    }
119}
120
121/// Controls how cascaded shadow mapping works.
122/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance.
123///
124/// ```
125/// # use bevy_pbr::CascadeShadowConfig;
126/// # use bevy_pbr::CascadeShadowConfigBuilder;
127/// # use bevy_utils::default;
128/// #
129/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder {
130///   maximum_distance: 100.0,
131///   ..default()
132/// }.into();
133/// ```
134#[derive(Component, Clone, Debug, Reflect)]
135#[reflect(Component, Default, Debug)]
136pub struct CascadeShadowConfig {
137    /// The (positive) distance to the far boundary of each cascade.
138    pub bounds: Vec<f32>,
139    /// The proportion of overlap each cascade has with the previous cascade.
140    pub overlap_proportion: f32,
141    /// The (positive) distance to the near boundary of the first cascade.
142    pub minimum_distance: f32,
143}
144
145impl Default for CascadeShadowConfig {
146    fn default() -> Self {
147        CascadeShadowConfigBuilder::default().into()
148    }
149}
150
151fn calculate_cascade_bounds(
152    num_cascades: usize,
153    nearest_bound: f32,
154    shadow_maximum_distance: f32,
155) -> Vec<f32> {
156    if num_cascades == 1 {
157        return vec![shadow_maximum_distance];
158    }
159    let base = ops::powf(
160        shadow_maximum_distance / nearest_bound,
161        1.0 / (num_cascades - 1) as f32,
162    );
163    (0..num_cascades)
164        .map(|i| nearest_bound * ops::powf(base, i as f32))
165        .collect()
166}
167
168/// Builder for [`CascadeShadowConfig`].
169pub struct CascadeShadowConfigBuilder {
170    /// The number of shadow cascades.
171    /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas
172    /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing
173    /// blocky looking shadows.
174    ///
175    /// This does come at the cost increased rendering overhead, however this overhead is still less
176    /// than if you were to use fewer cascades and much larger shadow map textures to achieve the
177    /// same quality level.
178    ///
179    /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may
180    /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing
181    /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately.
182    pub num_cascades: usize,
183    /// The minimum shadow distance, which can help improve the texel resolution of the first cascade.
184    /// Areas nearer to the camera than this will likely receive no shadows.
185    ///
186    /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as
187    /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the
188    /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane
189    /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to
190    /// `first_cascade_far_bound`.
191    pub minimum_distance: f32,
192    /// The maximum shadow distance.
193    /// Areas further from the camera than this will likely receive no shadows.
194    pub maximum_distance: f32,
195    /// Sets the far bound of the first cascade, relative to the view origin.
196    /// In-between cascades will be exponentially spaced relative to the maximum shadow distance.
197    /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence.
198    pub first_cascade_far_bound: f32,
199    /// Sets the overlap proportion between cascades.
200    /// The overlap is used to make the transition from one cascade's shadow map to the next
201    /// less abrupt by blending between both shadow maps.
202    pub overlap_proportion: f32,
203}
204
205impl CascadeShadowConfigBuilder {
206    /// Returns the cascade config as specified by this builder.
207    pub fn build(&self) -> CascadeShadowConfig {
208        assert!(
209            self.num_cascades > 0,
210            "num_cascades must be positive, but was {}",
211            self.num_cascades
212        );
213        assert!(
214            self.minimum_distance >= 0.0,
215            "maximum_distance must be non-negative, but was {}",
216            self.minimum_distance
217        );
218        assert!(
219            self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound,
220            "minimum_distance must be less than first_cascade_far_bound, but was {}",
221            self.minimum_distance
222        );
223        assert!(
224            self.maximum_distance > self.minimum_distance,
225            "maximum_distance must be greater than minimum_distance, but was {}",
226            self.maximum_distance
227        );
228        assert!(
229            (0.0..1.0).contains(&self.overlap_proportion),
230            "overlap_proportion must be in [0.0, 1.0) but was {}",
231            self.overlap_proportion
232        );
233        CascadeShadowConfig {
234            bounds: calculate_cascade_bounds(
235                self.num_cascades,
236                self.first_cascade_far_bound,
237                self.maximum_distance,
238            ),
239            overlap_proportion: self.overlap_proportion,
240            minimum_distance: self.minimum_distance,
241        }
242    }
243}
244
245impl Default for CascadeShadowConfigBuilder {
246    fn default() -> Self {
247        if cfg!(all(
248            feature = "webgl",
249            target_arch = "wasm32",
250            not(feature = "webgpu")
251        )) {
252            // Currently only support one cascade in webgl.
253            Self {
254                num_cascades: 1,
255                minimum_distance: 0.1,
256                maximum_distance: 100.0,
257                first_cascade_far_bound: 5.0,
258                overlap_proportion: 0.2,
259            }
260        } else {
261            Self {
262                num_cascades: 4,
263                minimum_distance: 0.1,
264                maximum_distance: 1000.0,
265                first_cascade_far_bound: 5.0,
266                overlap_proportion: 0.2,
267            }
268        }
269    }
270}
271
272impl From<CascadeShadowConfigBuilder> for CascadeShadowConfig {
273    fn from(builder: CascadeShadowConfigBuilder) -> Self {
274        builder.build()
275    }
276}
277
278#[derive(Component, Clone, Debug, Default, Reflect)]
279#[reflect(Component, Debug, Default)]
280pub struct Cascades {
281    /// Map from a view to the configuration of each of its [`Cascade`]s.
282    pub(crate) cascades: EntityHashMap<Vec<Cascade>>,
283}
284
285#[derive(Clone, Debug, Default, Reflect)]
286pub struct Cascade {
287    /// The transform of the light, i.e. the view to world matrix.
288    pub(crate) world_from_cascade: Mat4,
289    /// The orthographic projection for this cascade.
290    pub(crate) clip_from_cascade: Mat4,
291    /// The view-projection matrix for this cascade, converting world space into light clip space.
292    /// Importantly, this is derived and stored separately from `view_transform` and `projection` to
293    /// ensure shadow stability.
294    pub(crate) clip_from_world: Mat4,
295    /// Size of each shadow map texel in world units.
296    pub(crate) texel_size: f32,
297}
298
299pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) {
300    for (directional_light, mut cascades) in lights.iter_mut() {
301        if !directional_light.shadows_enabled {
302            continue;
303        }
304        cascades.cascades.clear();
305    }
306}
307
308pub fn build_directional_light_cascades<P: CameraProjection + Component>(
309    directional_light_shadow_map: Res<DirectionalLightShadowMap>,
310    views: Query<(Entity, &GlobalTransform, &P, &Camera)>,
311    mut lights: Query<(
312        &GlobalTransform,
313        &DirectionalLight,
314        &CascadeShadowConfig,
315        &mut Cascades,
316    )>,
317) {
318    let views = views
319        .iter()
320        .filter_map(|(entity, transform, projection, camera)| {
321            if camera.is_active {
322                Some((entity, projection, transform.compute_matrix()))
323            } else {
324                None
325            }
326        })
327        .collect::<Vec<_>>();
328
329    for (transform, directional_light, cascades_config, mut cascades) in &mut lights {
330        if !directional_light.shadows_enabled {
331            continue;
332        }
333
334        // It is very important to the numerical and thus visual stability of shadows that
335        // light_to_world has orthogonal upper-left 3x3 and zero translation.
336        // Even though only the direction (i.e. rotation) of the light matters, we don't constrain
337        // users to not change any other aspects of the transform - there's no guarantee
338        // `transform.compute_matrix()` will give us a matrix with our desired properties.
339        // Instead, we directly create a good matrix from just the rotation.
340        let world_from_light = Mat4::from_quat(transform.compute_transform().rotation);
341        let light_to_world_inverse = world_from_light.inverse();
342
343        for (view_entity, projection, view_to_world) in views.iter().copied() {
344            let camera_to_light_view = light_to_world_inverse * view_to_world;
345            let view_cascades = cascades_config
346                .bounds
347                .iter()
348                .enumerate()
349                .map(|(idx, far_bound)| {
350                    // Negate bounds as -z is camera forward direction.
351                    let z_near = if idx > 0 {
352                        (1.0 - cascades_config.overlap_proportion)
353                            * -cascades_config.bounds[idx - 1]
354                    } else {
355                        -cascades_config.minimum_distance
356                    };
357                    let z_far = -far_bound;
358
359                    let corners = projection.get_frustum_corners(z_near, z_far);
360
361                    calculate_cascade(
362                        corners,
363                        directional_light_shadow_map.size as f32,
364                        world_from_light,
365                        camera_to_light_view,
366                    )
367                })
368                .collect();
369            cascades.cascades.insert(view_entity, view_cascades);
370        }
371    }
372}
373
374/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`.
375///
376/// The corner vertices should be specified in the following order:
377/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane.
378fn calculate_cascade(
379    frustum_corners: [Vec3A; 8],
380    cascade_texture_size: f32,
381    world_from_light: Mat4,
382    light_from_camera: Mat4,
383) -> Cascade {
384    let mut min = Vec3A::splat(f32::MAX);
385    let mut max = Vec3A::splat(f32::MIN);
386    for corner_camera_view in frustum_corners {
387        let corner_light_view = light_from_camera.transform_point3a(corner_camera_view);
388        min = min.min(corner_light_view);
389        max = max.max(corner_light_view);
390    }
391
392    // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this
393    //       will be the maximum possible projection size. Use the ceiling to get an integer which is
394    //       very important for floating point stability later. It is also important that these are
395    //       calculated using the original camera space corner positions for floating point precision
396    //       as even though the lengths using corner_light_view above should be the same, precision can
397    //       introduce small but significant differences.
398    // NOTE: The size remains the same unless the view frustum or cascade configuration is modified.
399    let cascade_diameter = (frustum_corners[0] - frustum_corners[6])
400        .length()
401        .max((frustum_corners[4] - frustum_corners[6]).length())
402        .ceil();
403
404    // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an
405    //       integer, cascade_texel_size is then an integer multiple of a power of 2 and can be
406    //       exactly represented in a floating point value.
407    let cascade_texel_size = cascade_diameter / cascade_texture_size;
408    // NOTE: For shadow stability it is very important that the near_plane_center is at integer
409    //       multiples of the texel size to be exactly representable in a floating point value.
410    let near_plane_center = Vec3A::new(
411        (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size,
412        (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size,
413        // NOTE: max.z is the near plane for right-handed y-up
414        max.z,
415    );
416
417    // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world`
418    // and inverting it, which risks instability due to numerical precision, we directly form
419    // `world_to_cascade` as the reference material suggests.
420    let light_to_world_transpose = world_from_light.transpose();
421    let cascade_from_world = Mat4::from_cols(
422        light_to_world_transpose.x_axis,
423        light_to_world_transpose.y_axis,
424        light_to_world_transpose.z_axis,
425        (-near_plane_center).extend(1.0),
426    );
427
428    // Right-handed orthographic projection, centered at `near_plane_center`.
429    // NOTE: This is different from the reference material, as we use reverse Z.
430    let r = (max.z - min.z).recip();
431    let clip_from_cascade = Mat4::from_cols(
432        Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0),
433        Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0),
434        Vec4::new(0.0, 0.0, r, 0.0),
435        Vec4::new(0.0, 0.0, 1.0, 1.0),
436    );
437
438    let clip_from_world = clip_from_cascade * cascade_from_world;
439    Cascade {
440        world_from_cascade: cascade_from_world.inverse(),
441        clip_from_cascade,
442        clip_from_world,
443        texel_size: cascade_texel_size,
444    }
445}
446/// Add this component to make a [`Mesh3d`] not cast shadows.
447#[derive(Debug, Component, Reflect, Default)]
448#[reflect(Component, Default, Debug)]
449pub struct NotShadowCaster;
450/// Add this component to make a [`Mesh3d`] not receive shadows.
451///
452/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will
453/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled,
454/// even when [`TransmittedShadowReceiver`] is being used.
455#[derive(Debug, Component, Reflect, Default)]
456#[reflect(Component, Default, Debug)]
457pub struct NotShadowReceiver;
458/// Add this component to make a [`Mesh3d`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0`
459/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”)
460///
461/// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness)
462/// (and potentially even baking a thickness texture!) to match the geometry of the mesh, in order to avoid self-shadow artifacts.
463///
464/// **Note:** Using [`NotShadowReceiver`] overrides this component.
465#[derive(Debug, Component, Reflect, Default)]
466#[reflect(Component, Default, Debug)]
467pub struct TransmittedShadowReceiver;
468
469/// Add this component to a [`Camera3d`](bevy_core_pipeline::core_3d::Camera3d)
470/// to control how to anti-alias shadow edges.
471///
472/// The different modes use different approaches to
473/// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing).
474#[derive(Debug, Component, ExtractComponent, Reflect, Clone, Copy, PartialEq, Eq, Default)]
475#[reflect(Component, Default, Debug, PartialEq)]
476pub enum ShadowFilteringMethod {
477    /// Hardware 2x2.
478    ///
479    /// Fast but poor quality.
480    Hardware2x2,
481    /// Approximates a fixed Gaussian blur, good when TAA isn't in use.
482    ///
483    /// Good quality, good performance.
484    ///
485    /// For directional and spot lights, this uses a [method by Ignacio Castaño
486    /// for *The Witness*] using 9 samples and smart filtering to achieve the same
487    /// as a regular 5x5 filter kernel.
488    ///
489    /// [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/
490    #[default]
491    Gaussian,
492    /// A randomized filter that varies over time, good when TAA is in use.
493    ///
494    /// Good quality when used with
495    /// [`TemporalAntiAliasing`](bevy_core_pipeline::experimental::taa::TemporalAntiAliasing)
496    /// and good performance.
497    ///
498    /// For directional and spot lights, this uses a [method by Jorge Jimenez for
499    /// *Call of Duty: Advanced Warfare*] using 8 samples in spiral pattern,
500    /// randomly-rotated by interleaved gradient noise with spatial variation.
501    ///
502    /// [method by Jorge Jimenez for *Call of Duty: Advanced Warfare*]: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
503    Temporal,
504}
505
506/// System sets used to run light-related systems.
507#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
508pub enum SimulationLightSystems {
509    AddClusters,
510    AssignLightsToClusters,
511    /// System order ambiguities between systems in this set are ignored:
512    /// each [`build_directional_light_cascades`] system is independent of the others,
513    /// and should operate on distinct sets of entities.
514    UpdateDirectionalLightCascades,
515    UpdateLightFrusta,
516    /// System order ambiguities between systems in this set are ignored:
517    /// the order of systems within this set is irrelevant, as the various visibility-checking systesms
518    /// assumes that their operations are irreversible during the frame.
519    CheckLightVisibility,
520}
521
522// Sort lights by
523// - those with volumetric (and shadows) enabled first, so that the volumetric
524//   lighting pass can quickly find the volumetric lights;
525// - then those with shadows enabled second, so that the index can be used to
526//   render at most `directional_light_shadow_maps_count` directional light
527//   shadows;
528// - then by entity as a stable key to ensure that a consistent set of lights
529//   are chosen if the light count limit is exceeded.
530pub(crate) fn directional_light_order(
531    (entity_1, volumetric_1, shadows_enabled_1): (&Entity, &bool, &bool),
532    (entity_2, volumetric_2, shadows_enabled_2): (&Entity, &bool, &bool),
533) -> core::cmp::Ordering {
534    volumetric_2
535        .cmp(volumetric_1) // volumetric before shadows
536        .then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters
537        .then_with(|| entity_1.cmp(entity_2)) // stable
538}
539
540pub fn update_directional_light_frusta(
541    mut views: Query<
542        (
543            &Cascades,
544            &DirectionalLight,
545            &ViewVisibility,
546            &mut CascadesFrusta,
547        ),
548        (
549            // Prevents this query from conflicting with camera queries.
550            Without<Camera>,
551        ),
552    >,
553) {
554    for (cascades, directional_light, visibility, mut frusta) in &mut views {
555        // The frustum is used for culling meshes to the light for shadow mapping
556        // so if shadow mapping is disabled for this light, then the frustum is
557        // not needed.
558        if !directional_light.shadows_enabled || !visibility.get() {
559            continue;
560        }
561
562        frusta.frusta = cascades
563            .cascades
564            .iter()
565            .map(|(view, cascades)| {
566                (
567                    *view,
568                    cascades
569                        .iter()
570                        .map(|c| Frustum::from_clip_from_world(&c.clip_from_world))
571                        .collect::<Vec<_>>(),
572                )
573            })
574            .collect();
575    }
576}
577
578// NOTE: Run this after assign_lights_to_clusters!
579pub fn update_point_light_frusta(
580    global_lights: Res<GlobalVisibleClusterableObjects>,
581    mut views: Query<
582        (Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta),
583        Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
584    >,
585) {
586    let view_rotations = CUBE_MAP_FACES
587        .iter()
588        .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
589        .collect::<Vec<_>>();
590
591    for (entity, transform, point_light, mut cubemap_frusta) in &mut views {
592        // The frusta are used for culling meshes to the light for shadow mapping
593        // so if shadow mapping is disabled for this light, then the frusta are
594        // not needed.
595        // Also, if the light is not relevant for any cluster, it will not be in the
596        // global lights set and so there is no need to update its frusta.
597        if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) {
598            continue;
599        }
600
601        let clip_from_view = Mat4::perspective_infinite_reverse_rh(
602            core::f32::consts::FRAC_PI_2,
603            1.0,
604            point_light.shadow_map_near_z,
605        );
606
607        // ignore scale because we don't want to effectively scale light radius and range
608        // by applying those as a view transform to shadow map rendering of objects
609        // and ignore rotation because we want the shadow map projections to align with the axes
610        let view_translation = Transform::from_translation(transform.translation());
611        let view_backward = transform.back();
612
613        for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) {
614            let world_from_view = view_translation * *view_rotation;
615            let clip_from_world = clip_from_view * world_from_view.compute_matrix().inverse();
616
617            *frustum = Frustum::from_clip_from_world_custom_far(
618                &clip_from_world,
619                &transform.translation(),
620                &view_backward,
621                point_light.range,
622            );
623        }
624    }
625}
626
627pub fn update_spot_light_frusta(
628    global_lights: Res<GlobalVisibleClusterableObjects>,
629    mut views: Query<
630        (Entity, &GlobalTransform, &SpotLight, &mut Frustum),
631        Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
632    >,
633) {
634    for (entity, transform, spot_light, mut frustum) in &mut views {
635        // The frusta are used for culling meshes to the light for shadow mapping
636        // so if shadow mapping is disabled for this light, then the frusta are
637        // not needed.
638        // Also, if the light is not relevant for any cluster, it will not be in the
639        // global lights set and so there is no need to update its frusta.
640        if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) {
641            continue;
642        }
643
644        // ignore scale because we don't want to effectively scale light radius and range
645        // by applying those as a view transform to shadow map rendering of objects
646        let view_backward = transform.back();
647
648        let spot_world_from_view = spot_light_world_from_view(transform);
649        let spot_clip_from_view =
650            spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z);
651        let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse();
652
653        *frustum = Frustum::from_clip_from_world_custom_far(
654            &clip_from_world,
655            &transform.translation(),
656            &view_backward,
657            spot_light.range,
658        );
659    }
660}
661
662fn shrink_entities(visible_entities: &mut Vec<Entity>) {
663    // Check that visible entities capacity() is no more than two times greater than len()
664    let capacity = visible_entities.capacity();
665    let reserved = capacity
666        .checked_div(visible_entities.len())
667        .map_or(0, |reserve| {
668            if reserve > 2 {
669                capacity / (reserve / 2)
670            } else {
671                capacity
672            }
673        });
674
675    visible_entities.shrink_to(reserved);
676}
677
678pub fn check_dir_light_mesh_visibility(
679    mut commands: Commands,
680    mut directional_lights: Query<
681        (
682            &DirectionalLight,
683            &CascadesFrusta,
684            &mut CascadesVisibleEntities,
685            Option<&RenderLayers>,
686            &ViewVisibility,
687        ),
688        Without<SpotLight>,
689    >,
690    visible_entity_query: Query<
691        (
692            Entity,
693            &InheritedVisibility,
694            Option<&RenderLayers>,
695            Option<&Aabb>,
696            Option<&GlobalTransform>,
697            Has<VisibilityRange>,
698            Has<NoFrustumCulling>,
699        ),
700        (
701            Without<NotShadowCaster>,
702            Without<DirectionalLight>,
703            With<Mesh3d>,
704        ),
705    >,
706    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
707    mut defer_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
708    mut view_visible_entities_queue: Local<Parallel<Vec<Vec<Entity>>>>,
709) {
710    let visible_entity_ranges = visible_entity_ranges.as_deref();
711
712    for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in
713        &mut directional_lights
714    {
715        let mut views_to_remove = Vec::new();
716        for (view, cascade_view_entities) in &mut visible_entities.entities {
717            match frusta.frusta.get(view) {
718                Some(view_frusta) => {
719                    cascade_view_entities.resize(view_frusta.len(), Default::default());
720                    cascade_view_entities.iter_mut().for_each(|x| x.clear());
721                }
722                None => views_to_remove.push(*view),
723            };
724        }
725        for (view, frusta) in &frusta.frusta {
726            visible_entities
727                .entities
728                .entry(*view)
729                .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]);
730        }
731
732        for v in views_to_remove {
733            visible_entities.entities.remove(&v);
734        }
735
736        // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
737        if !directional_light.shadows_enabled || !light_view_visibility.get() {
738            continue;
739        }
740
741        let view_mask = maybe_view_mask.unwrap_or_default();
742
743        for (view, view_frusta) in &frusta.frusta {
744            visible_entity_query.par_iter().for_each_init(
745                || {
746                    let mut entities = view_visible_entities_queue.borrow_local_mut();
747                    entities.resize(view_frusta.len(), Vec::default());
748                    (defer_visible_entities_queue.borrow_local_mut(), entities)
749                },
750                |(defer_visible_entities_local_queue, view_visible_entities_local_queue),
751                 (
752                    entity,
753                    inherited_visibility,
754                    maybe_entity_mask,
755                    maybe_aabb,
756                    maybe_transform,
757                    has_visibility_range,
758                    has_no_frustum_culling,
759                )| {
760                    if !inherited_visibility.get() {
761                        return;
762                    }
763
764                    let entity_mask = maybe_entity_mask.unwrap_or_default();
765                    if !view_mask.intersects(entity_mask) {
766                        return;
767                    }
768
769                    // Check visibility ranges.
770                    if has_visibility_range
771                        && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
772                            !visible_entity_ranges.entity_is_in_range_of_view(entity, *view)
773                        })
774                    {
775                        return;
776                    }
777
778                    if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
779                        let mut visible = false;
780                        for (frustum, frustum_visible_entities) in view_frusta
781                            .iter()
782                            .zip(view_visible_entities_local_queue.iter_mut())
783                        {
784                            // Disable near-plane culling, as a shadow caster could lie before the near plane.
785                            if !has_no_frustum_culling
786                                && !frustum.intersects_obb(aabb, &transform.affine(), false, true)
787                            {
788                                continue;
789                            }
790                            visible = true;
791
792                            frustum_visible_entities.push(entity);
793                        }
794                        if visible {
795                            defer_visible_entities_local_queue.push(entity);
796                        }
797                    } else {
798                        defer_visible_entities_local_queue.push(entity);
799                        for frustum_visible_entities in view_visible_entities_local_queue.iter_mut()
800                        {
801                            frustum_visible_entities.push(entity);
802                        }
803                    }
804                },
805            );
806            // collect entities from parallel queue
807            for entities in view_visible_entities_queue.iter_mut() {
808                visible_entities
809                    .entities
810                    .get_mut(view)
811                    .unwrap()
812                    .iter_mut()
813                    .zip(entities.iter_mut())
814                    .for_each(|(dst, source)| {
815                        dst.append(source);
816                    });
817            }
818        }
819
820        for (_, cascade_view_entities) in &mut visible_entities.entities {
821            cascade_view_entities
822                .iter_mut()
823                .map(DerefMut::deref_mut)
824                .for_each(shrink_entities);
825        }
826    }
827
828    // Defer marking view visibility so this system can run in parallel with check_point_light_mesh_visibility
829    // TODO: use resource to avoid unnecessary memory alloc
830    let mut defer_queue = core::mem::take(defer_visible_entities_queue.deref_mut());
831    commands.queue(move |world: &mut World| {
832        let mut query = world.query::<&mut ViewVisibility>();
833        for entities in defer_queue.iter_mut() {
834            let mut iter = query.iter_many_mut(world, entities.iter());
835            while let Some(mut view_visibility) = iter.fetch_next() {
836                view_visibility.set();
837            }
838        }
839    });
840}
841
842#[allow(clippy::too_many_arguments)]
843pub fn check_point_light_mesh_visibility(
844    visible_point_lights: Query<&VisibleClusterableObjects>,
845    mut point_lights: Query<(
846        &PointLight,
847        &GlobalTransform,
848        &CubemapFrusta,
849        &mut CubemapVisibleEntities,
850        Option<&RenderLayers>,
851    )>,
852    mut spot_lights: Query<(
853        &SpotLight,
854        &GlobalTransform,
855        &Frustum,
856        &mut VisibleMeshEntities,
857        Option<&RenderLayers>,
858    )>,
859    mut visible_entity_query: Query<
860        (
861            Entity,
862            &InheritedVisibility,
863            &mut ViewVisibility,
864            Option<&RenderLayers>,
865            Option<&Aabb>,
866            Option<&GlobalTransform>,
867            Has<VisibilityRange>,
868            Has<NoFrustumCulling>,
869        ),
870        (
871            Without<NotShadowCaster>,
872            Without<DirectionalLight>,
873            With<Mesh3d>,
874        ),
875    >,
876    visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
877    mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
878    mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
879    mut checked_lights: Local<EntityHashSet>,
880) {
881    checked_lights.clear();
882
883    let visible_entity_ranges = visible_entity_ranges.as_deref();
884    for visible_lights in &visible_point_lights {
885        for light_entity in visible_lights.entities.iter().copied() {
886            if !checked_lights.insert(light_entity) {
887                continue;
888            }
889
890            // Point lights
891            if let Ok((
892                point_light,
893                transform,
894                cubemap_frusta,
895                mut cubemap_visible_entities,
896                maybe_view_mask,
897            )) = point_lights.get_mut(light_entity)
898            {
899                for visible_entities in cubemap_visible_entities.iter_mut() {
900                    visible_entities.entities.clear();
901                }
902
903                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
904                if !point_light.shadows_enabled {
905                    continue;
906                }
907
908                let view_mask = maybe_view_mask.unwrap_or_default();
909                let light_sphere = Sphere {
910                    center: Vec3A::from(transform.translation()),
911                    radius: point_light.range,
912                };
913
914                visible_entity_query.par_iter_mut().for_each_init(
915                    || cubemap_visible_entities_queue.borrow_local_mut(),
916                    |cubemap_visible_entities_local_queue,
917                     (
918                        entity,
919                        inherited_visibility,
920                        mut view_visibility,
921                        maybe_entity_mask,
922                        maybe_aabb,
923                        maybe_transform,
924                        has_visibility_range,
925                        has_no_frustum_culling,
926                    )| {
927                        if !inherited_visibility.get() {
928                            return;
929                        }
930                        let entity_mask = maybe_entity_mask.unwrap_or_default();
931                        if !view_mask.intersects(entity_mask) {
932                            return;
933                        }
934                        if has_visibility_range
935                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
936                                !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
937                            })
938                        {
939                            return;
940                        }
941
942                        // If we have an aabb and transform, do frustum culling
943                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
944                            let model_to_world = transform.affine();
945                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
946                            if !has_no_frustum_culling
947                                && !light_sphere.intersects_obb(aabb, &model_to_world)
948                            {
949                                return;
950                            }
951
952                            for (frustum, visible_entities) in cubemap_frusta
953                                .iter()
954                                .zip(cubemap_visible_entities_local_queue.iter_mut())
955                            {
956                                if has_no_frustum_culling
957                                    || frustum.intersects_obb(aabb, &model_to_world, true, true)
958                                {
959                                    view_visibility.set();
960                                    visible_entities.push(entity);
961                                }
962                            }
963                        } else {
964                            view_visibility.set();
965                            for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
966                            {
967                                visible_entities.push(entity);
968                            }
969                        }
970                    },
971                );
972
973                for entities in cubemap_visible_entities_queue.iter_mut() {
974                    cubemap_visible_entities
975                        .iter_mut()
976                        .zip(entities.iter_mut())
977                        .for_each(|(dst, source)| dst.entities.append(source));
978                }
979
980                for visible_entities in cubemap_visible_entities.iter_mut() {
981                    shrink_entities(visible_entities);
982                }
983            }
984
985            // Spot lights
986            if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
987                spot_lights.get_mut(light_entity)
988            {
989                visible_entities.clear();
990
991                // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
992                if !point_light.shadows_enabled {
993                    continue;
994                }
995
996                let view_mask = maybe_view_mask.unwrap_or_default();
997                let light_sphere = Sphere {
998                    center: Vec3A::from(transform.translation()),
999                    radius: point_light.range,
1000                };
1001
1002                visible_entity_query.par_iter_mut().for_each_init(
1003                    || spot_visible_entities_queue.borrow_local_mut(),
1004                    |spot_visible_entities_local_queue,
1005                     (
1006                        entity,
1007                        inherited_visibility,
1008                        mut view_visibility,
1009                        maybe_entity_mask,
1010                        maybe_aabb,
1011                        maybe_transform,
1012                        has_visibility_range,
1013                        has_no_frustum_culling,
1014                    )| {
1015                        if !inherited_visibility.get() {
1016                            return;
1017                        }
1018
1019                        let entity_mask = maybe_entity_mask.unwrap_or_default();
1020                        if !view_mask.intersects(entity_mask) {
1021                            return;
1022                        }
1023                        // Check visibility ranges.
1024                        if has_visibility_range
1025                            && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
1026                                !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
1027                            })
1028                        {
1029                            return;
1030                        }
1031
1032                        if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
1033                            let model_to_world = transform.affine();
1034                            // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
1035                            if !has_no_frustum_culling
1036                                && !light_sphere.intersects_obb(aabb, &model_to_world)
1037                            {
1038                                return;
1039                            }
1040
1041                            if has_no_frustum_culling
1042                                || frustum.intersects_obb(aabb, &model_to_world, true, true)
1043                            {
1044                                view_visibility.set();
1045                                spot_visible_entities_local_queue.push(entity);
1046                            }
1047                        } else {
1048                            view_visibility.set();
1049                            spot_visible_entities_local_queue.push(entity);
1050                        }
1051                    },
1052                );
1053
1054                for entities in spot_visible_entities_queue.iter_mut() {
1055                    visible_entities.append(entities);
1056                }
1057
1058                shrink_entities(visible_entities.deref_mut());
1059            }
1060        }
1061    }
1062}