bevy_light/
directional_light.rs

1use bevy_asset::Handle;
2use bevy_camera::{
3    primitives::{CascadesFrusta, Frustum},
4    visibility::{self, CascadesVisibleEntities, ViewVisibility, Visibility, VisibilityClass},
5    Camera,
6};
7use bevy_color::Color;
8use bevy_ecs::prelude::*;
9use bevy_image::Image;
10use bevy_reflect::prelude::*;
11use bevy_transform::components::Transform;
12use tracing::warn;
13
14use super::{
15    cascade::CascadeShadowConfig, cluster::ClusterVisibilityClass, light_consts, Cascades,
16};
17
18/// A Directional light.
19///
20/// Directional lights don't exist in reality but they are a good
21/// approximation for light sources VERY far away, like the sun or
22/// the moon.
23///
24/// The light shines along the forward direction of the entity's transform. With a default transform
25/// this would be along the negative-Z axis.
26///
27/// Valid values for `illuminance` are:
28///
29/// | Illuminance (lux) | Surfaces illuminated by                        |
30/// |-------------------|------------------------------------------------|
31/// | 0.0001            | Moonless, overcast night sky (starlight)       |
32/// | 0.002             | Moonless clear night sky with airglow          |
33/// | 0.05–0.3          | Full moon on a clear night                     |
34/// | 3.4               | Dark limit of civil twilight under a clear sky |
35/// | 20–50             | Public areas with dark surroundings            |
36/// | 50                | Family living room lights                      |
37/// | 80                | Office building hallway/toilet lighting        |
38/// | 100               | Very dark overcast day                         |
39/// | 150               | Train station platforms                        |
40/// | 320–500           | Office lighting                                |
41/// | 400               | Sunrise or sunset on a clear day.              |
42/// | 1000              | Overcast day; typical TV studio lighting       |
43/// | 10,000–25,000     | Full daylight (not direct sun)                 |
44/// | 32,000–100,000    | Direct sunlight                                |
45///
46/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux)
47///
48/// ## Shadows
49///
50/// To enable shadows, set the `shadows_enabled` property to `true`.
51///
52/// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf).
53///
54/// To modify the cascade setup, such as the number of cascades or the maximum shadow distance,
55/// change the [`CascadeShadowConfig`] component of the entity with the [`DirectionalLight`].
56///
57/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource.
58#[derive(Component, Debug, Clone, Copy, Reflect)]
59#[reflect(Component, Default, Debug, Clone)]
60#[require(
61    Cascades,
62    CascadesFrusta,
63    CascadeShadowConfig,
64    CascadesVisibleEntities,
65    Transform,
66    Visibility,
67    VisibilityClass
68)]
69#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
70pub struct DirectionalLight {
71    /// The color of the light.
72    ///
73    /// By default, this is white.
74    pub color: Color,
75
76    /// Illuminance in lux (lumens per square meter), representing the amount of
77    /// light projected onto surfaces by this light source. Lux is used here
78    /// instead of lumens because a directional light illuminates all surfaces
79    /// more-or-less the same way (depending on the angle of incidence). Lumens
80    /// can only be specified for light sources which emit light from a specific
81    /// area.
82    pub illuminance: f32,
83
84    /// Whether this light casts shadows.
85    ///
86    /// Note that shadows are rather expensive and become more so with every
87    /// light that casts them. In general, it's best to aggressively limit the
88    /// number of lights with shadows enabled to one or two at most.
89    pub shadows_enabled: bool,
90
91    /// Whether soft shadows are enabled, and if so, the size of the light.
92    ///
93    /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
94    /// cause shadows to become blurrier (i.e. their penumbra increases in
95    /// radius) as they extend away from objects. The blurriness of the shadow
96    /// depends on the size of the light; larger lights result in larger
97    /// penumbras and therefore blurrier shadows.
98    ///
99    /// Currently, soft shadows are rather noisy if not using the temporal mode.
100    /// If you enable soft shadows, consider choosing
101    /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
102    /// (TAA) to smooth the noise out over time.
103    ///
104    /// Note that soft shadows are significantly more expensive to render than
105    /// hard shadows.
106    ///
107    /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
108    #[cfg(feature = "experimental_pbr_pcss")]
109    pub soft_shadow_size: Option<f32>,
110
111    /// Whether this directional light contributes diffuse lighting to meshes
112    /// with lightmaps.
113    ///
114    /// Set this to false if your lightmap baking tool bakes the direct diffuse
115    /// light from this directional light into the lightmaps in order to avoid
116    /// counting the radiance from this light twice. Note that the specular
117    /// portion of the light is always considered, because Bevy currently has no
118    /// means to bake specular light.
119    ///
120    /// By default, this is set to true.
121    pub affects_lightmapped_mesh_diffuse: bool,
122
123    /// A value that adjusts the tradeoff between self-shadowing artifacts and
124    /// proximity of shadows to their casters.
125    ///
126    /// This value frequently must be tuned to the specific scene; this is
127    /// normal and a well-known part of the shadow mapping workflow. If set too
128    /// low, unsightly shadow patterns appear on objects not in shadow as
129    /// objects incorrectly cast shadows on themselves, known as *shadow acne*.
130    /// If set too high, shadows detach from the objects casting them and seem
131    /// to "fly" off the objects, known as *Peter Panning*.
132    pub shadow_depth_bias: f32,
133
134    /// A bias applied along the direction of the fragment's surface normal. It
135    /// is scaled to the shadow map's texel size so that it is automatically
136    /// adjusted to the orthographic projection.
137    pub shadow_normal_bias: f32,
138}
139
140impl Default for DirectionalLight {
141    fn default() -> Self {
142        DirectionalLight {
143            color: Color::WHITE,
144            illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
145            shadows_enabled: false,
146            shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
147            shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
148            affects_lightmapped_mesh_diffuse: true,
149            #[cfg(feature = "experimental_pbr_pcss")]
150            soft_shadow_size: None,
151        }
152    }
153}
154
155impl DirectionalLight {
156    pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
157    pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
158}
159
160/// Add to a [`DirectionalLight`] to add a light texture effect.
161/// A texture mask is applied to the light source to modulate its intensity,  
162/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs.
163#[derive(Clone, Component, Debug, Reflect)]
164#[reflect(Component, Debug)]
165#[require(DirectionalLight)]
166pub struct DirectionalLightTexture {
167    /// The texture image. Only the R channel is read.
168    pub image: Handle<Image>,
169    /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation
170    pub tiled: bool,
171}
172
173/// Controls the resolution of [`DirectionalLight`] and [`SpotLight`](crate::SpotLight) shadow maps.
174///
175/// ```
176/// # use bevy_app::prelude::*;
177/// # use bevy_light::DirectionalLightShadowMap;
178/// App::new()
179///     .insert_resource(DirectionalLightShadowMap { size: 4096 });
180/// ```
181#[derive(Resource, Clone, Debug, Reflect)]
182#[reflect(Resource, Debug, Default, Clone)]
183pub struct DirectionalLightShadowMap {
184    // The width and height of each cascade.
185    ///
186    /// Must be a power of two to avoid unstable cascade positioning.
187    ///
188    /// Defaults to `2048`.
189    pub size: usize,
190}
191
192impl Default for DirectionalLightShadowMap {
193    fn default() -> Self {
194        Self { size: 2048 }
195    }
196}
197
198pub fn validate_shadow_map_size(mut shadow_map: ResMut<DirectionalLightShadowMap>) {
199    if shadow_map.is_changed() && !shadow_map.size.is_power_of_two() {
200        let new_size = shadow_map.size.next_power_of_two();
201        warn!("Non-power-of-two DirectionalLightShadowMap sizes are not supported, correcting {} to {new_size}", shadow_map.size);
202        shadow_map.size = new_size;
203    }
204}
205
206pub fn update_directional_light_frusta(
207    mut views: Query<
208        (
209            &Cascades,
210            &DirectionalLight,
211            &ViewVisibility,
212            &mut CascadesFrusta,
213        ),
214        (
215            // Prevents this query from conflicting with camera queries.
216            Without<Camera>,
217        ),
218    >,
219) {
220    for (cascades, directional_light, visibility, mut frusta) in &mut views {
221        // The frustum is used for culling meshes to the light for shadow mapping
222        // so if shadow mapping is disabled for this light, then the frustum is
223        // not needed.
224        if !directional_light.shadows_enabled || !visibility.get() {
225            continue;
226        }
227
228        frusta.frusta = cascades
229            .cascades
230            .iter()
231            .map(|(view, cascades)| {
232                (
233                    *view,
234                    cascades
235                        .iter()
236                        .map(|c| Frustum::from_clip_from_world(&c.clip_from_world))
237                        .collect::<Vec<_>>(),
238                )
239            })
240            .collect();
241    }
242}
243
244/// Add to a [`DirectionalLight`] to control rendering of the visible solar disk in the sky.
245/// Affects only the disk’s appearance, not the light’s illuminance or shadows.
246/// Requires a `bevy::pbr::Atmosphere` component on a [`Camera3d`](bevy_camera::Camera3d) to have any effect.
247///
248/// By default, the atmosphere is rendered with [`SunDisk::EARTH`], which approximates the
249/// apparent size and brightness of the Sun as seen from Earth. You can also disable the sun
250/// disk entirely with [`SunDisk::OFF`].
251///
252/// In order to cause the sun to "glow" and light up the surrounding sky, enable bloom
253/// in your post-processing pipeline by adding a `Bloom` component to your camera.
254#[derive(Component, Clone)]
255#[require(DirectionalLight)]
256pub struct SunDisk {
257    /// The angular size (diameter) of the sun disk in radians, as observed from the scene.
258    pub angular_size: f32,
259    /// Multiplier for the brightness of the sun disk.
260    ///
261    /// `0.0` disables the disk entirely (atmospheric scattering still occurs),
262    /// `1.0` is the default physical intensity, and values `>1.0` overexpose it.
263    pub intensity: f32,
264}
265
266impl SunDisk {
267    /// Earth-like parameters for the sun disk.
268    ///
269    /// Uses the mean apparent size (~32 arcminutes) of the Sun at 1 AU distance
270    /// with default intensity.
271    pub const EARTH: SunDisk = SunDisk {
272        angular_size: 0.00930842,
273        intensity: 1.0,
274    };
275
276    /// No visible sun disk.
277    ///
278    /// Keeps scattering and directional light illumination, but hides the disk itself.
279    pub const OFF: SunDisk = SunDisk {
280        angular_size: 0.0,
281        intensity: 0.0,
282    };
283}
284
285impl Default for SunDisk {
286    fn default() -> Self {
287        Self::EARTH
288    }
289}
290
291impl Default for &SunDisk {
292    fn default() -> Self {
293        &SunDisk::EARTH
294    }
295}