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