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