use bevy_asset::Handle;1use bevy_camera::{2primitives::{Frustum, Sphere},3visibility::{self, ViewVisibility, Visibility, VisibilityClass, VisibleMeshEntities},4};5use bevy_color::Color;6use bevy_ecs::prelude::*;7use bevy_image::Image;8use bevy_math::{primitives::ViewFrustum, Affine3A, Dir3, Mat3, Mat4, Vec3};9use bevy_reflect::prelude::*;10use bevy_transform::components::{GlobalTransform, Transform};1112use crate::cluster::ClusterVisibilityClass;1314/// A light that emits light in a given direction from a central point.15///16/// Behaves like a point light in a perfectly absorbent housing that17/// shines light only in a given direction. The direction is taken from18/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at).19///20/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`](`crate::DirectionalLightShadowMap`) resource.21#[derive(Component, Debug, Clone, Copy, Reflect)]22#[reflect(Component, Default, Debug, Clone)]23#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]24#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]25pub struct SpotLight {26/// The color of the light.27///28/// By default, this is white.29pub color: Color,3031/// Luminous power in lumens, representing the amount of light emitted by this source in all directions.32pub intensity: f32,3334/// Range in meters that this light illuminates.35///36/// Note that this value affects resolution of the shadow maps; generally, the37/// higher you set it, the lower-resolution your shadow maps will be.38/// Consequently, you should set this value to be only the size that you need.39pub range: f32,4041/// Simulates a light source coming from a spherical volume with the given42/// radius.43///44/// This affects the size of specular highlights created by this light, as45/// well as the soft shadow penumbra size. Because of this, large values may46/// not produce the intended result -- for example, light radius does not47/// affect shadow softness or diffuse lighting.48pub radius: f32,4950/// Whether this light casts shadows.51///52/// Note that shadows are rather expensive and become more so with every53/// light that casts them. In general, it's best to aggressively limit the54/// number of lights with shadows enabled to one or two at most.55pub shadow_maps_enabled: bool,5657/// Whether this light casts contact shadows. Cameras must also have the `ContactShadows`58/// component.59pub contact_shadows_enabled: bool,6061/// Whether soft shadows are enabled.62///63/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,64/// cause shadows to become blurrier (i.e. their penumbra increases in65/// radius) as they extend away from objects. The blurriness of the shadow66/// depends on the [`SpotLight::radius`] of the light; larger lights result in larger67/// penumbras and therefore blurrier shadows.68///69/// Currently, soft shadows are rather noisy if not using the temporal mode.70/// If you enable soft shadows, consider choosing71/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing72/// (TAA) to smooth the noise out over time.73///74/// Note that soft shadows are significantly more expensive to render than75/// hard shadows.76///77/// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal78#[cfg(feature = "experimental_pbr_pcss")]79pub soft_shadows_enabled: bool,8081/// Whether this spot light contributes diffuse lighting to meshes with82/// lightmaps.83///84/// Set this to false if your lightmap baking tool bakes the direct diffuse85/// light from this directional light into the lightmaps in order to avoid86/// counting the radiance from this light twice. Note that the specular87/// portion of the light is always considered, because Bevy currently has no88/// means to bake specular light.89///90/// By default, this is set to true.91pub affects_lightmapped_mesh_diffuse: bool,9293/// A value that adjusts the tradeoff between self-shadowing artifacts and94/// proximity of shadows to their casters.95///96/// This value frequently must be tuned to the specific scene; this is97/// normal and a well-known part of the shadow mapping workflow. If set too98/// low, unsightly shadow patterns appear on objects not in shadow as99/// objects incorrectly cast shadows on themselves, known as *shadow acne*.100/// If set too high, shadows detach from the objects casting them and seem101/// to "fly" off the objects, known as *Peter Panning*.102pub shadow_depth_bias: f32,103104/// A bias applied along the direction of the fragment's surface normal. It is scaled to the105/// shadow map's texel size so that it can be small close to the camera and gets larger further106/// away.107pub shadow_normal_bias: f32,108109/// The distance from the light to the near Z plane in the shadow map.110///111/// Objects closer than this distance to the light won't cast shadows.112/// Setting this higher increases the shadow map's precision.113///114/// This only has an effect if shadows are enabled.115pub shadow_map_near_z: f32,116117/// Angle defining the distance from the spot light direction to the outer limit118/// of the light's cone of effect.119/// `outer_angle` should be < `PI / 2.0`.120/// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle121/// approaches this limit.122pub outer_angle: f32,123124/// Angle defining the distance from the spot light direction to the inner limit125/// of the light's cone of effect.126/// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff.127/// `inner_angle` should be <= `outer_angle`128pub inner_angle: f32,129}130131impl SpotLight {132/// The default value of [`SpotLight::shadow_depth_bias`].133pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;134/// The default value of [`SpotLight::shadow_normal_bias`].135pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;136/// The default value of [`SpotLight::shadow_map_near_z`].137pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;138}139140impl Default for SpotLight {141fn default() -> Self {142// a quarter arc attenuating from the center143Self {144color: Color::WHITE,145// 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's146// default "very overcast day" exposure level. For "indoor lighting" with a lower exposure,147// this would be way too bright.148intensity: 1_000_000.0,149range: 20.0,150radius: 0.0,151shadow_maps_enabled: false,152contact_shadows_enabled: false,153affects_lightmapped_mesh_diffuse: true,154shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,155shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,156shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,157inner_angle: 0.0,158outer_angle: core::f32::consts::FRAC_PI_4,159#[cfg(feature = "experimental_pbr_pcss")]160soft_shadows_enabled: false,161}162}163}164165/// Constructs a right-handed orthonormal basis from a given unit Z vector.166///167/// This method of constructing a basis from a [`Vec3`] is used by [`bevy_math::Vec3::any_orthonormal_pair`]168// we will also construct it in the fragment shader and need our implementations to match exactly,169// so we reproduce it here to avoid a mismatch if glam changes.170// See bevy_render/maths.wgsl:orthonormalize171pub fn orthonormalize(z_basis: Dir3) -> Mat3 {172let sign = 1f32.copysign(z_basis.z);173let a = -1.0 / (sign + z_basis.z);174let b = z_basis.x * z_basis.y * a;175let x_basis = Vec3::new(1761.0 + sign * z_basis.x * z_basis.x * a,177sign * b,178-sign * z_basis.x,179);180let y_basis = Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y);181Mat3::from_cols(x_basis, y_basis, z_basis.into())182}183/// Constructs a right-handed orthonormal basis with translation, using only the forward direction and translation of a given [`GlobalTransform`].184///185/// This is a version of [`orthonormalize`] which also includes translation.186pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Affine3A {187// the matrix z_local (opposite of transform.forward())188let fwd_dir = transform.back();189190let basis = orthonormalize(fwd_dir);191Affine3A::from_mat3_translation(basis, transform.translation())192}193194/// Creates the projection matrix that transforms the light's view space into the light's clip space.195pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 {196// spot light projection FOV is 2x the angle from spot light center to outer edge197Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z)198}199200/// Add to a [`SpotLight`] to add a light texture effect.201/// A texture mask is applied to the light source to modulate its intensity,202/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs.203#[derive(Clone, Component, Debug, Reflect)]204#[reflect(Component, Debug)]205#[require(SpotLight)]206pub struct SpotLightTexture {207/// The texture image. Only the R channel is read.208/// Note the border of the image should be entirely black to avoid leaking light.209pub image: Handle<Image>,210}211212/// A system that updates the bounding [`Sphere`] for changed spot lights.213///214/// The [`Sphere`] component is used for frustum culling.215pub fn update_spot_light_bounding_spheres(216mut commands: Commands,217spot_lights_query: Query<218(Entity, &SpotLight, &GlobalTransform),219Or<(Changed<SpotLight>, Changed<GlobalTransform>)>,220>,221) {222for (spot_light_entity, spot_light, global_transform) in &spot_lights_query {223commands.entity(spot_light_entity).insert(Sphere {224center: global_transform.translation_vec3a(),225radius: spot_light.range,226});227}228}229230/// Updates the frusta for all visible shadow mapped [`SpotLight`]s.231pub fn update_spot_light_frusta(232mut views: Query<233(&GlobalTransform, &SpotLight, &mut Frustum, &ViewVisibility),234Or<(235Changed<GlobalTransform>,236Changed<SpotLight>,237Changed<ViewVisibility>,238)>,239>,240) {241for (transform, spot_light, mut frustum, view_visibility) in &mut views {242// The frusta are used for culling meshes to the light for shadow mapping243// so if shadow mapping is disabled for this light, then the frusta are244// not needed.245// Also, if the light is not relevant for any cluster, it will not be in the246// global lights set and so there is no need to update its frusta.247if !spot_light.shadow_maps_enabled || !view_visibility.get() {248continue;249}250251// ignore scale because we don't want to effectively scale light radius and range252// by applying those as a view transform to shadow map rendering of objects253let view_backward = transform.back();254255let spot_world_from_view = spot_light_world_from_view(transform);256let spot_clip_from_view =257spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z);258let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse();259260*frustum = Frustum(ViewFrustum::from_clip_from_world_custom_far(261&clip_from_world,262&transform.translation(),263&view_backward,264spot_light.range,265));266}267}268269270