use bevy_asset::Handle;
use bevy_camera::{
primitives::{Frustum, Sphere},
visibility::{self, ViewVisibility, Visibility, VisibilityClass, VisibleMeshEntities},
};
use bevy_color::Color;
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{primitives::ViewFrustum, Affine3A, Dir3, Mat3, Mat4, Vec3};
use bevy_reflect::prelude::*;
use bevy_transform::components::{GlobalTransform, Transform};
use crate::cluster::ClusterVisibilityClass;
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]
#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
pub struct SpotLight {
pub color: Color,
pub intensity: f32,
pub range: f32,
pub radius: f32,
pub shadow_maps_enabled: bool,
pub contact_shadows_enabled: bool,
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool,
pub affects_lightmapped_mesh_diffuse: bool,
pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32,
pub shadow_map_near_z: f32,
pub outer_angle: f32,
pub inner_angle: f32,
}
impl SpotLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
}
impl Default for SpotLight {
fn default() -> Self {
Self {
color: Color::WHITE,
intensity: 1_000_000.0,
range: 20.0,
radius: 0.0,
shadow_maps_enabled: false,
contact_shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
inner_angle: 0.0,
outer_angle: core::f32::consts::FRAC_PI_4,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: false,
}
}
}
pub fn orthonormalize(z_basis: Dir3) -> Mat3 {
let sign = 1f32.copysign(z_basis.z);
let a = -1.0 / (sign + z_basis.z);
let b = z_basis.x * z_basis.y * a;
let x_basis = Vec3::new(
1.0 + sign * z_basis.x * z_basis.x * a,
sign * b,
-sign * z_basis.x,
);
let y_basis = Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y);
Mat3::from_cols(x_basis, y_basis, z_basis.into())
}
pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Affine3A {
let fwd_dir = transform.back();
let basis = orthonormalize(fwd_dir);
Affine3A::from_mat3_translation(basis, transform.translation())
}
pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 {
Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z)
}
#[derive(Clone, Component, Debug, Reflect, FromTemplate)]
#[reflect(Component, Debug)]
#[require(SpotLight)]
pub struct SpotLightTexture {
pub image: Handle<Image>,
}
pub fn update_spot_light_bounding_spheres(
mut commands: Commands,
spot_lights_query: Query<
(Entity, &SpotLight, &GlobalTransform),
Or<(Changed<SpotLight>, Changed<GlobalTransform>)>,
>,
) {
for (spot_light_entity, spot_light, global_transform) in &spot_lights_query {
commands.entity(spot_light_entity).insert(Sphere {
center: global_transform.translation_vec3a(),
radius: spot_light.range,
});
}
}
pub fn update_spot_light_frusta(
mut views: Query<
(&GlobalTransform, &SpotLight, &mut Frustum, &ViewVisibility),
Or<(
Changed<GlobalTransform>,
Changed<SpotLight>,
Changed<ViewVisibility>,
)>,
>,
) {
for (transform, spot_light, mut frustum, view_visibility) in &mut views {
if !spot_light.shadow_maps_enabled || !view_visibility.get() {
continue;
}
let view_backward = transform.back();
let spot_world_from_view = spot_light_world_from_view(transform);
let spot_clip_from_view =
spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z);
let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse();
*frustum = Frustum(ViewFrustum::from_clip_from_world_custom_far(
&clip_from_world,
&transform.translation(),
&view_backward,
spot_light.range,
));
}
}