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