bevy_light/point_light.rs
1use bevy_asset::Handle;
2use bevy_camera::{
3 primitives::{CubeMapFace, CubemapFrusta, CubemapLayout, Frustum, CUBE_MAP_FACES},
4 visibility::{self, CubemapVisibleEntities, Visibility, VisibilityClass},
5};
6use bevy_color::Color;
7use bevy_ecs::prelude::*;
8use bevy_image::Image;
9use bevy_math::Mat4;
10use bevy_reflect::prelude::*;
11use bevy_transform::components::{GlobalTransform, Transform};
12
13use crate::{
14 cluster::{ClusterVisibilityClass, GlobalVisibleClusterableObjects},
15 light_consts,
16};
17
18/// A light that emits light in all directions from a central point.
19///
20/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power
21/// consumption of the type of real-world light are:
22///
23/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts) |
24/// |------|-----|----|--------|-------|
25/// | 200 | 25 | | 3-5 | 3 |
26/// | 450 | 40 | 29 | 9-11 | 5-8 |
27/// | 800 | 60 | | 13-15 | 8-12 |
28/// | 1100 | 75 | 53 | 18-20 | 10-16 |
29/// | 1600 | 100 | 72 | 24-28 | 14-17 |
30/// | 2400 | 150 | | 30-52 | 24-30 |
31/// | 3100 | 200 | | 49-75 | 32 |
32/// | 4000 | 300 | | 75-100 | 40.5 |
33///
34/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting)
35///
36/// ## Shadows
37///
38/// To enable shadows, set the `shadows_enabled` property to `true`.
39///
40/// To control the resolution of the shadow maps, use the [`PointLightShadowMap`] resource.
41#[derive(Component, Debug, Clone, Copy, Reflect)]
42#[reflect(Component, Default, Debug, Clone)]
43#[require(
44 CubemapFrusta,
45 CubemapVisibleEntities,
46 Transform,
47 Visibility,
48 VisibilityClass
49)]
50#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
51pub struct PointLight {
52 /// The color of this light source.
53 pub color: Color,
54
55 /// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
56 pub intensity: f32,
57
58 /// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by
59 /// this light at all, so it's important to tune this together with `intensity` to prevent hard
60 /// lighting cut-offs.
61 pub range: f32,
62
63 /// Simulates a light source coming from a spherical volume with the given
64 /// radius.
65 ///
66 /// This affects the size of specular highlights created by this light, as
67 /// well as the soft shadow penumbra size. Because of this, large values may
68 /// not produce the intended result -- for example, light radius does not
69 /// affect shadow softness or diffuse lighting.
70 pub radius: f32,
71
72 /// Whether this light casts shadows.
73 pub 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 shadows_enabled: false,
136 affects_lightmapped_mesh_diffuse: true,
137 shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
138 shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
139 shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
140 #[cfg(feature = "experimental_pbr_pcss")]
141 soft_shadows_enabled: false,
142 }
143 }
144}
145
146impl PointLight {
147 pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08;
148 pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
149 pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
150}
151
152/// Add to a [`PointLight`] to add a light texture effect.
153/// A texture mask is applied to the light source to modulate its intensity,
154/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs.
155#[derive(Clone, Component, Debug, Reflect)]
156#[reflect(Component, Debug)]
157#[require(PointLight)]
158pub struct PointLightTexture {
159 /// The texture image. Only the R channel is read.
160 pub image: Handle<Image>,
161 /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum.
162 pub cubemap_layout: CubemapLayout,
163}
164
165/// Controls the resolution of [`PointLight`] shadow maps.
166///
167/// ```
168/// # use bevy_app::prelude::*;
169/// # use bevy_light::PointLightShadowMap;
170/// App::new()
171/// .insert_resource(PointLightShadowMap { size: 2048 });
172/// ```
173#[derive(Resource, Clone, Debug, Reflect)]
174#[reflect(Resource, Debug, Default, Clone)]
175pub struct PointLightShadowMap {
176 /// The width and height of each of the 6 faces of the cubemap.
177 ///
178 /// Defaults to `1024`.
179 pub size: usize,
180}
181
182impl Default for PointLightShadowMap {
183 fn default() -> Self {
184 Self { size: 1024 }
185 }
186}
187
188// NOTE: Run this after assign_lights_to_clusters!
189pub fn update_point_light_frusta(
190 global_lights: Res<GlobalVisibleClusterableObjects>,
191 mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
192 changed_lights: Query<
193 Entity,
194 (
195 With<PointLight>,
196 Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
197 ),
198 >,
199) {
200 let view_rotations = CUBE_MAP_FACES
201 .iter()
202 .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
203 .collect::<Vec<_>>();
204
205 for (entity, transform, point_light, mut cubemap_frusta) in &mut views {
206 // If this light hasn't changed, and neither has the set of global_lights,
207 // then we can skip this calculation.
208 if !global_lights.is_changed() && !changed_lights.contains(entity) {
209 continue;
210 }
211
212 // The frusta are used for culling meshes to the light for shadow mapping
213 // so if shadow mapping is disabled for this light, then the frusta are
214 // not needed.
215 // Also, if the light is not relevant for any cluster, it will not be in the
216 // global lights set and so there is no need to update its frusta.
217 if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) {
218 continue;
219 }
220
221 let clip_from_view = Mat4::perspective_infinite_reverse_rh(
222 core::f32::consts::FRAC_PI_2,
223 1.0,
224 point_light.shadow_map_near_z,
225 );
226
227 // ignore scale because we don't want to effectively scale light radius and range
228 // by applying those as a view transform to shadow map rendering of objects
229 // and ignore rotation because we want the shadow map projections to align with the axes
230 let view_translation = Transform::from_translation(transform.translation());
231 let view_backward = transform.back();
232
233 for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) {
234 let world_from_view = view_translation * *view_rotation;
235 let clip_from_world = clip_from_view * world_from_view.compute_affine().inverse();
236
237 *frustum = Frustum::from_clip_from_world_custom_far(
238 &clip_from_world,
239 &transform.translation(),
240 &view_backward,
241 point_light.range,
242 );
243 }
244 }
245}