Skip to main content

bevy_light/
spot_light.rs

1use bevy_asset::Handle;
2use bevy_camera::{
3    primitives::{Frustum, Sphere},
4    visibility::{self, ViewVisibility, Visibility, VisibilityClass, VisibleMeshEntities},
5};
6use bevy_color::Color;
7use bevy_ecs::prelude::*;
8use bevy_image::Image;
9use bevy_math::{proj, Affine3A, Dir3, Mat3, Mat4, Vec3};
10use bevy_reflect::prelude::*;
11use bevy_shape::ViewFrustum;
12use bevy_transform::components::{GlobalTransform, Transform};
13
14use crate::cluster::ClusterVisibilityClass;
15
16/// A light that emits light in a given direction from a central point.
17///
18/// Behaves like a point light in a perfectly absorbent housing that
19/// shines light only in a given direction. The direction is taken from
20/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at).
21///
22/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`](`crate::DirectionalLightShadowMap`) resource.
23///
24/// **Warning:**
25/// If this component is used on an entity that also has the [`Camera`](`bevy_camera::Camera`)
26/// component, the [`Frustum`](`bevy_camera::primitives::Frustum`) of the
27/// spotlight will override the frustum of the camera, causing possible
28/// rendering artifacts. To fix this, add either the spotlight or the camera
29/// component on a child entity instead.
30#[derive(Component, Debug, Clone, Copy, Reflect)]
31#[reflect(Component, Default, Debug, Clone)]
32#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]
33#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
34pub struct SpotLight {
35    /// The color of the light.
36    ///
37    /// By default, this is white.
38    pub color: Color,
39
40    /// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
41    pub intensity: f32,
42
43    /// Range in meters that this light illuminates.
44    ///
45    /// Note that this value affects resolution of the shadow maps; generally, the
46    /// higher you set it, the lower-resolution your shadow maps will be.
47    /// Consequently, you should set this value to be only the size that you need.
48    pub range: f32,
49
50    /// Simulates a light source coming from a spherical volume with the given
51    /// radius.
52    ///
53    /// This affects the size of specular highlights created by this light, as
54    /// well as the soft shadow penumbra size. Because of this, large values may
55    /// not produce the intended result -- for example, light radius does not
56    /// affect shadow softness or diffuse lighting.
57    pub radius: f32,
58
59    /// Whether this light casts shadows.
60    ///
61    /// Note that shadows are rather expensive and become more so with every
62    /// light that casts them. In general, it's best to aggressively limit the
63    /// number of lights with shadows enabled to one or two at most.
64    pub shadow_maps_enabled: bool,
65
66    /// Whether this light casts contact shadows. Cameras must also have the `ContactShadows`
67    /// component.
68    pub contact_shadows_enabled: bool,
69
70    /// Whether soft shadows are enabled.
71    ///
72    /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
73    /// cause shadows to become blurrier (i.e. their penumbra increases in
74    /// radius) as they extend away from objects. The blurriness of the shadow
75    /// depends on the [`SpotLight::radius`] of the light; larger lights result in larger
76    /// penumbras and therefore blurrier shadows.
77    ///
78    /// Currently, soft shadows are rather noisy if not using the temporal mode.
79    /// If you enable soft shadows, consider choosing
80    /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
81    /// (TAA) to smooth the noise out over time.
82    ///
83    /// Note that soft shadows are significantly more expensive to render than
84    /// hard shadows.
85    ///
86    /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
87    #[cfg(feature = "experimental_pbr_pcss")]
88    pub soft_shadows_enabled: bool,
89
90    /// Whether this spot light contributes diffuse lighting to meshes with
91    /// lightmaps.
92    ///
93    /// Set this to false if your lightmap baking tool bakes the direct diffuse
94    /// light from this directional light into the lightmaps in order to avoid
95    /// counting the radiance from this light twice. Note that the specular
96    /// portion of the light is always considered, because Bevy currently has no
97    /// means to bake specular light.
98    ///
99    /// By default, this is set to true.
100    pub affects_lightmapped_mesh_diffuse: bool,
101
102    /// A value that adjusts the tradeoff between self-shadowing artifacts and
103    /// proximity of shadows to their casters.
104    ///
105    /// This value frequently must be tuned to the specific scene; this is
106    /// normal and a well-known part of the shadow mapping workflow. If set too
107    /// low, unsightly shadow patterns appear on objects not in shadow as
108    /// objects incorrectly cast shadows on themselves, known as *shadow acne*.
109    /// If set too high, shadows detach from the objects casting them and seem
110    /// to "fly" off the objects, known as *Peter Panning*.
111    pub shadow_depth_bias: f32,
112
113    /// A bias applied along the direction of the fragment's surface normal. It is scaled to the
114    /// shadow map's texel size so that it can be small close to the camera and gets larger further
115    /// away.
116    pub shadow_normal_bias: f32,
117
118    /// The distance from the light to the near Z plane in the shadow map.
119    ///
120    /// Objects closer than this distance to the light won't cast shadows.
121    /// Setting this higher increases the shadow map's precision.
122    ///
123    /// This only has an effect if shadows are enabled.
124    pub shadow_map_near_z: f32,
125
126    /// Angle defining the distance from the spot light direction to the outer limit
127    /// of the light's cone of effect.
128    /// `outer_angle` must be < `PI / 2.0`.
129    pub outer_angle: f32,
130
131    /// Angle defining the distance from the spot light direction to the inner limit
132    /// of the light's cone of effect.
133    /// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff.
134    /// `inner_angle` should be <= `outer_angle`
135    pub inner_angle: f32,
136}
137
138impl SpotLight {
139    /// The default value of [`SpotLight::shadow_depth_bias`].
140    pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
141    /// The default value of [`SpotLight::shadow_normal_bias`].
142    pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
143    /// The default value of [`SpotLight::shadow_map_near_z`].
144    pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
145}
146
147impl Default for SpotLight {
148    fn default() -> Self {
149        // a quarter arc attenuating from the center
150        Self {
151            color: Color::WHITE,
152            // 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's
153            // default "very overcast day" exposure level. For "indoor lighting" with a lower exposure,
154            // this would be way too bright.
155            intensity: 1_000_000.0,
156            range: 20.0,
157            radius: 0.0,
158            shadow_maps_enabled: false,
159            contact_shadows_enabled: false,
160            affects_lightmapped_mesh_diffuse: true,
161            shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
162            shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
163            shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
164            inner_angle: 0.0,
165            outer_angle: core::f32::consts::FRAC_PI_4,
166            #[cfg(feature = "experimental_pbr_pcss")]
167            soft_shadows_enabled: false,
168        }
169    }
170}
171
172/// Constructs a right-handed orthonormal basis from a given unit Z vector.
173///
174/// This method of constructing a basis from a [`Vec3`] is used by [`bevy_math::Vec3::any_orthonormal_pair`]
175// we will also construct it in the fragment shader and need our implementations to match exactly,
176// so we reproduce it here to avoid a mismatch if glam changes.
177// See bevy_render/maths.wgsl:orthonormalize
178pub fn orthonormalize(z_basis: Dir3) -> Mat3 {
179    let sign = 1f32.copysign(z_basis.z);
180    let a = -1.0 / (sign + z_basis.z);
181    let b = z_basis.x * z_basis.y * a;
182    let x_basis = Vec3::new(
183        1.0 + sign * z_basis.x * z_basis.x * a,
184        sign * b,
185        -sign * z_basis.x,
186    );
187    let y_basis = Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y);
188    Mat3::from_cols(x_basis, y_basis, z_basis.into())
189}
190/// Constructs a right-handed orthonormal basis with translation, using only the forward direction and translation of a given [`GlobalTransform`].
191///
192/// This is a version of [`orthonormalize`] which also includes translation.
193pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Affine3A {
194    // the matrix z_local (opposite of transform.forward())
195    let fwd_dir = transform.back();
196
197    let basis = orthonormalize(fwd_dir);
198    Affine3A::from_mat3_translation(basis, transform.translation())
199}
200
201/// Creates the projection matrix that transforms the light's view space into the light's clip space.
202pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 {
203    // spot light projection FOV is 2x the angle from spot light center to outer edge
204    proj::perspective_infinite_reverse(angle * 2.0, 1.0, near_z)
205}
206
207/// Add to a [`SpotLight`] to add a light texture effect.
208/// A texture mask is applied to the light source to modulate its intensity,  
209/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs.
210#[derive(Clone, Component, Debug, Reflect, FromTemplate)]
211#[reflect(Component, Debug)]
212#[require(SpotLight)]
213pub struct SpotLightTexture {
214    /// The texture image. Only the R channel is read.
215    /// Note the border of the image should be entirely black to avoid leaking light.
216    pub image: Handle<Image>,
217}
218
219/// A system that updates the bounding [`Sphere`] for changed spot lights.
220///
221/// The [`Sphere`] component is used for frustum culling.
222pub fn update_spot_light_bounding_spheres(
223    mut commands: Commands,
224    spot_lights_query: Query<
225        (Entity, &SpotLight, &GlobalTransform),
226        Or<(Changed<SpotLight>, Changed<GlobalTransform>)>,
227    >,
228) {
229    for (spot_light_entity, spot_light, global_transform) in &spot_lights_query {
230        commands.entity(spot_light_entity).insert(Sphere {
231            center: global_transform.translation_vec3a(),
232            radius: spot_light.range,
233        });
234    }
235}
236
237/// Updates the frusta for all visible shadow mapped [`SpotLight`]s.
238pub fn update_spot_light_frusta(
239    mut views: Query<
240        (&GlobalTransform, &SpotLight, &mut Frustum, &ViewVisibility),
241        Or<(
242            Changed<GlobalTransform>,
243            Changed<SpotLight>,
244            Changed<ViewVisibility>,
245        )>,
246    >,
247) {
248    for (transform, spot_light, mut frustum, view_visibility) in &mut views {
249        // The frusta are used for culling meshes to the light for shadow mapping
250        // so if shadow mapping is disabled for this light, then the frusta are
251        // not needed.
252        // Also, if the light is not relevant for any cluster, it will not be in the
253        // global lights set and so there is no need to update its frusta.
254        if !spot_light.shadow_maps_enabled || !view_visibility.get() {
255            continue;
256        }
257
258        // ignore scale because we don't want to effectively scale light radius and range
259        // by applying those as a view transform to shadow map rendering of objects
260        let view_backward = transform.back();
261
262        let spot_world_from_view = spot_light_world_from_view(transform);
263        let spot_clip_from_view =
264            spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z);
265        let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse();
266
267        *frustum = Frustum(ViewFrustum::from_clip_from_world_custom_far(
268            &clip_from_world,
269            &transform.translation(),
270            &view_backward,
271            spot_light.range,
272        ));
273    }
274}