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}