use math::{length, mat::MatTranslate as _, normalize, Base as _, Matrix4, Vector3, Vector4};
use smallvec::SmallVec;
use super::view::View;
pub(crate) fn make_cascade_split_ranges(camera_view: View, num_cascades: usize) -> impl ExactSizeIterator<Item = (f32, f32)> {
let near = camera_view.near();
let far = camera_view.far();
let range = far - near;
let ratio = far / near;
let mut cascade_near = near;
(0..num_cascades).map(move |index| {
let p = (index + 1) as f32 / num_cascades as f32;
let log = near * ratio.powf(p);
let uniform = near + range * p;
let cascade_far = 0.95f32 * (log - uniform) + uniform;
let cascade_range = (cascade_near, cascade_far);
cascade_near = cascade_far;
cascade_range
})
}
pub fn make_csm_views(
camera_view: View,
light_direction: Vector3,
num_cascades: usize,
shadow_map_resolution: u32,
) -> impl ExactSizeIterator<Item = View> {
let light_direction = normalize(light_direction);
let camera_far = camera_view.far();
make_cascade_split_ranges(camera_view, num_cascades).map(move |(cascade_near, cascade_far)| {
let camera_view = camera_view.from_from_z_planes(cascade_near, cascade_far);
let camera_frustum_corners = camera_view.get_frustum_corners();
let center = camera_frustum_corners.iter().fold(Vector4::zero(), |acc, x| acc + *x) / 8.0;
let radius = stabilize_cascade_radius(center, &camera_frustum_corners, shadow_map_resolution);
let center: Vector3 = center.into();
let light_view = {
let back_extension = camera_far;
let depth = 2.0 * radius + back_extension;
let from = center - light_direction * (radius + back_extension);
View::new_orthographic(-radius, radius, -radius, radius, 0f32, depth, from, light_direction)
};
snap_shadow_view_to_texels(light_view, center, radius, shadow_map_resolution)
})
}
fn stabilize_cascade_radius(center: Vector4, camera_frustum_corners: &[Vector4; 8], shadow_map_resolution: u32) -> f32 {
let base_radius = camera_frustum_corners
.iter()
.map(|x| length(x - center))
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
if shadow_map_resolution == 0 {
return (base_radius * 16.0).ceil() / 16.0;
}
let minimum_radius = (base_radius * 16.0).ceil() / 16.0;
let texel_scale = shadow_map_resolution as f32 / 2.0;
(minimum_radius * texel_scale).ceil() / texel_scale
}
fn snap_shadow_view_to_texels(light_view: View, center: Vector3, radius: f32, shadow_map_resolution: u32) -> View {
if shadow_map_resolution == 0 {
return light_view;
}
let texel_size = (2.0 * radius) / shadow_map_resolution as f32;
if texel_size <= 0.0 {
return light_view;
}
let light_space_center = light_view.view() * Vector4::new(center.x, center.y, center.z, 1.0);
let snapped_center = Vector3::new(
(light_space_center.x / texel_size).round() * texel_size,
(light_space_center.y / texel_size).round() * texel_size,
light_space_center.z,
);
let snap_offset = Vector3::new(
snapped_center.x - light_space_center.x,
snapped_center.y - light_space_center.y,
0.0,
);
light_view.from_view(Matrix4::from_translation(snap_offset) * light_view.view())
}
#[cfg(test)]
mod tests {
use math::{assert_float_eq, length, Base as _, VecN as _, Vector3, Vector4};
use crate::rendering::view::View;
#[test]
fn cascade_split_ranges_partition_the_camera_frustum() {
let camera_view = View::new_perspective(90.0, 1.0, 0.1, 100.0, Vector3::zero(), Vector3::unit_z());
let cascade_ranges = super::make_cascade_split_ranges(camera_view, 4);
let mut expected_near = camera_view.near();
for (cascade_near, cascade_far) in cascade_ranges {
assert_float_eq!(
cascade_near,
expected_near,
"Cascade near plane should continue from the previous split"
);
assert!(
cascade_far > cascade_near,
"Cascade far plane should lie beyond the cascade near plane"
);
expected_near = cascade_far;
}
assert_float_eq!(
expected_near,
camera_view.far(),
"Cascade splits should reach the camera far plane"
);
}
#[test]
fn shadow_view_keeps_cascade_center_in_front_of_the_light() {
let camera_view = View::new_perspective(90.0, 1.0, 0.1, 100.0, Vector3::zero(), Vector3::unit_z());
let shadow_view = super::make_csm_views(camera_view, Vector3::unit_z(), 1, 2048)
.next()
.expect("A shadow cascade view should be generated");
let corners = camera_view.get_frustum_corners();
let center = corners
.iter()
.copied()
.fold(math::Vector4::new(0.0, 0.0, 0.0, 0.0), |acc, corner| acc + corner)
/ corners.len() as f32;
let light_space_center = shadow_view.view() * center;
assert!(
light_space_center.z >= 0.0,
"Cascade center should be in front of the light view"
);
assert!(
light_space_center.z <= shadow_view.far(),
"Cascade center should lie inside the shadow depth range"
);
}
#[test]
fn surface_point_projects_into_valid_shadow_ndc() {
let camera_view = View::new_perspective(90.0, 1.0, 0.1, 100.0, Vector3::zero(), Vector3::unit_z());
let light_direction = math::normalize(Vector3::new(0.5, -1.0, 0.3));
let num_cascades = 4;
let cascade_views = super::make_csm_views(camera_view, light_direction, num_cascades, 2048);
let cascade_ranges = super::make_cascade_split_ranges(camera_view, num_cascades);
for (cascade_idx, ((cascade_near, cascade_far), cascade_view)) in cascade_ranges.zip(cascade_views).enumerate() {
let mid_depth = (cascade_near + cascade_far) / 2.0;
let surface_point = math::Vector4::new(0.0, 0.0, mid_depth, 1.0);
let clip = cascade_view.view_projection() * surface_point;
let ndc = Vector3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
assert!(
ndc.x >= -1.0 && ndc.x <= 1.0,
"Cascade {}: NDC x ({}) out of range for surface at depth {}",
cascade_idx,
ndc.x,
mid_depth
);
assert!(
ndc.y >= -1.0 && ndc.y <= 1.0,
"Cascade {}: NDC y ({}) out of range for surface at depth {}",
cascade_idx,
ndc.y,
mid_depth
);
assert!(
ndc.z >= 0.0 && ndc.z <= 1.0,
"Cascade {}: NDC z ({}) out of [0,1] for surface at depth {}",
cascade_idx,
ndc.z,
mid_depth
);
}
}
#[test]
fn shadow_view_matrices_are_valid_for_various_light_directions() {
let camera_view = View::new_perspective(90.0, 1.0, 0.1, 100.0, Vector3::zero(), Vector3::unit_z());
let light_directions = [
Vector3::new(0.0, -1.0, 0.0), Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.5, -1.0, 0.3), Vector3::new(-0.2, -0.8, 0.5), ];
for light_dir in &light_directions {
let cascade_views = super::make_csm_views(camera_view, *light_dir, 4, 2048);
for (i, view) in cascade_views.enumerate() {
let m = view.view();
let r0 = Vector3::new(m[0], m[1], m[2]);
let r1 = Vector3::new(m[4], m[5], m[6]);
let r2 = Vector3::new(m[8], m[9], m[10]);
let len0 = math::length(r0);
let len1 = math::length(r1);
let len2 = math::length(r2);
assert!(
(len0 - 1.0).abs() < 1e-5,
"Light {:?}, cascade {}: row 0 length = {}",
light_dir,
i,
len0
);
assert!(
(len1 - 1.0).abs() < 1e-5,
"Light {:?}, cascade {}: row 1 length = {}",
light_dir,
i,
len1
);
assert!(
(len2 - 1.0).abs() < 1e-5,
"Light {:?}, cascade {}: row 2 length = {}",
light_dir,
i,
len2
);
let d01 = math::dot(r0, r1);
let d02 = math::dot(r0, r2);
let d12 = math::dot(r1, r2);
assert!(d01.abs() < 1e-5, "Light {:?}, cascade {}: dot(r0,r1) = {}", light_dir, i, d01);
assert!(d02.abs() < 1e-5, "Light {:?}, cascade {}: dot(r0,r2) = {}", light_dir, i, d02);
assert!(d12.abs() < 1e-5, "Light {:?}, cascade {}: dot(r1,r2) = {}", light_dir, i, d12);
}
}
}
#[test]
fn shadow_view_snaps_cascade_center_to_texel_grid() {
let camera_view = View::new_perspective(
75.0,
16.0 / 9.0,
0.1,
100.0,
Vector3::new(0.37, -1.12, 2.83),
Vector3::unit_z(),
);
let light_direction = math::normalize(Vector3::new(0.5, -1.0, 0.3));
let resolution = 1024;
let shadow_view = super::make_csm_views(camera_view, light_direction, 1, resolution)
.next()
.expect("A shadow cascade view should be generated");
let frustum_corners = camera_view.get_frustum_corners();
let center = frustum_corners.iter().fold(Vector4::zero(), |acc, x| acc + *x) / 8.0;
let radius = super::stabilize_cascade_radius(center, &frustum_corners, resolution);
let texel_size = (2.0 * radius) / resolution as f32;
let light_space_center = shadow_view.view() * center;
assert!((light_space_center.x / texel_size).fract().abs() < 1e-4);
assert!((light_space_center.y / texel_size).fract().abs() < 1e-4);
}
#[test]
fn cascade_radius_is_quantized_to_stable_texel_steps() {
let camera_view = View::new_perspective(
75.0,
16.0 / 9.0,
0.1,
100.0,
Vector3::new(0.37, -1.12, 2.83),
Vector3::unit_z(),
);
let frustum_corners = camera_view.get_frustum_corners();
let center = frustum_corners.iter().fold(Vector4::zero(), |acc, x| acc + *x) / 8.0;
let resolution = 1024;
let radius = super::stabilize_cascade_radius(center, &frustum_corners, resolution);
assert!(((radius * resolution as f32) / 2.0).fract().abs() < 1e-4);
}
}