Skip to main content

bevy_light/
point_light.rs

1use bevy_asset::Handle;
2use bevy_camera::{
3    primitives::{CubeMapFace, CubemapFrusta, CubemapLayout, Frustum, Sphere, CUBE_MAP_FACES},
4    visibility::{self, CubemapVisibleEntities, ViewVisibility, Visibility, VisibilityClass},
5};
6use bevy_color::Color;
7use bevy_ecs::prelude::*;
8use bevy_image::Image;
9use bevy_math::{primitives::ViewFrustum, Mat4};
10use bevy_reflect::prelude::*;
11use bevy_transform::components::{GlobalTransform, Transform};
12
13use crate::{cluster::ClusterVisibilityClass, light_consts};
14
15/// A light that emits light in all directions from a central point.
16///
17/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power
18/// consumption of the type of real-world light are:
19///
20/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts) |
21/// |------|-----|----|--------|-------|
22/// | 200  | 25  |    | 3-5    | 3     |
23/// | 450  | 40  | 29 | 9-11   | 5-8   |
24/// | 800  | 60  |    | 13-15  | 8-12  |
25/// | 1100 | 75  | 53 | 18-20  | 10-16 |
26/// | 1600 | 100 | 72 | 24-28  | 14-17 |
27/// | 2400 | 150 |    | 30-52  | 24-30 |
28/// | 3100 | 200 |    | 49-75  | 32    |
29/// | 4000 | 300 |    | 75-100 | 40.5  |
30///
31/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting)
32///
33/// ## Shadows
34///
35/// To enable shadows, set the `shadow_maps_enabled` property to `true`.
36///
37/// To control the resolution of the shadow maps, use the [`PointLightShadowMap`] resource.
38#[derive(Component, Debug, Clone, Copy, Reflect)]
39#[reflect(Component, Default, Debug, Clone)]
40#[require(
41    CubemapFrusta,
42    CubemapVisibleEntities,
43    Transform,
44    Visibility,
45    VisibilityClass
46)]
47#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
48pub struct PointLight {
49    /// The color of this light source.
50    pub color: Color,
51
52    /// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
53    pub intensity: f32,
54
55    /// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by
56    /// this light at all, so it's important to tune this together with `intensity` to prevent hard
57    /// lighting cut-offs.
58    pub range: f32,
59
60    /// Simulates a light source coming from a spherical volume with the given
61    /// radius.
62    ///
63    /// This affects the size of specular highlights created by this light, as
64    /// well as the soft shadow penumbra size. Because of this, large values may
65    /// not produce the intended result -- for example, light radius does not
66    /// affect shadow softness or diffuse lighting.
67    pub radius: f32,
68
69    /// Whether this light casts shadows.
70    pub shadow_maps_enabled: bool,
71
72    /// Whether this light casts contact shadows.
73    pub contact_shadows_enabled: bool,
74
75    /// Whether soft shadows are enabled.
76    ///
77    /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
78    /// cause shadows to become blurrier (i.e. their penumbra increases in
79    /// radius) as they extend away from objects. The blurriness of the shadow
80    /// depends on the [`PointLight::radius`] of the light; larger lights result
81    /// in larger penumbras and therefore blurrier shadows.
82    ///
83    /// Currently, soft shadows are rather noisy if not using the temporal mode.
84    /// If you enable soft shadows, consider choosing
85    /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
86    /// (TAA) to smooth the noise out over time.
87    ///
88    /// Note that soft shadows are significantly more expensive to render than
89    /// hard shadows.
90    ///
91    /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
92    #[cfg(feature = "experimental_pbr_pcss")]
93    pub soft_shadows_enabled: bool,
94
95    /// Whether this point light contributes diffuse lighting to meshes with
96    /// lightmaps.
97    ///
98    /// Set this to false if your lightmap baking tool bakes the direct diffuse
99    /// light from this point light into the lightmaps in order to avoid
100    /// counting the radiance from this light twice. Note that the specular
101    /// portion of the light is always considered, because Bevy currently has no
102    /// means to bake specular light.
103    ///
104    /// By default, this is set to true.
105    pub affects_lightmapped_mesh_diffuse: bool,
106
107    /// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions
108    /// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments.
109    /// Too high of a depth bias can lead to shadows detaching from their casters, or
110    /// "peter-panning". This bias can be tuned together with `shadow_normal_bias` to correct shadow
111    /// artifacts for a given scene.
112    pub shadow_depth_bias: f32,
113
114    /// A bias applied along the direction of the fragment's surface normal. It is scaled to the
115    /// shadow map's texel size so that it can be small close to the camera and gets larger further
116    /// away.
117    pub shadow_normal_bias: f32,
118
119    /// The distance from the light to near Z plane in the shadow map.
120    ///
121    /// Objects closer than this distance to the light won't cast shadows.
122    /// Setting this higher increases the shadow map's precision.
123    ///
124    /// This only has an effect if shadows are enabled.
125    pub shadow_map_near_z: f32,
126}
127
128impl Default for PointLight {
129    fn default() -> Self {
130        PointLight {
131            color: Color::WHITE,
132            intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT,
133            range: 20.0,
134            radius: 0.0,
135            shadow_maps_enabled: false,
136            contact_shadows_enabled: false,
137            affects_lightmapped_mesh_diffuse: true,
138            shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
139            shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
140            shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
141            #[cfg(feature = "experimental_pbr_pcss")]
142            soft_shadows_enabled: false,
143        }
144    }
145}
146
147impl PointLight {
148    /// The default value of [`PointLight::shadow_depth_bias`].
149    pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08;
150    /// The default value of [`PointLight::shadow_normal_bias`].
151    pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
152    /// The default value of [`PointLight::shadow_map_near_z`].
153    pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
154}
155
156/// Add to a [`PointLight`] to add a light texture effect.
157/// A texture mask is applied to the light source to modulate its intensity,  
158/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs.
159#[derive(Clone, Component, Debug, Reflect, FromTemplate)]
160#[reflect(Component, Debug)]
161#[require(PointLight)]
162pub struct PointLightTexture {
163    /// The texture image. Only the R channel is read.
164    pub image: Handle<Image>,
165    /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum.
166    pub cubemap_layout: CubemapLayout,
167}
168
169/// Controls the resolution of [`PointLight`] shadow maps.
170///
171/// ```
172/// # use bevy_app::prelude::*;
173/// # use bevy_light::PointLightShadowMap;
174/// App::new()
175///     .insert_resource(PointLightShadowMap { size: 2048 });
176/// ```
177#[derive(Resource, Clone, Debug, Reflect)]
178#[reflect(Resource, Debug, Default, Clone)]
179pub struct PointLightShadowMap {
180    /// The width and height of each of the 6 faces of the cubemap.
181    ///
182    /// Defaults to `1024`.
183    pub size: usize,
184}
185
186impl Default for PointLightShadowMap {
187    fn default() -> Self {
188        Self { size: 1024 }
189    }
190}
191
192/// A system that updates the bounding [`Sphere`] for changed point lights.
193///
194/// The [`Sphere`] component is used for frustum culling.
195pub fn update_point_light_bounding_spheres(
196    mut commands: Commands,
197    point_lights_query: Query<
198        (Entity, &PointLight, &GlobalTransform),
199        Or<(Changed<PointLight>, Changed<GlobalTransform>)>,
200    >,
201) {
202    for (point_light_entity, point_light, global_transform) in &point_lights_query {
203        commands.entity(point_light_entity).insert(Sphere {
204            center: global_transform.translation_vec3a(),
205            radius: point_light.range,
206        });
207    }
208}
209
210// NOTE: Run this after assign_lights_to_clusters!
211/// Updates the frusta for all visible shadow mapped [`PointLight`]s.
212pub fn update_point_light_frusta(
213    mut views: Query<
214        (
215            &GlobalTransform,
216            &PointLight,
217            &mut CubemapFrusta,
218            &ViewVisibility,
219        ),
220        Or<(
221            Changed<GlobalTransform>,
222            Changed<PointLight>,
223            Changed<ViewVisibility>,
224        )>,
225    >,
226) {
227    let view_rotations = CUBE_MAP_FACES
228        .iter()
229        .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
230        .collect::<Vec<_>>();
231
232    for (transform, point_light, mut cubemap_frusta, view_visibility) in &mut views {
233        // The frusta are used for culling meshes to the light for shadow mapping
234        // so if shadow mapping is disabled for this light, then the frusta are
235        // not needed.
236        // Also, if the light is not relevant for any cluster, it will not be in the
237        // global lights set and so there is no need to update its frusta.
238        if !point_light.shadow_maps_enabled || !view_visibility.get() {
239            continue;
240        }
241
242        let clip_from_view = Mat4::perspective_infinite_reverse_rh(
243            core::f32::consts::FRAC_PI_2,
244            1.0,
245            point_light.shadow_map_near_z,
246        );
247
248        // ignore scale because we don't want to effectively scale light radius and range
249        // by applying those as a view transform to shadow map rendering of objects
250        // and ignore rotation because we want the shadow map projections to align with the axes
251        let view_translation = Transform::from_translation(transform.translation());
252        let view_backward = transform.back();
253
254        for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) {
255            let world_from_view = view_translation * *view_rotation;
256            let clip_from_world = clip_from_view * world_from_view.compute_affine().inverse();
257
258            *frustum = Frustum(ViewFrustum::from_clip_from_world_custom_far(
259                &clip_from_world,
260                &transform.translation(),
261                &view_backward,
262                point_light.range,
263            ));
264        }
265    }
266}