Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add orthographic camera support back to directional shadows #7796

Closed
wants to merge 15 commits into from
Closed
98 changes: 55 additions & 43 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashSet;

use bevy_ecs::prelude::*;
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_math::{Mat4, Rect, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_reflect::prelude::*;
use bevy_render::{
camera::Camera,
Expand Down Expand Up @@ -413,19 +413,12 @@ pub fn update_directional_light_cascades(
) {
let views = views
.iter()
.filter_map(|view| match view {
// TODO: orthographic camera projection support.
(entity, transform, Projection::Perspective(projection), camera)
if camera.is_active =>
{
Some((
entity,
projection.aspect_ratio,
(0.5 * projection.fov).tan(),
transform.compute_matrix(),
))
.filter_map(|(entity, transform, projection, camera)| {
if camera.is_active {
Some((entity, projection, transform.compute_matrix()))
} else {
None
}
_ => None,
})
.collect::<Vec<_>>();

Expand All @@ -444,27 +437,38 @@ pub fn update_directional_light_cascades(
let light_to_world_inverse = light_to_world.inverse();

cascades.cascades.clear();
for (view_entity, aspect_ratio, tan_half_fov, view_to_world) in views.iter().copied() {
for (view_entity, projection, view_to_world) in views.iter().copied() {
let camera_to_light_view = light_to_world_inverse * view_to_world;
let view_cascades = cascades_config
.bounds
.iter()
.enumerate()
.map(|(idx, far_bound)| {
// Negate bounds as -z is camera forward direction.
let z_near = if idx > 0 {
(1.0 - cascades_config.overlap_proportion)
* -cascades_config.bounds[idx - 1]
} else {
-cascades_config.minimum_distance
};
let z_far = -far_bound;

let corners = match projection {
Projection::Perspective(projection) => frustum_corners(
projection.aspect_ratio,
(projection.fov / 2.).tan(),
z_near,
z_far,
),
Projection::Orthographic(projection) => {
frustum_corners_ortho(projection.area, z_near, z_far)
}
};
calculate_cascade(
aspect_ratio,
tan_half_fov,
corners,
directional_light_shadow_map.size as f32,
light_to_world,
camera_to_light_view,
// Negate bounds as -z is camera forward direction.
if idx > 0 {
(1.0 - cascades_config.overlap_proportion)
* -cascades_config.bounds[idx - 1]
} else {
-cascades_config.minimum_distance
},
-far_bound,
)
})
.collect();
Expand All @@ -473,27 +477,25 @@ pub fn update_directional_light_cascades(
}
}

fn calculate_cascade(
aspect_ratio: f32,
tan_half_fov: f32,
cascade_texture_size: f32,
light_to_world: Mat4,
camera_to_light: Mat4,
z_near: f32,
z_far: f32,
) -> Cascade {
debug_assert!(z_near <= 0.0, "z_near {z_near} must be <= 0.0");
debug_assert!(z_far <= 0.0, "z_far {z_far} must be <= 0.0");
// NOTE: This whole function is very sensitive to floating point precision and instability and
// has followed instructions to avoid view dependence from the section on cascade shadow maps in
// Eric Lengyel's Foundations of Game Engine Development 2: Rendering. Be very careful when
// modifying this code!
fn frustum_corners_ortho(area: Rect, z_near: f32, z_far: f32) -> [Vec3A; 8] {
// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(area.max.x, area.min.y, z_near),
Vec3A::new(area.max.x, area.max.y, z_near),
Vec3A::new(area.min.x, area.max.y, z_near),
Vec3A::new(area.min.x, area.min.y, z_near),
Vec3A::new(area.min.x, area.min.y, z_far),
Vec3A::new(area.max.x, area.min.y, z_far),
Vec3A::new(area.min.x, area.max.y, z_far),
Vec3A::new(area.min.x, area.min.y, z_far),
]
}

fn frustum_corners(aspect_ratio: f32, tan_half_fov: f32, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let a = z_near.abs() * tan_half_fov;
let b = z_far.abs() * tan_half_fov;
// NOTE: These vertices are in a specific order: bottom right, top right, top left, bottom left
// for near then for far
let frustum_corners = [
// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(a * aspect_ratio, -a, z_near),
Vec3A::new(a * aspect_ratio, a, z_near),
Vec3A::new(-a * aspect_ratio, a, z_near),
Expand All @@ -502,8 +504,18 @@ fn calculate_cascade(
Vec3A::new(b * aspect_ratio, b, z_far),
Vec3A::new(-b * aspect_ratio, b, z_far),
Vec3A::new(-b * aspect_ratio, -b, z_far),
];
]
}

/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`.
/// The corner vertices should be specified in the following order:
/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane.
fn calculate_cascade(
frustum_corners: [Vec3A; 8],
cascade_texture_size: f32,
light_to_world: Mat4,
camera_to_light: Mat4,
) -> Cascade {
let mut min = Vec3A::splat(f32::MAX);
let mut max = Vec3A::splat(f32::MIN);
for corner_camera_view in frustum_corners {
Expand Down