1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
2
3use bevy_app::{App, Plugin, PostUpdate};
4use bevy_camera::{
5 primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
6 visibility::{
7 CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling,
8 PreviousVisibleEntities, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems,
9 VisibleEntityRanges, VisibleMeshEntities,
10 },
11 CameraUpdateSystems,
12};
13use bevy_ecs::{entity::EntityHashSet, prelude::*};
14use bevy_math::Vec3A;
15use bevy_mesh::Mesh3d;
16use bevy_reflect::prelude::*;
17use bevy_transform::{components::GlobalTransform, TransformSystems};
18use bevy_utils::Parallel;
19use core::ops::DerefMut;
20
21pub mod cluster;
22pub use cluster::ClusteredDecal;
23use cluster::{
24 add_clusters, assign::assign_objects_to_clusters, GlobalVisibleClusterableObjects,
25 VisibleClusterableObjects,
26};
27mod ambient_light;
28pub use ambient_light::AmbientLight;
29mod probe;
30pub use probe::{
31 AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight,
32 IrradianceVolume, LightProbe,
33};
34mod volumetric;
35pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};
36pub mod cascade;
37use cascade::{build_directional_light_cascades, clear_directional_light_cascades};
38pub use cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades};
39mod point_light;
40pub use point_light::{
41 update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture,
42};
43mod spot_light;
44pub use spot_light::{
45 orthonormalize, spot_light_clip_from_view, spot_light_world_from_view,
46 update_spot_light_frusta, SpotLight, SpotLightTexture,
47};
48mod directional_light;
49pub use directional_light::{
50 update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap,
51 DirectionalLightTexture, SunDisk,
52};
53
54pub mod prelude {
58 #[doc(hidden)]
59 pub use crate::{
60 light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight,
61 GeneratedEnvironmentMapLight, LightProbe, PointLight, SpotLight,
62 };
63}
64
65use crate::directional_light::validate_shadow_map_size;
66
67pub mod light_consts {
69 pub mod lumens {
81 pub const LUMENS_PER_LED_WATTS: f32 = 90.0;
82 pub const LUMENS_PER_INCANDESCENT_WATTS: f32 = 13.8;
83 pub const LUMENS_PER_HALOGEN_WATTS: f32 = 19.8;
84 pub const VERY_LARGE_CINEMA_LIGHT: f32 = 1_000_000.0;
88 }
89
90 pub mod lux {
101 pub const MOONLESS_NIGHT: f32 = 0.0001;
103 pub const FULL_MOON_NIGHT: f32 = 0.05;
105 pub const CIVIL_TWILIGHT: f32 = 3.4;
107 pub const LIVING_ROOM: f32 = 50.;
109 pub const HALLWAY: f32 = 80.;
111 pub const DARK_OVERCAST_DAY: f32 = 100.;
113 pub const OFFICE: f32 = 320.;
115 pub const CLEAR_SUNRISE: f32 = 400.;
117 pub const OVERCAST_DAY: f32 = 1000.;
119 pub const AMBIENT_DAYLIGHT: f32 = 10_000.;
121 pub const FULL_DAYLIGHT: f32 = 20_000.;
123 pub const DIRECT_SUNLIGHT: f32 = 100_000.;
125 pub const RAW_SUNLIGHT: f32 = 130_000.;
127 }
128}
129
130#[derive(Default)]
131pub struct LightPlugin;
132
133impl Plugin for LightPlugin {
134 fn build(&self, app: &mut App) {
135 app.init_resource::<GlobalVisibleClusterableObjects>()
136 .init_resource::<AmbientLight>()
137 .init_resource::<DirectionalLightShadowMap>()
138 .init_resource::<PointLightShadowMap>()
139 .configure_sets(
140 PostUpdate,
141 SimulationLightSystems::UpdateDirectionalLightCascades
142 .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades),
143 )
144 .configure_sets(
145 PostUpdate,
146 SimulationLightSystems::CheckLightVisibility
147 .ambiguous_with(SimulationLightSystems::CheckLightVisibility),
148 )
149 .add_systems(
150 PostUpdate,
151 (
152 validate_shadow_map_size.before(build_directional_light_cascades),
153 add_clusters
154 .in_set(SimulationLightSystems::AddClusters)
155 .after(CameraUpdateSystems),
156 assign_objects_to_clusters
157 .in_set(SimulationLightSystems::AssignLightsToClusters)
158 .after(TransformSystems::Propagate)
159 .after(VisibilitySystems::CheckVisibility)
160 .after(CameraUpdateSystems),
161 clear_directional_light_cascades
162 .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
163 .after(TransformSystems::Propagate)
164 .after(CameraUpdateSystems),
165 update_directional_light_frusta
166 .in_set(SimulationLightSystems::UpdateLightFrusta)
167 .after(VisibilitySystems::CheckVisibility)
169 .after(TransformSystems::Propagate)
170 .after(SimulationLightSystems::UpdateDirectionalLightCascades)
171 .ambiguous_with(update_spot_light_frusta),
175 update_point_light_frusta
176 .in_set(SimulationLightSystems::UpdateLightFrusta)
177 .after(TransformSystems::Propagate)
178 .after(SimulationLightSystems::AssignLightsToClusters),
179 update_spot_light_frusta
180 .in_set(SimulationLightSystems::UpdateLightFrusta)
181 .after(TransformSystems::Propagate)
182 .after(SimulationLightSystems::AssignLightsToClusters),
183 (
184 check_dir_light_mesh_visibility,
185 check_point_light_mesh_visibility,
186 )
187 .in_set(SimulationLightSystems::CheckLightVisibility)
188 .after(VisibilitySystems::CalculateBounds)
189 .after(TransformSystems::Propagate)
190 .after(SimulationLightSystems::UpdateLightFrusta)
191 .after(VisibilitySystems::CheckVisibility)
195 .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
196 build_directional_light_cascades
197 .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
198 .after(clear_directional_light_cascades),
199 ),
200 );
201 }
202}
203
204pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
207
208#[derive(Debug, Component, Reflect, Default, Clone, PartialEq)]
210#[reflect(Component, Default, Debug, Clone, PartialEq)]
211pub struct NotShadowCaster;
212#[derive(Debug, Component, Reflect, Default)]
218#[reflect(Component, Default, Debug)]
219pub struct NotShadowReceiver;
220#[derive(Debug, Component, Reflect, Default)]
228#[reflect(Component, Default, Debug)]
229pub struct TransmittedShadowReceiver;
230
231#[derive(Debug, Component, Reflect, Clone, Copy, PartialEq, Eq, Default)]
237#[reflect(Component, Default, Debug, PartialEq, Clone)]
238pub enum ShadowFilteringMethod {
239 Hardware2x2,
243 #[default]
253 Gaussian,
254 Temporal,
265}
266
267#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
269pub enum SimulationLightSystems {
270 AddClusters,
271 AssignLightsToClusters,
272 UpdateDirectionalLightCascades,
276 UpdateLightFrusta,
277 CheckLightVisibility,
281}
282
283fn shrink_entities(visible_entities: &mut Vec<Entity>) {
284 let capacity = visible_entities.capacity();
286 let reserved = capacity
287 .checked_div(visible_entities.len())
288 .map_or(0, |reserve| {
289 if reserve > 2 {
290 capacity / (reserve / 2)
291 } else {
292 capacity
293 }
294 });
295
296 visible_entities.shrink_to(reserved);
297}
298
299pub fn check_dir_light_mesh_visibility(
300 mut commands: Commands,
301 mut directional_lights: Query<
302 (
303 &DirectionalLight,
304 &CascadesFrusta,
305 &mut CascadesVisibleEntities,
306 Option<&RenderLayers>,
307 &ViewVisibility,
308 ),
309 Without<SpotLight>,
310 >,
311 visible_entity_query: Query<
312 (
313 Entity,
314 &InheritedVisibility,
315 Option<&RenderLayers>,
316 Option<&Aabb>,
317 Option<&GlobalTransform>,
318 Has<VisibilityRange>,
319 Has<NoFrustumCulling>,
320 ),
321 (
322 Without<NotShadowCaster>,
323 Without<DirectionalLight>,
324 With<Mesh3d>,
325 ),
326 >,
327 visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
328 mut defer_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
329 mut view_visible_entities_queue: Local<Parallel<Vec<Vec<Entity>>>>,
330) {
331 let visible_entity_ranges = visible_entity_ranges.as_deref();
332
333 for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in
334 &mut directional_lights
335 {
336 let mut views_to_remove = Vec::new();
337 for (view, cascade_view_entities) in &mut visible_entities.entities {
338 match frusta.frusta.get(view) {
339 Some(view_frusta) => {
340 cascade_view_entities.resize(view_frusta.len(), Default::default());
341 cascade_view_entities.iter_mut().for_each(|x| x.clear());
342 }
343 None => views_to_remove.push(*view),
344 };
345 }
346 for (view, frusta) in &frusta.frusta {
347 visible_entities
348 .entities
349 .entry(*view)
350 .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]);
351 }
352
353 for v in views_to_remove {
354 visible_entities.entities.remove(&v);
355 }
356
357 if !directional_light.shadows_enabled || !light_view_visibility.get() {
359 continue;
360 }
361
362 let view_mask = maybe_view_mask.unwrap_or_default();
363
364 for (view, view_frusta) in &frusta.frusta {
365 visible_entity_query.par_iter().for_each_init(
366 || {
367 let mut entities = view_visible_entities_queue.borrow_local_mut();
368 entities.resize(view_frusta.len(), Vec::default());
369 (defer_visible_entities_queue.borrow_local_mut(), entities)
370 },
371 |(defer_visible_entities_local_queue, view_visible_entities_local_queue),
372 (
373 entity,
374 inherited_visibility,
375 maybe_entity_mask,
376 maybe_aabb,
377 maybe_transform,
378 has_visibility_range,
379 has_no_frustum_culling,
380 )| {
381 if !inherited_visibility.get() {
382 return;
383 }
384
385 let entity_mask = maybe_entity_mask.unwrap_or_default();
386 if !view_mask.intersects(entity_mask) {
387 return;
388 }
389
390 if has_visibility_range
392 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
393 !visible_entity_ranges.entity_is_in_range_of_view(entity, *view)
394 })
395 {
396 return;
397 }
398
399 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
400 let mut visible = false;
401 for (frustum, frustum_visible_entities) in view_frusta
402 .iter()
403 .zip(view_visible_entities_local_queue.iter_mut())
404 {
405 if !has_no_frustum_culling
407 && !frustum.intersects_obb(aabb, &transform.affine(), false, true)
408 {
409 continue;
410 }
411 visible = true;
412
413 frustum_visible_entities.push(entity);
414 }
415 if visible {
416 defer_visible_entities_local_queue.push(entity);
417 }
418 } else {
419 defer_visible_entities_local_queue.push(entity);
420 for frustum_visible_entities in view_visible_entities_local_queue.iter_mut()
421 {
422 frustum_visible_entities.push(entity);
423 }
424 }
425 },
426 );
427 for entities in view_visible_entities_queue.iter_mut() {
429 visible_entities
430 .entities
431 .get_mut(view)
432 .unwrap()
433 .iter_mut()
434 .zip(entities.iter_mut())
435 .for_each(|(dst, source)| {
436 dst.append(source);
437 });
438 }
439 }
440
441 for (_, cascade_view_entities) in &mut visible_entities.entities {
442 cascade_view_entities
443 .iter_mut()
444 .map(DerefMut::deref_mut)
445 .for_each(shrink_entities);
446 }
447 }
448
449 let mut defer_queue = core::mem::take(defer_visible_entities_queue.deref_mut());
452 commands.queue(move |world: &mut World| {
453 world.resource_scope::<PreviousVisibleEntities, _>(
454 |world, mut previous_visible_entities| {
455 let mut query = world.query::<(Entity, &mut ViewVisibility)>();
456 for entities in defer_queue.iter_mut() {
457 let mut iter = query.iter_many_mut(world, entities.iter());
458 while let Some((entity, mut view_visibility)) = iter.fetch_next() {
459 if !**view_visibility {
460 view_visibility.set();
461 }
462
463 previous_visible_entities.remove(&entity);
466 }
467 }
468 },
469 );
470 });
471}
472
473pub fn check_point_light_mesh_visibility(
474 visible_point_lights: Query<&VisibleClusterableObjects>,
475 mut point_lights: Query<(
476 &PointLight,
477 &GlobalTransform,
478 &CubemapFrusta,
479 &mut CubemapVisibleEntities,
480 Option<&RenderLayers>,
481 )>,
482 mut spot_lights: Query<(
483 &SpotLight,
484 &GlobalTransform,
485 &Frustum,
486 &mut VisibleMeshEntities,
487 Option<&RenderLayers>,
488 )>,
489 mut visible_entity_query: Query<
490 (
491 Entity,
492 &InheritedVisibility,
493 &mut ViewVisibility,
494 Option<&RenderLayers>,
495 Option<&Aabb>,
496 Option<&GlobalTransform>,
497 Has<VisibilityRange>,
498 Has<NoFrustumCulling>,
499 ),
500 (
501 Without<NotShadowCaster>,
502 Without<DirectionalLight>,
503 With<Mesh3d>,
504 ),
505 >,
506 visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
507 mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
508 mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
509 mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
510 mut checked_lights: Local<EntityHashSet>,
511) {
512 checked_lights.clear();
513
514 let visible_entity_ranges = visible_entity_ranges.as_deref();
515 for visible_lights in &visible_point_lights {
516 for light_entity in visible_lights.entities.iter().copied() {
517 if !checked_lights.insert(light_entity) {
518 continue;
519 }
520
521 if let Ok((
523 point_light,
524 transform,
525 cubemap_frusta,
526 mut cubemap_visible_entities,
527 maybe_view_mask,
528 )) = point_lights.get_mut(light_entity)
529 {
530 for visible_entities in cubemap_visible_entities.iter_mut() {
531 visible_entities.entities.clear();
532 }
533
534 if !point_light.shadows_enabled {
536 continue;
537 }
538
539 let view_mask = maybe_view_mask.unwrap_or_default();
540 let light_sphere = Sphere {
541 center: Vec3A::from(transform.translation()),
542 radius: point_light.range,
543 };
544
545 visible_entity_query.par_iter_mut().for_each_init(
546 || cubemap_visible_entities_queue.borrow_local_mut(),
547 |cubemap_visible_entities_local_queue,
548 (
549 entity,
550 inherited_visibility,
551 mut view_visibility,
552 maybe_entity_mask,
553 maybe_aabb,
554 maybe_transform,
555 has_visibility_range,
556 has_no_frustum_culling,
557 )| {
558 if !inherited_visibility.get() {
559 return;
560 }
561 let entity_mask = maybe_entity_mask.unwrap_or_default();
562 if !view_mask.intersects(entity_mask) {
563 return;
564 }
565 if has_visibility_range
566 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
567 !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
568 })
569 {
570 return;
571 }
572
573 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
575 let model_to_world = transform.affine();
576 if !has_no_frustum_culling
578 && !light_sphere.intersects_obb(aabb, &model_to_world)
579 {
580 return;
581 }
582
583 for (frustum, visible_entities) in cubemap_frusta
584 .iter()
585 .zip(cubemap_visible_entities_local_queue.iter_mut())
586 {
587 if has_no_frustum_culling
588 || frustum.intersects_obb(aabb, &model_to_world, true, true)
589 {
590 if !**view_visibility {
591 view_visibility.set();
592 }
593 visible_entities.push(entity);
594 }
595 }
596 } else {
597 if !**view_visibility {
598 view_visibility.set();
599 }
600 for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
601 {
602 visible_entities.push(entity);
603 }
604 }
605 },
606 );
607
608 for entities in cubemap_visible_entities_queue.iter_mut() {
609 for (dst, source) in
610 cubemap_visible_entities.iter_mut().zip(entities.iter_mut())
611 {
612 for entity in source.iter() {
615 previous_visible_entities.remove(entity);
616 }
617
618 dst.entities.append(source);
619 }
620 }
621
622 for visible_entities in cubemap_visible_entities.iter_mut() {
623 shrink_entities(visible_entities);
624 }
625 }
626
627 if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
629 spot_lights.get_mut(light_entity)
630 {
631 visible_entities.clear();
632
633 if !point_light.shadows_enabled {
635 continue;
636 }
637
638 let view_mask = maybe_view_mask.unwrap_or_default();
639 let light_sphere = Sphere {
640 center: Vec3A::from(transform.translation()),
641 radius: point_light.range,
642 };
643
644 visible_entity_query.par_iter_mut().for_each_init(
645 || spot_visible_entities_queue.borrow_local_mut(),
646 |spot_visible_entities_local_queue,
647 (
648 entity,
649 inherited_visibility,
650 mut view_visibility,
651 maybe_entity_mask,
652 maybe_aabb,
653 maybe_transform,
654 has_visibility_range,
655 has_no_frustum_culling,
656 )| {
657 if !inherited_visibility.get() {
658 return;
659 }
660
661 let entity_mask = maybe_entity_mask.unwrap_or_default();
662 if !view_mask.intersects(entity_mask) {
663 return;
664 }
665 if has_visibility_range
667 && visible_entity_ranges.is_some_and(|visible_entity_ranges| {
668 !visible_entity_ranges.entity_is_in_range_of_any_view(entity)
669 })
670 {
671 return;
672 }
673
674 if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
675 let model_to_world = transform.affine();
676 if !has_no_frustum_culling
678 && !light_sphere.intersects_obb(aabb, &model_to_world)
679 {
680 return;
681 }
682
683 if has_no_frustum_culling
684 || frustum.intersects_obb(aabb, &model_to_world, true, true)
685 {
686 if !**view_visibility {
687 view_visibility.set();
688 }
689 spot_visible_entities_local_queue.push(entity);
690 }
691 } else {
692 if !**view_visibility {
693 view_visibility.set();
694 }
695 spot_visible_entities_local_queue.push(entity);
696 }
697 },
698 );
699
700 for entities in spot_visible_entities_queue.iter_mut() {
701 visible_entities.append(entities);
702
703 for entity in entities {
706 previous_visible_entities.remove(entity);
707 }
708 }
709
710 shrink_entities(visible_entities.deref_mut());
711 }
712 }
713 }
714}