use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Point3, Vector2, Vector3},
color::Color,
math::{aabb::AxisAlignedBoundingBox, frustum::Frustum, Rect},
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
bundle::{
BundleRenderContext, LightSource, LightSourceKind, RenderDataBundleStorage,
RenderDataBundleStorageOptions,
},
cache::{
geometry::GeometryCache, shader::ShaderCache, texture::TextureCache,
uniform::UniformMemoryAllocator, DynamicSurfaceCache,
},
observer::{Observer, ObserverPosition},
resources::RendererResources,
settings::ShadowMapPrecision,
RenderPassStatistics, DIRECTIONAL_SHADOW_PASS_NAME,
},
scene::{
graph::Graph,
light::directional::{FrustumSplitOptions, CSM_NUM_CASCADES},
},
};
use approx::relative_eq;
pub struct Cascade {
pub frame_buffer: GpuFrameBuffer,
pub view_proj_matrix: Matrix4<f32>,
pub z_far: f32,
}
impl Cascade {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
let depth = server.create_2d_render_target(
"CsmCascadeTexture",
match precision {
ShadowMapPrecision::Full => PixelKind::D32F,
ShadowMapPrecision::Half => PixelKind::D16,
},
size,
size,
)?;
Ok(Self {
frame_buffer: server
.create_frame_buffer(Some(Attachment::depth(depth)), Default::default())?,
view_proj_matrix: Default::default(),
z_far: 0.0,
})
}
pub fn texture(&self) -> &GpuTexture {
&self.frame_buffer.depth_attachment().unwrap().texture
}
}
pub struct CsmRenderer {
cascades: [Cascade; CSM_NUM_CASCADES],
size: usize,
precision: ShadowMapPrecision,
}
pub(crate) struct CsmRenderContext<'a, 'c> {
pub elapsed_time: f32,
pub frame_size: Vector2<f32>,
pub server: &'a dyn GraphicsServer,
pub graph: &'c Graph,
pub light: &'c LightSource,
pub observer: &'a Observer,
pub geom_cache: &'a mut GeometryCache,
pub shader_cache: &'a mut ShaderCache,
pub texture_cache: &'a mut TextureCache,
pub renderer_resources: &'a RendererResources,
pub uniform_memory_allocator: &'a mut UniformMemoryAllocator,
pub dynamic_surface_cache: &'a mut DynamicSurfaceCache,
pub resource_manager: &'a ResourceManager,
}
impl CsmRenderer {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
Ok(Self {
precision,
size,
cascades: [
Cascade::new(server, size, precision)?,
Cascade::new(server, size, precision)?,
Cascade::new(server, size, precision)?,
],
})
}
pub fn precision(&self) -> ShadowMapPrecision {
self.precision
}
pub fn size(&self) -> usize {
self.size
}
pub fn cascades(&self) -> &[Cascade] {
&self.cascades
}
pub(crate) fn render(
&mut self,
ctx: CsmRenderContext,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = ctx.server.begin_scope("CascadedShadowMap");
let mut stats = RenderPassStatistics::default();
let CsmRenderContext {
elapsed_time,
frame_size,
server,
graph,
light,
observer,
geom_cache,
shader_cache,
texture_cache,
renderer_resources,
uniform_memory_allocator,
dynamic_surface_cache,
resource_manager,
} = ctx;
let LightSourceKind::Directional { ref csm_options } = light.kind else {
return Ok(stats);
};
let light_direction = -light
.up_vector
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::y);
let light_up_vec = light
.look_vector
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::z);
let z_values = match csm_options.split_options {
FrustumSplitOptions::Absolute { far_planes } => [
observer.position.z_near,
far_planes[0],
far_planes[1],
far_planes[2],
],
FrustumSplitOptions::Relative { fractions } => [
observer.position.z_near,
observer.position.z_far * fractions[0],
observer.position.z_far * fractions[1],
observer.position.z_far * fractions[2],
],
};
for i in 0..CSM_NUM_CASCADES {
let _debug_scope = ctx.server.begin_scope(&format!("Cascade {i}"));
let z_near = z_values[i];
let mut z_far = z_values[i + 1];
if relative_eq!(z_far, z_near) {
const MIN_DEPTH_DELTA: f32 = f32::EPSILON * 2.0;
z_far += MIN_DEPTH_DELTA * z_near;
}
let projection_matrix = observer
.projection
.clone()
.with_z_near(z_near)
.with_z_far(z_far)
.matrix(frame_size);
let frustum = Frustum::from_view_projection_matrix(
projection_matrix * observer.position.view_matrix,
)
.unwrap_or_default();
let center = frustum.center();
let observer_position = center + light_direction;
let light_view_matrix = Matrix4::look_at_lh(
&Point3::from(observer_position),
&Point3::from(center),
&light_up_vec,
);
let mut aabb = AxisAlignedBoundingBox::default();
for corner in frustum.corners() {
let light_space_corner = light_view_matrix
.transform_point(&Point3::from(corner))
.coords;
aabb.add_point(light_space_corner);
}
let z_mult = 10.0;
if aabb.min.z < 0.0 {
aabb.min.z *= z_mult;
} else {
aabb.min.z /= z_mult;
}
if aabb.max.z < 0.0 {
aabb.max.z /= z_mult;
} else {
aabb.max.z *= z_mult;
}
let cascade_projection_matrix = Matrix4::new_orthographic(
aabb.min.x, aabb.max.x, aabb.min.y, aabb.max.y, aabb.min.z, aabb.max.z,
);
let light_view_projection = cascade_projection_matrix * light_view_matrix;
self.cascades[i].view_proj_matrix = light_view_projection;
self.cascades[i].z_far = z_far;
let viewport = Rect::new(0, 0, self.size as i32, self.size as i32);
let framebuffer = &self.cascades[i].frame_buffer;
framebuffer.clear(viewport, None, Some(1.0), None);
let bundle_storage = RenderDataBundleStorage::from_graph(
graph,
observer.render_mask,
elapsed_time,
&ObserverPosition {
translation: observer_position,
z_near,
z_far,
view_matrix: light_view_matrix,
projection_matrix: cascade_projection_matrix,
view_projection_matrix: cascade_projection_matrix * light_view_matrix,
},
DIRECTIONAL_SHADOW_PASS_NAME.clone(),
RenderDataBundleStorageOptions {
collect_lights: false,
},
dynamic_surface_cache,
);
stats += bundle_storage.render_to_frame_buffer(
server,
geom_cache,
shader_cache,
|_| true,
|_| true,
BundleRenderContext {
texture_cache,
render_pass_name: &DIRECTIONAL_SHADOW_PASS_NAME,
frame_buffer: framebuffer,
viewport,
uniform_memory_allocator,
resource_manager,
use_pom: false,
light_position: &Default::default(),
renderer_resources,
ambient_light: Color::WHITE, scene_depth: None,
},
)?;
}
Ok(stats)
}
}