use bevy_camera::{Camera, Projection};
use bevy_ecs::{entity::EntityHashMap, prelude::*};
use bevy_math::{ops, Mat4, Vec3A, Vec4};
use bevy_reflect::prelude::*;
use bevy_transform::components::GlobalTransform;
use crate::{DirectionalLight, DirectionalLightShadowMap};
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct CascadeShadowConfig {
pub bounds: Vec<f32>,
pub overlap_proportion: f32,
pub minimum_distance: f32,
}
impl Default for CascadeShadowConfig {
fn default() -> Self {
CascadeShadowConfigBuilder::default().into()
}
}
fn calculate_cascade_bounds(
num_cascades: usize,
nearest_bound: f32,
shadow_maximum_distance: f32,
) -> Vec<f32> {
if num_cascades == 1 {
return vec![shadow_maximum_distance];
}
let base = ops::powf(
shadow_maximum_distance / nearest_bound,
1.0 / (num_cascades - 1) as f32,
);
(0..num_cascades)
.map(|i| nearest_bound * ops::powf(base, i as f32))
.collect()
}
pub struct CascadeShadowConfigBuilder {
pub num_cascades: usize,
pub minimum_distance: f32,
pub maximum_distance: f32,
pub first_cascade_far_bound: f32,
pub overlap_proportion: f32,
}
impl CascadeShadowConfigBuilder {
pub fn build(&self) -> CascadeShadowConfig {
assert!(
self.num_cascades > 0,
"num_cascades must be positive, but was {}",
self.num_cascades
);
assert!(
self.minimum_distance >= 0.0,
"maximum_distance must be non-negative, but was {}",
self.minimum_distance
);
assert!(
self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound,
"minimum_distance must be less than first_cascade_far_bound, but was {}",
self.minimum_distance
);
assert!(
self.maximum_distance > self.minimum_distance,
"maximum_distance must be greater than minimum_distance, but was {}",
self.maximum_distance
);
assert!(
(0.0..1.0).contains(&self.overlap_proportion),
"overlap_proportion must be in [0.0, 1.0) but was {}",
self.overlap_proportion
);
CascadeShadowConfig {
bounds: calculate_cascade_bounds(
self.num_cascades,
self.first_cascade_far_bound,
self.maximum_distance,
),
overlap_proportion: self.overlap_proportion,
minimum_distance: self.minimum_distance,
}
}
}
impl Default for CascadeShadowConfigBuilder {
fn default() -> Self {
Self {
num_cascades: if cfg!(all(
feature = "webgl",
target_arch = "wasm32",
not(feature = "webgpu")
)) {
1
} else {
4
},
minimum_distance: 0.1,
maximum_distance: 150.0,
first_cascade_far_bound: 10.0,
overlap_proportion: 0.2,
}
}
}
impl From<CascadeShadowConfigBuilder> for CascadeShadowConfig {
fn from(builder: CascadeShadowConfigBuilder) -> Self {
builder.build()
}
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub struct Cascades {
pub cascades: EntityHashMap<Vec<Cascade>>,
}
#[derive(Clone, Debug, Default, Reflect)]
#[reflect(Clone, Default)]
pub struct Cascade {
pub world_from_cascade: Mat4,
pub clip_from_cascade: Mat4,
pub clip_from_world: Mat4,
pub texel_size: f32,
}
pub fn build_directional_light_cascades(
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>,
mut lights: Query<(
&GlobalTransform,
&DirectionalLight,
&CascadeShadowConfig,
&mut Cascades,
)>,
) {
let views = views
.iter()
.filter_map(|(entity, transform, projection, camera)| {
if camera.is_active {
Some((entity, projection, transform.to_matrix()))
} else {
None
}
})
.collect::<Vec<_>>();
for (transform, directional_light, cascades_config, mut cascades) in &mut lights {
if !directional_light.shadow_maps_enabled {
continue;
}
cascades.cascades.clear();
let world_from_light = Mat4::from_quat(transform.rotation());
let light_from_world = world_from_light.transpose();
for (view_entity, projection, world_from_view) in views.iter().copied() {
let light_view_from_camera = light_from_world * world_from_view;
let overlap_factor = 1.0 - cascades_config.overlap_proportion;
let far_bounds = cascades_config.bounds.iter();
let near_bounds = [cascades_config.minimum_distance]
.into_iter()
.chain(far_bounds.clone().map(|bound| overlap_factor * bound));
let view_cascades = near_bounds
.zip(far_bounds)
.map(|(near_bound, far_bound)| {
let corners = projection.get_frustum_corners(-near_bound, -far_bound);
calculate_cascade(
corners,
directional_light_shadow_map.size as f32,
world_from_light,
light_view_from_camera,
)
})
.collect();
cascades.cascades.insert(view_entity, view_cascades);
}
}
}
fn calculate_cascade(
frustum_corners: [Vec3A; 8],
cascade_texture_size: f32,
world_from_light: Mat4,
light_from_camera: Mat4,
) -> Cascade {
let mut min = Vec3A::splat(f32::MAX);
let mut max = Vec3A::splat(f32::MIN);
for corner_camera_view in frustum_corners {
let corner_light_view = light_from_camera.transform_point3a(corner_camera_view);
min = min.min(corner_light_view);
max = max.max(corner_light_view);
}
let body_diagonal = (frustum_corners[0] - frustum_corners[6]).length_squared();
let far_plane_diagonal = (frustum_corners[4] - frustum_corners[6]).length_squared();
let cascade_diameter = body_diagonal.max(far_plane_diagonal).sqrt().ceil();
let cascade_texel_size = cascade_diameter / cascade_texture_size;
let near_plane_center = Vec3A::new(
(0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size,
(0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size,
max.z,
);
let world_from_light_transpose = world_from_light.transpose();
let cascade_from_world = Mat4::from_cols(
world_from_light_transpose.x_axis,
world_from_light_transpose.y_axis,
world_from_light_transpose.z_axis,
(-near_plane_center).extend(1.0),
);
let world_from_cascade = Mat4::from_cols(
world_from_light.x_axis,
world_from_light.y_axis,
world_from_light.z_axis,
world_from_light * near_plane_center.extend(1.0),
);
let r = (max.z - min.z).recip();
let clip_from_cascade = Mat4::from_cols(
Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0),
Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0),
Vec4::new(0.0, 0.0, r, 0.0),
Vec4::new(0.0, 0.0, 1.0, 1.0),
);
let clip_from_world = clip_from_cascade * cascade_from_world;
Cascade {
world_from_cascade,
clip_from_cascade,
clip_from_world,
texel_size: cascade_texel_size,
}
}