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