use bevy_asset::Handle;
use bevy_camera::{
primitives::Frustum,
visibility::{self, Visibility, VisibilityClass, VisibleMeshEntities},
};
use bevy_color::Color;
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{Affine3A, Dir3, Mat3, Mat4, Vec3};
use bevy_reflect::prelude::*;
use bevy_transform::components::{GlobalTransform, Transform};
use crate::cluster::{ClusterVisibilityClass, GlobalVisibleClusterableObjects};
#[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 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,
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)]
#[reflect(Component, Debug)]
#[require(SpotLight)]
pub struct SpotLightTexture {
pub image: Handle<Image>,
}
pub fn update_spot_light_frusta(
global_lights: Res<GlobalVisibleClusterableObjects>,
mut views: Query<
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
>,
) {
for (entity, transform, spot_light, mut frustum) in &mut views {
if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) {
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::from_clip_from_world_custom_far(
&clip_from_world,
&transform.translation(),
&view_backward,
spot_light.range,
);
}
}