1extern crate alloc;
6
7use bevy_app::{App, Plugin, PostUpdate, Update};
8use bevy_asset::{AssetApp, AssetEventSystems};
9use bevy_camera::{
10 primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
11 visibility::{
12 CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoCpuCulling,
13 NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems,
14 VisibleEntities, VisibleEntityRanges, VisibleMeshEntities,
15 },
16 Camera, Camera3d, CameraUpdateSystems, RenderTarget, ShadowLodOrigin,
17};
18use bevy_ecs::{entity::EntityHashSet, prelude::*, system::QueryLens};
19#[cfg(feature = "bevy_gizmos")]
20use bevy_gizmos::frustum::FrustumGizmoSystems;
21use bevy_log::warn_once;
22use bevy_math::Vec3A;
23use bevy_mesh::Mesh3d;
24use bevy_reflect::prelude::*;
25use bevy_transform::{components::GlobalTransform, TransformSystems};
26use bevy_utils::Parallel;
27use core::{any::TypeId, mem, ops::DerefMut};
28use smallvec::{smallvec, SmallVec};
29
30pub mod cluster;
31use cluster::assign::assign_objects_to_clusters;
32pub use cluster::ClusteredDecal;
33mod ambient_light;
34pub use ambient_light::{AmbientLight, GlobalAmbientLight};
35use bevy_camera::visibility::SetViewVisibility;
36
37mod probe;
38pub use probe::{
39 automatically_add_parallax_correction_components, AtmosphereEnvironmentMapLight,
40 EnvironmentMapLight, GeneratedEnvironmentMapLight, IrradianceVolume, LightProbe,
41 ParallaxCorrection, Skybox,
42};
43pub mod atmosphere;
44pub use atmosphere::Atmosphere;
45mod volumetric;
46pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};
47pub mod cascade;
48use cascade::build_directional_light_cascades;
49pub use cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades};
50mod point_light;
51pub use point_light::{
52 update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture,
53};
54mod spot_light;
55pub use spot_light::{
56 orthonormalize, spot_light_clip_from_view, spot_light_world_from_view,
57 update_spot_light_frusta, SpotLight, SpotLightTexture,
58};
59mod directional_light;
60pub use directional_light::{
61 update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap,
62 DirectionalLightTexture, SunDisk,
63};
64mod rect_light;
65pub use rect_light::RectLight;
66#[cfg(feature = "bevy_gizmos")]
68pub mod gizmos;
69
70pub mod prelude {
74 #[doc(hidden)]
75 pub use crate::{
76 light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight,
77 GeneratedEnvironmentMapLight, GlobalAmbientLight, LightProbe, PointLight, RectLight,
78 SpotLight,
79 };
80
81 #[doc(hidden)]
82 #[cfg(feature = "bevy_gizmos")]
83 pub use crate::gizmos::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo};
84}
85
86use crate::{
87 atmosphere::{extract_chromatic_phase_textures, ScatteringMedium},
88 cluster::{add_light_probe_and_decal_aabbs, ClusterVisibilityClass, Clusters},
89 directional_light::validate_shadow_map_size,
90 point_light::update_point_light_bounding_spheres,
91 spot_light::update_spot_light_bounding_spheres,
92};
93
94pub mod light_consts {
96 pub mod lumens {
108 pub const LUMENS_PER_LED_WATTS: f32 = 90.0;
110 pub const LUMENS_PER_INCANDESCENT_WATTS: f32 = 13.8;
112 pub const LUMENS_PER_HALOGEN_WATTS: f32 = 19.8;
114 pub const VERY_LARGE_CINEMA_LIGHT: f32 = 1_000_000.0;
118 }
119
120 pub mod lux {
131 pub const MOONLESS_NIGHT: f32 = 0.0001;
133 pub const FULL_MOON_NIGHT: f32 = 0.05;
135 pub const CIVIL_TWILIGHT: f32 = 3.4;
137 pub const LIVING_ROOM: f32 = 50.;
139 pub const HALLWAY: f32 = 80.;
141 pub const DARK_OVERCAST_DAY: f32 = 100.;
143 pub const OFFICE: f32 = 320.;
145 pub const CLEAR_SUNRISE: f32 = 400.;
147 pub const OVERCAST_DAY: f32 = 1000.;
149 pub const AMBIENT_DAYLIGHT: f32 = 10_000.;
152 pub const FULL_DAYLIGHT: f32 = 20_000.;
154 pub const DIRECT_SUNLIGHT: f32 = 100_000.;
156 pub const RAW_SUNLIGHT: f32 = 130_000.;
158 }
159}
160
161#[derive(Default)]
163pub struct LightPlugin;
164
165impl Plugin for LightPlugin {
166 fn build(&self, app: &mut App) {
167 app.init_resource::<GlobalAmbientLight>()
168 .init_resource::<DirectionalLightShadowMap>()
169 .init_resource::<PointLightShadowMap>()
170 .init_asset::<ScatteringMedium>()
171 .register_required_components::<Camera3d, Clusters>()
172 .configure_sets(
173 PostUpdate,
174 SimulationLightSystems::CheckLightVisibility
175 .ambiguous_with(SimulationLightSystems::CheckLightVisibility),
176 )
177 .add_systems(Update, automatically_add_parallax_correction_components)
178 .add_systems(
179 PostUpdate,
180 (
181 validate_shadow_map_size.before(build_directional_light_cascades),
182 assign_objects_to_clusters
183 .in_set(SimulationLightSystems::AssignLightsToClusters)
184 .after(TransformSystems::Propagate)
185 .after(VisibilitySystems::CheckVisibility)
186 .after(CameraUpdateSystems),
187 update_directional_light_frusta
188 .in_set(SimulationLightSystems::UpdateLightFrusta)
189 .after(VisibilitySystems::CheckVisibility)
191 .after(TransformSystems::Propagate)
192 .after(SimulationLightSystems::UpdateDirectionalLightCascades)
193 .ambiguous_with(update_spot_light_frusta),
197 update_point_light_frusta
198 .in_set(SimulationLightSystems::UpdateLightFrusta)
199 .after(TransformSystems::Propagate)
200 .after(SimulationLightSystems::AssignLightsToClusters),
201 #[cfg(feature = "bevy_gizmos")]
202 update_spot_light_frusta
203 .in_set(SimulationLightSystems::UpdateLightFrusta)
204 .before(FrustumGizmoSystems)
205 .after(TransformSystems::Propagate)
206 .after(SimulationLightSystems::AssignLightsToClusters),
207 #[cfg(not(feature = "bevy_gizmos"))]
208 update_spot_light_frusta
209 .in_set(SimulationLightSystems::UpdateLightFrusta)
210 .after(TransformSystems::Propagate)
211 .after(SimulationLightSystems::AssignLightsToClusters),
212 (
213 check_dir_light_mesh_visibility,
214 check_point_light_mesh_visibility,
215 )
216 .in_set(SimulationLightSystems::CheckLightVisibility)
217 .after(VisibilitySystems::CalculateBounds)
218 .after(TransformSystems::Propagate)
219 .after(SimulationLightSystems::UpdateLightFrusta)
220 .after(VisibilitySystems::CheckVisibility)
225 .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
226 (
227 update_point_light_bounding_spheres.after(TransformSystems::Propagate),
228 update_spot_light_bounding_spheres.after(TransformSystems::Propagate),
229 add_light_probe_and_decal_aabbs,
230 )
231 .in_set(SimulationLightSystems::UpdateBounds)
232 .before(VisibilitySystems::UpdateFrusta),
233 build_directional_light_cascades
234 .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
235 .after(TransformSystems::Propagate)
236 .after(CameraUpdateSystems),
237 extract_chromatic_phase_textures.after(AssetEventSystems),
238 ),
239 );
240
241 #[cfg(feature = "bevy_gizmos")]
242 app.add_plugins(gizmos::LightGizmoPlugin);
243 }
244}
245
246pub type WithLight = Or<(
249 With<PointLight>,
250 With<SpotLight>,
251 With<DirectionalLight>,
252 With<RectLight>,
253)>;
254
255#[derive(Debug, Component, Reflect, Default, Clone, PartialEq)]
257#[reflect(Component, Default, Debug, Clone, PartialEq)]
258pub struct NotShadowCaster;
259#[derive(Debug, Component, Reflect, Default, Clone)]
265#[reflect(Component, Default, Debug)]
266pub struct NotShadowReceiver;
267#[derive(Debug, Component, Reflect, Default, Clone)]
275#[reflect(Component, Default, Debug)]
276pub struct TransmittedShadowReceiver;
277
278#[derive(Debug, Component, Reflect, Clone, Copy, PartialEq, Eq, Default)]
284#[reflect(Component, Default, Debug, PartialEq, Clone)]
285pub enum ShadowFilteringMethod {
286 Hardware2x2,
290 #[default]
300 Gaussian,
301 Temporal,
312}
313
314#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
316pub enum SimulationLightSystems {
317 UpdateBounds,
319 AssignLightsToClusters,
321 UpdateDirectionalLightCascades,
323 UpdateLightFrusta,
325 CheckLightVisibility,
329}
330
331pub fn check_dir_light_mesh_visibility(
337 mut commands: Commands,
338 mut directional_lights: Query<
339 (
340 &DirectionalLight,
341 &CascadesFrusta,
342 &mut CascadesVisibleEntities,
343 Option<&RenderLayers>,
344 &ViewVisibility,
345 ),
346 Without<SpotLight>,
347 >,
348 visible_entity_query: Query<
349 (
350 Entity,
351 &InheritedVisibility,
352 Option<&RenderLayers>,
353 Option<&Aabb>,
354 Option<&GlobalTransform>,
355 Has<VisibilityRange>,
356 Has<NoFrustumCulling>,
357 ),
358 (
359 Without<NotShadowCaster>,
360 Without<DirectionalLight>,
361 Without<NoCpuCulling>,
362 With<Mesh3d>,
363 ),
364 >,
365 visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
366 mut defer_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
367 mut view_visible_entities_queue: Local<Parallel<Vec<Vec<Entity>>>>,
368) {
369 let visible_entity_ranges = visible_entity_ranges.as_deref();
370
371 for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in
372 &mut directional_lights
373 {
374 let mut views_to_remove = Vec::new();
375 for (view, cascade_view_entities) in &mut visible_entities.entities {
376 match frusta.frusta.get(view) {
377 Some(view_frusta) => {
378 cascade_view_entities.resize(view_frusta.len(), Default::default());
379 }
380 None => views_to_remove.push(*view),
381 };
382 }
383 for (view, frusta) in &frusta.frusta {
384 visible_entities
385 .entities
386 .entry(*view)
387 .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]);
388 }
389
390 for v in views_to_remove {
391 visible_entities.entities.remove(&v);
392 }
393
394 if !directional_light.shadow_maps_enabled || !light_view_visibility.get() {
396 visible_entities.entities.clear();
397 continue;
398 }
399
400 let view_mask = maybe_view_mask.unwrap_or_default();
401
402 for (view, view_frusta) in &frusta.frusta {
403 visible_entity_query.par_iter().for_each_init(
404 || {
405 let mut entities = view_visible_entities_queue.borrow_local_mut();
406 entities.resize(view_frusta.len(), Vec::default());
407 (defer_visible_entities_queue.borrow_local_mut(), entities)
408 },
409 |(defer_visible_entities_local_queue, view_visible_entities_local_queue),
410 (
411 entity,
412 inherited_visibility,
413 maybe_entity_mask,
414 maybe_aabb,
415 maybe_transform,
416 has_visibility_range,
417 has_no_frustum_culling,
418 )| {
419 if !inherited_visibility.get() {
420 return;
421 }
422
423 let entity_mask = maybe_entity_mask.unwrap_or_default();
424 if !view_mask.intersects(entity_mask) {
425 return;
426 }
427
428 if has_visibility_range
430 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
431 !visible_entity_ranges.entity_is_in_range_of_view(entity, *view)
432 })
433 {
434 return;
435 }
436
437 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
438 let mut visible = false;
439 for (frustum, frustum_visible_entities) in view_frusta
440 .iter()
441 .zip(view_visible_entities_local_queue.iter_mut())
442 {
443 if !has_no_frustum_culling
445 && !frustum.intersects_obb(aabb, &transform.affine(), false, true)
446 {
447 continue;
448 }
449 visible = true;
450
451 frustum_visible_entities.push(entity);
452 }
453 if visible {
454 defer_visible_entities_local_queue.push(entity);
455 }
456 } else {
457 defer_visible_entities_local_queue.push(entity);
458 for frustum_visible_entities in view_visible_entities_local_queue.iter_mut()
459 {
460 frustum_visible_entities.push(entity);
461 }
462 }
463 },
464 );
465 for (view_dest_index, view_dest) in visible_entities
467 .entities
468 .get_mut(view)
469 .unwrap()
470 .iter_mut()
471 .enumerate()
472 {
473 view_dest.entities.clear();
474 for thread_entity_queue in view_visible_entities_queue.iter_mut() {
475 view_dest
476 .entities
477 .append(&mut thread_entity_queue[view_dest_index]);
478 }
479 view_dest.shrink();
480 view_dest.entities.sort_unstable();
481 }
482 }
483 }
484
485 let mut defer_queue = mem::take(defer_visible_entities_queue.deref_mut());
488 commands.queue(move |world: &mut World| {
489 let mut query = world.query::<&mut ViewVisibility>();
490 for entities in defer_queue.iter_mut() {
491 let mut iter = query.iter_many_mut(world, entities.iter());
492 while let Some(mut view_visibility) = iter.fetch_next() {
493 view_visibility.set_visible();
494 }
495 }
496 });
497}
498
499pub fn check_point_light_mesh_visibility(
505 visible_point_lights: Query<&VisibleEntities>,
506 mut point_lights: Query<(
507 &PointLight,
508 &GlobalTransform,
509 &CubemapFrusta,
510 &mut CubemapVisibleEntities,
511 Option<&RenderLayers>,
512 )>,
513 mut spot_lights: Query<(
514 &SpotLight,
515 &GlobalTransform,
516 &Frustum,
517 &mut VisibleMeshEntities,
518 Option<&RenderLayers>,
519 )>,
520 mut visible_entity_query: Query<
521 (
522 Entity,
523 &InheritedVisibility,
524 &mut ViewVisibility,
525 Option<&RenderLayers>,
526 Option<&Aabb>,
527 Option<&GlobalTransform>,
528 Has<VisibilityRange>,
529 Has<NoFrustumCulling>,
530 ),
531 (
532 Without<NotShadowCaster>,
533 Without<DirectionalLight>,
534 Without<NoCpuCulling>,
535 With<Mesh3d>,
536 ),
537 >,
538 mut camera_query: Query<(Entity, &RenderTarget), With<Camera>>,
539 mut shadow_lod_origin_query: Query<Entity, With<ShadowLodOrigin>>,
540 mut point_and_spot_light_query: Query<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
541 visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
542 mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
543 mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
544 mut checked_lights: Local<EntityHashSet>,
545) {
546 checked_lights.clear();
547
548 let shadow_lod_origin = get_shadow_lod_origin(
549 camera_query.transmute_lens_filtered(),
550 shadow_lod_origin_query.transmute_lens_filtered(),
551 point_and_spot_light_query.transmute_lens_filtered(),
552 );
553
554 let visible_entity_ranges = visible_entity_ranges.as_deref();
555 for visible_lights in &visible_point_lights {
556 for &light_entity in visible_lights.get(TypeId::of::<ClusterVisibilityClass>()) {
557 if !checked_lights.insert(light_entity) {
558 continue;
559 }
560
561 if let Ok((
563 point_light,
564 transform,
565 cubemap_frusta,
566 mut cubemap_visible_entities,
567 maybe_view_mask,
568 )) = point_lights.get_mut(light_entity)
569 {
570 if !point_light.shadow_maps_enabled {
572 continue;
573 }
574
575 let view_mask = maybe_view_mask.unwrap_or_default();
576 let light_sphere = Sphere {
577 center: Vec3A::from(transform.translation()),
578 radius: point_light.range,
579 };
580
581 visible_entity_query.par_iter_mut().for_each_init(
582 || cubemap_visible_entities_queue.borrow_local_mut(),
583 |cubemap_visible_entities_local_queue,
584 (
585 entity,
586 inherited_visibility,
587 mut view_visibility,
588 maybe_entity_mask,
589 maybe_aabb,
590 maybe_transform,
591 has_visibility_range,
592 has_no_frustum_culling,
593 )| {
594 if !inherited_visibility.get() {
595 return;
596 }
597 let entity_mask = maybe_entity_mask.unwrap_or_default();
598 if !view_mask.intersects(entity_mask) {
599 return;
600 }
601 if has_visibility_range
602 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
603 shadow_lod_origin.is_none_or(|shadow_lod_origin| {
604 !visible_entity_ranges
605 .entity_is_in_range_of_view(entity, shadow_lod_origin)
606 })
607 })
608 {
609 return;
610 }
611
612 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
614 let model_to_world = transform.affine();
615 if !has_no_frustum_culling
617 && !light_sphere.intersects_obb(aabb, &model_to_world)
618 {
619 return;
620 }
621
622 for (frustum, visible_entities) in cubemap_frusta
623 .iter()
624 .zip(cubemap_visible_entities_local_queue.iter_mut())
625 {
626 if has_no_frustum_culling
627 || frustum.intersects_obb(aabb, &model_to_world, true, true)
628 {
629 view_visibility.set_visible();
630 visible_entities.push(entity);
631 }
632 }
633 } else {
634 view_visibility.set_visible();
635 for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
636 {
637 visible_entities.push(entity);
638 }
639 }
640 },
641 );
642
643 for (view_dest_index, view_dest) in cubemap_visible_entities.iter_mut().enumerate()
645 {
646 view_dest.entities.clear();
647 for thread_entity_queue in cubemap_visible_entities_queue.iter_mut() {
648 view_dest
649 .entities
650 .append(&mut thread_entity_queue[view_dest_index]);
651 }
652 view_dest.shrink();
653 view_dest.entities.sort_unstable();
654 }
655 }
656
657 if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
659 spot_lights.get_mut(light_entity)
660 {
661 if !point_light.shadow_maps_enabled {
663 continue;
664 }
665
666 let view_mask = maybe_view_mask.unwrap_or_default();
667 let light_sphere = Sphere {
668 center: Vec3A::from(transform.translation()),
669 radius: point_light.range,
670 };
671
672 visible_entity_query.par_iter_mut().for_each_init(
673 || spot_visible_entities_queue.borrow_local_mut(),
674 |spot_visible_entities_local_queue,
675 (
676 entity,
677 inherited_visibility,
678 mut view_visibility,
679 maybe_entity_mask,
680 maybe_aabb,
681 maybe_transform,
682 has_visibility_range,
683 has_no_frustum_culling,
684 )| {
685 if !inherited_visibility.get() {
686 return;
687 }
688
689 let entity_mask = maybe_entity_mask.unwrap_or_default();
690 if !view_mask.intersects(entity_mask) {
691 return;
692 }
693 if has_visibility_range
695 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
696 shadow_lod_origin.is_none_or(|shadow_lod_origin| {
697 !visible_entity_ranges
698 .entity_is_in_range_of_view(entity, shadow_lod_origin)
699 })
700 })
701 {
702 return;
703 }
704
705 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
706 let model_to_world = transform.affine();
707 if !has_no_frustum_culling
709 && !light_sphere.intersects_obb(aabb, &model_to_world)
710 {
711 return;
712 }
713
714 if has_no_frustum_culling
715 || frustum.intersects_obb(aabb, &model_to_world, true, true)
716 {
717 view_visibility.set_visible();
718 spot_visible_entities_local_queue.push(entity);
719 }
720 } else {
721 view_visibility.set_visible();
722 spot_visible_entities_local_queue.push(entity);
723 }
724 },
725 );
726
727 visible_entities.entities.clear();
728 for thread_entity_queue in spot_visible_entities_queue.iter_mut() {
729 visible_entities.entities.append(thread_entity_queue);
730 }
731 visible_entities.shrink();
732 visible_entities.entities.sort_unstable();
733 }
734 }
735 }
736}
737
738pub fn get_shadow_lod_origin(
748 mut camera_query: QueryLens<(Entity, &RenderTarget), With<Camera>>,
749 mut shadow_lod_origin_query: QueryLens<Entity, With<ShadowLodOrigin>>,
750 mut lights_query: QueryLens<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
751) -> Option<Entity> {
752 let (camera_query, shadow_lod_origin_query) =
753 (camera_query.query(), shadow_lod_origin_query.query());
754
755 let mut entities: SmallVec<[Entity; 4]> = smallvec![];
756 entities.extend(shadow_lod_origin_query.iter());
757 if let Some(lod_origin) = entities.iter().min() {
758 return Some(*lod_origin);
759 }
760
761 entities.extend(
762 camera_query
763 .iter()
764 .filter_map(|(main_entity, render_target)| match *render_target {
765 RenderTarget::Window(_) => Some(main_entity),
766 _ => None,
767 }),
768 );
769 if let Some(lod_origin) = entities.iter().min() {
770 return Some(*lod_origin);
771 };
772
773 entities.extend(camera_query.iter().map(|(main_entity, _)| main_entity));
774 if let Some(lod_origin) = entities.iter().min() {
775 if !lights_query.query().is_empty() {
776 warn_once!(
777 "Point lights and/or spot lights are present, but no entity has \
778 `ShadowLodOrigin`, and no camera that renders to the window has been found. \
779 Consider using the `ShadowLodOrigin` component to set a LOD origin."
780 );
781 }
782
783 return Some(*lod_origin);
784 };
785
786 None
787}