Make DirectionalLight Cascades computation generic over CameraProjection (#9226)

# Objective

Fixes https://github.com/bevyengine/bevy/issues/9077 (see this issue for
motivations)

## Solution

Implement 1 and 2 of the "How to fix it" section of
https://github.com/bevyengine/bevy/issues/9077

`update_directional_light_cascades` is split into
`clear_directional_light_cascades` and a generic
`build_directional_light_cascades`, to clear once and potentially insert
many times.

---

## Changelog

`DirectionalLight`'s computation is now generic over `CameraProjection`
and can work with custom camera projections.

## Migration Guide

If you have a component `MyCustomProjection` that implements
`CameraProjection`:
- You need to implement a new required associated method,
`get_frustum_corners`, returning an array of the corners of a subset of
the frustum with given `z_near` and `z_far`, in local camera space.
- You can now add the
`build_directional_light_cascades::<MyCustomProjection>` system in
`SimulationLightSystems::UpdateDirectionalLightCascades` after
`clear_directional_light_cascades` for your projection to work with
directional lights.

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Sélène Amanita 2023-11-03 06:07:59 +00:00 committed by GitHub
parent 3ec52c2bdb
commit c376954b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 53 deletions

View File

@ -58,10 +58,17 @@ use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_render::{ use bevy_render::{
camera::CameraUpdateSystem, extract_component::ExtractComponentPlugin, camera::{CameraUpdateSystem, Projection},
extract_resource::ExtractResourcePlugin, prelude::Color, render_asset::prepare_assets, extract_component::ExtractComponentPlugin,
render_graph::RenderGraph, render_phase::sort_phase_system, render_resource::Shader, extract_resource::ExtractResourcePlugin,
texture::Image, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderSet, prelude::Color,
render_asset::prepare_assets,
render_graph::RenderGraph,
render_phase::sort_phase_system,
render_resource::Shader,
texture::Image,
view::VisibilitySystems,
ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_transform::TransformSystem; use bevy_transform::TransformSystem;
use environment_map::EnvironmentMapPlugin; use environment_map::EnvironmentMapPlugin;
@ -273,7 +280,11 @@ impl Plugin for PbrPlugin {
.after(TransformSystem::TransformPropagate) .after(TransformSystem::TransformPropagate)
.after(VisibilitySystems::CheckVisibility) .after(VisibilitySystems::CheckVisibility)
.after(CameraUpdateSystem), .after(CameraUpdateSystem),
update_directional_light_cascades (
clear_directional_light_cascades,
build_directional_light_cascades::<Projection>,
)
.chain()
.in_set(SimulationLightSystems::UpdateDirectionalLightCascades) .in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
.after(TransformSystem::TransformPropagate) .after(TransformSystem::TransformPropagate)
.after(CameraUpdateSystem), .after(CameraUpdateSystem),

View File

@ -1,14 +1,13 @@
use std::collections::HashSet; use std::collections::HashSet;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_math::{Mat4, Rect, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::{ use bevy_render::{
camera::Camera, camera::{Camera, CameraProjection},
color::Color, color::Color,
extract_component::ExtractComponent, extract_component::ExtractComponent,
extract_resource::ExtractResource, extract_resource::ExtractResource,
prelude::Projection,
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere}, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere},
render_resource::BufferBindingType, render_resource::BufferBindingType,
renderer::RenderDevice, renderer::RenderDevice,
@ -397,9 +396,18 @@ pub struct Cascade {
pub(crate) texel_size: f32, pub(crate) texel_size: f32,
} }
pub fn update_directional_light_cascades( pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) {
for (directional_light, mut cascades) in lights.iter_mut() {
if !directional_light.shadows_enabled {
continue;
}
cascades.cascades.clear();
}
}
pub fn build_directional_light_cascades<P: CameraProjection + Component>(
directional_light_shadow_map: Res<DirectionalLightShadowMap>, directional_light_shadow_map: Res<DirectionalLightShadowMap>,
views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, views: Query<(Entity, &GlobalTransform, &P, &Camera)>,
mut lights: Query<( mut lights: Query<(
&GlobalTransform, &GlobalTransform,
&DirectionalLight, &DirectionalLight,
@ -432,7 +440,6 @@ pub fn update_directional_light_cascades(
let light_to_world = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world = Mat4::from_quat(transform.compute_transform().rotation);
let light_to_world_inverse = light_to_world.inverse(); let light_to_world_inverse = light_to_world.inverse();
cascades.cascades.clear();
for (view_entity, projection, 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 camera_to_light_view = light_to_world_inverse * view_to_world;
let view_cascades = cascades_config let view_cascades = cascades_config
@ -449,17 +456,8 @@ pub fn update_directional_light_cascades(
}; };
let z_far = -far_bound; let z_far = -far_bound;
let corners = match projection { let corners = projection.get_frustum_corners(z_near, z_far);
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( calculate_cascade(
corners, corners,
directional_light_shadow_map.size as f32, directional_light_shadow_map.size as f32,
@ -473,36 +471,6 @@ pub fn update_directional_light_cascades(
} }
} }
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), // bottom right
Vec3A::new(area.max.x, area.max.y, z_near), // top right
Vec3A::new(area.min.x, area.max.y, z_near), // top left
Vec3A::new(area.min.x, area.min.y, z_near), // bottom left
Vec3A::new(area.max.x, area.min.y, z_far), // bottom right
Vec3A::new(area.max.x, area.max.y, z_far), // top right
Vec3A::new(area.min.x, area.max.y, z_far), // top left
Vec3A::new(area.min.x, area.min.y, z_far), // bottom left
]
}
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 the specific order required by [`calculate_cascade`].
[
Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right
Vec3A::new(a * aspect_ratio, a, z_near), // top right
Vec3A::new(-a * aspect_ratio, a, z_near), // top left
Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left
Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right
Vec3A::new(b * aspect_ratio, b, z_far), // top right
Vec3A::new(-b * aspect_ratio, b, z_far), // top left
Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left
]
}
/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. /// Returns a [`Cascade`] for the frustum defined by `frustum_corners`.
/// The corner vertices should be specified in the following order: /// 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. /// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane.

View File

@ -2,7 +2,7 @@ use std::marker::PhantomData;
use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_app::{App, Plugin, PostStartup, PostUpdate};
use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_math::{Mat4, Rect, Vec2}; use bevy_math::{Mat4, Rect, Vec2, Vec3A};
use bevy_reflect::{ use bevy_reflect::{
std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize,
}; };
@ -58,6 +58,7 @@ pub trait CameraProjection {
fn get_projection_matrix(&self) -> Mat4; fn get_projection_matrix(&self) -> Mat4;
fn update(&mut self, width: f32, height: f32); fn update(&mut self, width: f32, height: f32);
fn far(&self) -> f32; fn far(&self) -> f32;
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];
} }
/// A configurable [`CameraProjection`] that can select its projection type at runtime. /// A configurable [`CameraProjection`] that can select its projection type at runtime.
@ -101,6 +102,13 @@ impl CameraProjection for Projection {
Projection::Orthographic(projection) => projection.far(), Projection::Orthographic(projection) => projection.far(),
} }
} }
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
match self {
Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far),
Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far),
}
}
} }
impl Default for Projection { impl Default for Projection {
@ -153,6 +161,24 @@ impl CameraProjection for PerspectiveProjection {
fn far(&self) -> f32 { fn far(&self) -> f32 {
self.far self.far
} }
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let tan_half_fov = (self.fov / 2.).tan();
let a = z_near.abs() * tan_half_fov;
let b = z_far.abs() * tan_half_fov;
let aspect_ratio = self.aspect_ratio;
// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right
Vec3A::new(a * aspect_ratio, a, z_near), // top right
Vec3A::new(-a * aspect_ratio, a, z_near), // top left
Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left
Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right
Vec3A::new(b * aspect_ratio, b, z_far), // top right
Vec3A::new(-b * aspect_ratio, b, z_far), // top left
Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left
]
}
} }
impl Default for PerspectiveProjection { impl Default for PerspectiveProjection {
@ -309,6 +335,21 @@ impl CameraProjection for OrthographicProjection {
fn far(&self) -> f32 { fn far(&self) -> f32 {
self.far self.far
} }
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let area = self.area;
// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(area.max.x, area.min.y, z_near), // bottom right
Vec3A::new(area.max.x, area.max.y, z_near), // top right
Vec3A::new(area.min.x, area.max.y, z_near), // top left
Vec3A::new(area.min.x, area.min.y, z_near), // bottom left
Vec3A::new(area.max.x, area.min.y, z_far), // bottom right
Vec3A::new(area.max.x, area.max.y, z_far), // top right
Vec3A::new(area.min.x, area.max.y, z_far), // top left
Vec3A::new(area.min.x, area.min.y, z_far), // bottom left
]
}
} }
impl Default for OrthographicProjection { impl Default for OrthographicProjection {