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
35pub mod light_consts {
37 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 pub mod lux {
65 pub const MOONLESS_NIGHT: f32 = 0.0001;
67 pub const FULL_MOON_NIGHT: f32 = 0.05;
69 pub const CIVIL_TWILIGHT: f32 = 3.4;
71 pub const LIVING_ROOM: f32 = 50.;
73 pub const HALLWAY: f32 = 80.;
75 pub const DARK_OVERCAST_DAY: f32 = 100.;
77 pub const OFFICE: f32 = 320.;
79 pub const CLEAR_SUNRISE: f32 = 400.;
81 pub const OVERCAST_DAY: f32 = 1000.;
83 pub const AMBIENT_DAYLIGHT: f32 = 10_000.;
85 pub const FULL_DAYLIGHT: f32 = 20_000.;
87 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
104pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
107
108#[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#[derive(Component, Clone, Debug, Reflect)]
135#[reflect(Component, Default, Debug)]
136pub struct CascadeShadowConfig {
137 pub bounds: Vec<f32>,
139 pub overlap_proportion: f32,
141 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
168pub struct CascadeShadowConfigBuilder {
170 pub num_cascades: usize,
183 pub minimum_distance: f32,
192 pub maximum_distance: f32,
195 pub first_cascade_far_bound: f32,
199 pub overlap_proportion: f32,
203}
204
205impl CascadeShadowConfigBuilder {
206 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 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 pub(crate) cascades: EntityHashMap<Vec<Cascade>>,
283}
284
285#[derive(Clone, Debug, Default, Reflect)]
286pub struct Cascade {
287 pub(crate) world_from_cascade: Mat4,
289 pub(crate) clip_from_cascade: Mat4,
291 pub(crate) clip_from_world: Mat4,
295 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 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 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
374fn 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 let cascade_diameter = (frustum_corners[0] - frustum_corners[6])
400 .length()
401 .max((frustum_corners[4] - frustum_corners[6]).length())
402 .ceil();
403
404 let cascade_texel_size = cascade_diameter / cascade_texture_size;
408 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 max.z,
415 );
416
417 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 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#[derive(Debug, Component, Reflect, Default)]
448#[reflect(Component, Default, Debug)]
449pub struct NotShadowCaster;
450#[derive(Debug, Component, Reflect, Default)]
456#[reflect(Component, Default, Debug)]
457pub struct NotShadowReceiver;
458#[derive(Debug, Component, Reflect, Default)]
466#[reflect(Component, Default, Debug)]
467pub struct TransmittedShadowReceiver;
468
469#[derive(Debug, Component, ExtractComponent, Reflect, Clone, Copy, PartialEq, Eq, Default)]
475#[reflect(Component, Default, Debug, PartialEq)]
476pub enum ShadowFilteringMethod {
477 Hardware2x2,
481 #[default]
491 Gaussian,
492 Temporal,
504}
505
506#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
508pub enum SimulationLightSystems {
509 AddClusters,
510 AssignLightsToClusters,
511 UpdateDirectionalLightCascades,
515 UpdateLightFrusta,
516 CheckLightVisibility,
520}
521
522pub(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) .then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) .then_with(|| entity_1.cmp(entity_2)) }
539
540pub fn update_directional_light_frusta(
541 mut views: Query<
542 (
543 &Cascades,
544 &DirectionalLight,
545 &ViewVisibility,
546 &mut CascadesFrusta,
547 ),
548 (
549 Without<Camera>,
551 ),
552 >,
553) {
554 for (cascades, directional_light, visibility, mut frusta) in &mut views {
555 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
578pub 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 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 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 if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) {
641 continue;
642 }
643
644 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 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 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 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 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 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 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 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 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 let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
944 let model_to_world = transform.affine();
945 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 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 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 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 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}