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