pub(super) fn reverse_z_ortho_light(
left: f32,
right: f32,
bottom: f32,
top: f32,
near: f32,
far: f32,
) -> nalgebra_glm::Mat4 {
let width = right - left;
let height = top - bottom;
let depth = far - near;
nalgebra_glm::Mat4::new(
2.0 / width,
0.0,
0.0,
-(right + left) / width,
0.0,
2.0 / height,
0.0,
-(top + bottom) / height,
0.0,
0.0,
1.0 / depth,
-near / depth,
0.0,
0.0,
0.0,
1.0,
)
}
pub(super) fn get_frustum_corners_world_space(
view: &nalgebra_glm::Mat4,
fov: f32,
aspect: f32,
near: f32,
far: f32,
) -> [nalgebra_glm::Vec3; 8] {
let inv_view = nalgebra_glm::inverse(view);
let tan_half_fov = (fov / 2.0).tan();
let near_height = near * tan_half_fov;
let near_width = near_height * aspect;
let far_height = far * tan_half_fov;
let far_width = far_height * aspect;
let corners_view = [
nalgebra_glm::vec3(-near_width, -near_height, -near),
nalgebra_glm::vec3(near_width, -near_height, -near),
nalgebra_glm::vec3(near_width, near_height, -near),
nalgebra_glm::vec3(-near_width, near_height, -near),
nalgebra_glm::vec3(-far_width, -far_height, -far),
nalgebra_glm::vec3(far_width, -far_height, -far),
nalgebra_glm::vec3(far_width, far_height, -far),
nalgebra_glm::vec3(-far_width, far_height, -far),
];
let mut corners_world = [nalgebra_glm::vec3(0.0, 0.0, 0.0); 8];
for (index, corner) in corners_view.iter().enumerate() {
let world_pos = inv_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
corners_world[index] = nalgebra_glm::vec3(world_pos.x, world_pos.y, world_pos.z);
}
corners_world
}
pub struct CascadeViewProjectionResult {
pub view_projection: nalgebra_glm::Mat4,
pub cascade_diameter: f32,
}
pub(super) fn calculate_cascade_view_projection(
frustum_corners: &[nalgebra_glm::Vec3; 8],
light_direction: &nalgebra_glm::Vec3,
_cascade_resolution: f32,
_cascade_far: f32,
) -> CascadeViewProjectionResult {
let mut center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
for corner in frustum_corners {
center += corner;
}
center /= 8.0;
let mut max_radius = 0.0f32;
for corner in frustum_corners {
let dist = nalgebra_glm::length(&(corner - center));
max_radius = max_radius.max(dist);
}
let up = if light_direction.y.abs() > 0.99 {
nalgebra_glm::vec3(1.0, 0.0, 0.0)
} else {
nalgebra_glm::vec3(0.0, 1.0, 0.0)
};
let light_dir_normalized = light_direction.normalize();
let light_view = nalgebra_glm::look_at(
&(center - light_dir_normalized * max_radius * 4.0),
¢er,
&up,
);
let mut min_x = f32::MAX;
let mut max_x = f32::MIN;
let mut min_y = f32::MAX;
let mut max_y = f32::MIN;
let mut min_z = f32::MAX;
let mut max_z = f32::MIN;
for corner in frustum_corners {
let light_space = light_view * nalgebra_glm::vec4(corner.x, corner.y, corner.z, 1.0);
min_x = min_x.min(light_space.x);
max_x = max_x.max(light_space.x);
min_y = min_y.min(light_space.y);
max_y = max_y.max(light_space.y);
min_z = min_z.min(light_space.z);
max_z = max_z.max(light_space.z);
}
let padding = (max_x - min_x).max(max_y - min_y) * 0.1;
min_x -= padding;
max_x += padding;
min_y -= padding;
max_y += padding;
let z_mult = 10.0;
if min_z < 0.0 {
min_z *= z_mult;
} else {
min_z /= z_mult;
}
if max_z < 0.0 {
max_z /= z_mult;
} else {
max_z *= z_mult;
}
let light_projection = reverse_z_ortho_light(min_x, max_x, min_y, max_y, min_z, max_z);
let cascade_diameter = max_radius * 2.0;
CascadeViewProjectionResult {
view_projection: light_projection * light_view,
cascade_diameter,
}
}
pub(super) fn reverse_z_perspective(
fov: f32,
aspect: f32,
near: f32,
far: f32,
) -> nalgebra_glm::Mat4 {
let f = 1.0 / (fov / 2.0).tan();
let depth = far - near;
nalgebra_glm::Mat4::new(
f / aspect,
0.0,
0.0,
0.0,
0.0,
f,
0.0,
0.0,
0.0,
0.0,
near / depth,
near * far / depth,
0.0,
0.0,
-1.0,
0.0,
)
}
pub(super) fn extract_frustum_planes(
view_proj: &crate::ecs::world::Mat4,
) -> [crate::ecs::world::Vec4; 6] {
let mut planes = [crate::ecs::world::Vec4::zeros(); 6];
let row0 = crate::ecs::world::Vec4::new(
view_proj[(0, 0)],
view_proj[(0, 1)],
view_proj[(0, 2)],
view_proj[(0, 3)],
);
let row1 = crate::ecs::world::Vec4::new(
view_proj[(1, 0)],
view_proj[(1, 1)],
view_proj[(1, 2)],
view_proj[(1, 3)],
);
let row2 = crate::ecs::world::Vec4::new(
view_proj[(2, 0)],
view_proj[(2, 1)],
view_proj[(2, 2)],
view_proj[(2, 3)],
);
let row3 = crate::ecs::world::Vec4::new(
view_proj[(3, 0)],
view_proj[(3, 1)],
view_proj[(3, 2)],
view_proj[(3, 3)],
);
planes[0] = row3 + row0;
planes[1] = row3 - row0;
planes[2] = row3 + row1;
planes[3] = row3 - row1;
planes[4] = row2;
planes[5] = row3 - row2;
for plane in &mut planes {
let normal_length = (plane.x * plane.x + plane.y * plane.y + plane.z * plane.z).sqrt();
if normal_length > 1e-6 {
*plane /= normal_length;
}
}
planes
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightData {
pub position: [f32; 4],
pub direction: [f32; 4],
pub color: [f32; 4],
pub light_type: u32,
pub range: f32,
pub inner_cone: f32,
pub outer_cone: f32,
pub shadow_index: i32,
pub light_size: f32,
pub _padding: [f32; 2],
}
pub const CASCADE_SPLIT_DISTANCES: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES] =
[10.0, 40.0, 150.0, 500.0];
pub const MAX_SPOTLIGHT_SHADOWS: usize = 16;
pub struct LightCollectionResult {
pub lights_data: Vec<LightData>,
pub directional_light: Option<(
crate::ecs::light::components::Light,
crate::ecs::transform::components::GlobalTransform,
)>,
pub num_directional_lights: u32,
pub entity_to_index: std::collections::HashMap<crate::ecs::world::Entity, usize>,
}
pub fn collect_lights(
world: &crate::ecs::world::World,
max_lights: usize,
) -> LightCollectionResult {
let mut directional_lights = Vec::new();
let mut local_lights = Vec::new();
let mut directional_light = None;
let mut directional_entities = Vec::new();
let mut local_entities = Vec::new();
for entity in world
.core
.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
{
if let (Some(light), Some(transform)) = (
world.core.get_light(entity),
world.core.get_global_transform(entity),
) {
let light_type = match light.light_type {
crate::ecs::light::components::LightType::Directional => 0,
crate::ecs::light::components::LightType::Point => 1,
crate::ecs::light::components::LightType::Spot => 2,
};
if light_type == 0 && light.cast_shadows && directional_light.is_none() {
directional_light = Some((light.clone(), *transform));
}
let position = transform.translation();
let direction = transform.forward_vector();
let light_size = match light.light_type {
crate::ecs::light::components::LightType::Directional => 1.0,
crate::ecs::light::components::LightType::Point => (light.range * 0.05).max(0.1),
crate::ecs::light::components::LightType::Spot => (light.range * 0.03).max(0.1),
};
let light_data = LightData {
position: [position.x, position.y, position.z, 1.0],
direction: [direction.x, direction.y, direction.z, 0.0],
color: [
light.color.x * light.intensity,
light.color.y * light.intensity,
light.color.z * light.intensity,
1.0,
],
light_type,
range: light.range,
inner_cone: light.inner_cone_angle.cos(),
outer_cone: light.outer_cone_angle.cos(),
shadow_index: -1,
light_size,
_padding: [0.0; 2],
};
if light_type == 0 {
directional_lights.push(light_data);
directional_entities.push(entity);
} else {
local_lights.push(light_data);
local_entities.push(entity);
}
if directional_lights.len() + local_lights.len() >= max_lights {
break;
}
}
}
let num_directional_lights = directional_lights.len() as u32;
let mut lights_data = directional_lights;
lights_data.extend(local_lights);
let mut entity_to_index = std::collections::HashMap::new();
for (index, entity) in directional_entities.iter().enumerate() {
entity_to_index.insert(*entity, index);
}
let offset = directional_entities.len();
for (index, entity) in local_entities.iter().enumerate() {
entity_to_index.insert(*entity, offset + index);
}
LightCollectionResult {
lights_data,
directional_light,
num_directional_lights,
entity_to_index,
}
}
pub struct CascadeShadowResult {
pub cascade_view_projections: [[[f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
pub cascade_diameters: [f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES],
pub light_view_projection: [[f32; 4]; 4],
pub shadow_bias: f32,
pub shadows_enabled: f32,
}
pub fn calculate_cascade_shadows(
world: &crate::ecs::world::World,
directional_light: Option<&(
crate::ecs::light::components::Light,
crate::ecs::transform::components::GlobalTransform,
)>,
) -> CascadeShadowResult {
let mut cascade_view_projections =
[[[0.0f32; 4]; 4]; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];
let mut cascade_diameters = [0.0f32; crate::render::wgpu::passes::NUM_SHADOW_CASCADES];
let (light_view_projection, shadow_bias, shadows_enabled) =
if let Some((_light, light_transform)) = directional_light {
let light_direction = light_transform.forward_vector();
let camera_matrices = crate::ecs::camera::queries::query_active_camera_matrices(world);
if let Some(camera_matrices) = camera_matrices {
let active_camera = world.resources.active_camera;
let camera = active_camera.and_then(|entity| world.core.get_camera(entity));
let (fov, aspect, camera_near) = if let Some(camera) = camera {
match &camera.projection {
crate::ecs::camera::components::Projection::Perspective(persp) => {
let aspect = persp.aspect_ratio.unwrap_or_else(|| {
crate::ecs::camera::queries::query_window_aspect_ratio(world)
.unwrap_or(1.78)
});
(persp.y_fov_rad, aspect, persp.z_near)
}
crate::ecs::camera::components::Projection::Orthographic(_) => {
(std::f32::consts::FRAC_PI_4, 1.78, 0.1)
}
}
} else {
(std::f32::consts::FRAC_PI_4, 1.78, 0.1)
};
let cascade_resolution = if cfg!(target_arch = "wasm32") {
2048.0
} else {
4096.0
};
for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
let cascade_near = if cascade_index == 0 {
camera_near
} else {
CASCADE_SPLIT_DISTANCES[cascade_index - 1]
};
let cascade_far = CASCADE_SPLIT_DISTANCES[cascade_index];
let frustum_corners = get_frustum_corners_world_space(
&camera_matrices.view,
fov,
aspect,
cascade_near,
cascade_far,
);
let result = calculate_cascade_view_projection(
&frustum_corners,
&light_direction,
cascade_resolution,
cascade_far,
);
cascade_view_projections[cascade_index] = result.view_projection.into();
cascade_diameters[cascade_index] = result.cascade_diameter;
}
} else {
let up = if light_direction.y.abs() > 0.99 {
nalgebra_glm::vec3(1.0, 0.0, 0.0)
} else {
nalgebra_glm::vec3(0.0, 1.0, 0.0)
};
for cascade_index in 0..crate::render::wgpu::passes::NUM_SHADOW_CASCADES {
let cascade_far = CASCADE_SPLIT_DISTANCES[cascade_index];
let half_size = cascade_far * 0.5;
let scene_center = nalgebra_glm::vec3(0.0, 0.0, 0.0);
let light_position = scene_center - light_direction * 100.0;
let light_view = nalgebra_glm::look_at(&light_position, &scene_center, &up);
let light_projection = reverse_z_ortho_light(
-half_size,
half_size,
-half_size,
half_size,
0.1,
cascade_far * 2.0,
);
cascade_view_projections[cascade_index] =
(light_projection * light_view).into();
cascade_diameters[cascade_index] = cascade_far;
}
}
(cascade_view_projections[0], 0.02, 1.0)
} else {
(nalgebra_glm::Mat4::identity().into(), 0.0, 0.0)
};
CascadeShadowResult {
cascade_view_projections,
cascade_diameters,
light_view_projection,
shadow_bias,
shadows_enabled,
}
}
pub struct SpotlightShadowResult {
pub shadow_data: Vec<crate::render::wgpu::passes::SpotlightShadowData>,
pub entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32>,
}
pub fn collect_spotlight_shadows(
world: &crate::ecs::world::World,
camera_position: nalgebra_glm::Vec3,
) -> SpotlightShadowResult {
let mut spotlight_candidates: Vec<(
crate::ecs::world::Entity,
f32,
crate::ecs::light::components::Light,
crate::ecs::transform::components::GlobalTransform,
)> = Vec::new();
for entity in world
.core
.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
{
if let (Some(light), Some(transform)) = (
world.core.get_light(entity),
world.core.get_global_transform(entity),
) && matches!(
light.light_type,
crate::ecs::light::components::LightType::Spot
) && light.cast_shadows
{
let light_pos = nalgebra_glm::vec3(
transform.0[(0, 3)],
transform.0[(1, 3)],
transform.0[(2, 3)],
);
let distance_sq = nalgebra_glm::length2(&(light_pos - camera_position));
spotlight_candidates.push((entity, distance_sq, light.clone(), *transform));
}
}
spotlight_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut shadow_data: Vec<crate::render::wgpu::passes::SpotlightShadowData> = Vec::new();
let mut entity_to_shadow_index: std::collections::HashMap<crate::ecs::world::Entity, i32> =
std::collections::HashMap::new();
let slot_scale = 1.0 / 4.0;
for (slot_index, (entity, _distance, light, transform)) in spotlight_candidates
.iter()
.take(MAX_SPOTLIGHT_SHADOWS)
.enumerate()
{
entity_to_shadow_index.insert(*entity, slot_index as i32);
let light_position = nalgebra_glm::vec3(
transform.0[(0, 3)],
transform.0[(1, 3)],
transform.0[(2, 3)],
);
let light_direction = transform.forward_vector();
let up = if light_direction.y.abs() > 0.99 {
nalgebra_glm::vec3(1.0, 0.0, 0.0)
} else {
nalgebra_glm::vec3(0.0, 1.0, 0.0)
};
let target = light_position + light_direction;
let light_view = nalgebra_glm::look_at(&light_position, &target, &up);
let fov = light.outer_cone_angle * 2.0;
let near = 0.1;
let far = light.range.max(1.0);
let light_projection = reverse_z_perspective(fov, 1.0, near, far);
let view_projection = light_projection * light_view;
let slot_x = (slot_index as u32) % 4;
let slot_y = (slot_index as u32) / 4;
shadow_data.push(crate::render::wgpu::passes::SpotlightShadowData {
view_projection: view_projection.into(),
atlas_offset: [slot_x as f32 * slot_scale, slot_y as f32 * slot_scale],
atlas_scale: [slot_scale, slot_scale],
bias: light.shadow_bias,
_padding: [0.0; 3],
});
}
SpotlightShadowResult {
shadow_data,
entity_to_shadow_index,
}
}
pub fn apply_spotlight_shadow_indices(
lights_data: &mut [LightData],
entity_to_shadow_index: &std::collections::HashMap<crate::ecs::world::Entity, i32>,
entity_to_lights_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
) {
for (entity, &shadow_index) in entity_to_shadow_index {
if let Some(&lights_index) = entity_to_lights_index.get(entity)
&& lights_index < lights_data.len()
{
lights_data[lights_index].shadow_index = shadow_index;
}
}
}
pub const MAX_POINT_LIGHT_SHADOWS: usize = 4;
pub fn collect_point_light_shadows(
world: &crate::ecs::world::World,
camera_position: nalgebra_glm::Vec3,
lights_data: &mut [LightData],
entity_to_lights_index: &std::collections::HashMap<crate::ecs::world::Entity, usize>,
) -> Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> {
let mut point_light_candidates: Vec<(
crate::ecs::world::Entity,
f32,
crate::ecs::light::components::Light,
crate::ecs::transform::components::GlobalTransform,
)> = Vec::new();
for entity in world
.core
.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
{
if let (Some(light), Some(transform)) = (
world.core.get_light(entity),
world.core.get_global_transform(entity),
) && matches!(
light.light_type,
crate::ecs::light::components::LightType::Point
) && light.cast_shadows
{
let light_pos = nalgebra_glm::vec3(
transform.0[(0, 3)],
transform.0[(1, 3)],
transform.0[(2, 3)],
);
let distance_sq = nalgebra_glm::length2(&(light_pos - camera_position));
point_light_candidates.push((entity, distance_sq, light.clone(), *transform));
}
}
point_light_candidates
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut shadow_data: Vec<crate::render::wgpu::passes::shadow_depth::PointLightShadowData> =
Vec::new();
for (slot_index, (entity, _distance, light, transform)) in point_light_candidates
.iter()
.take(MAX_POINT_LIGHT_SHADOWS)
.enumerate()
{
let light_position = nalgebra_glm::vec3(
transform.0[(0, 3)],
transform.0[(1, 3)],
transform.0[(2, 3)],
);
shadow_data.push(
crate::render::wgpu::passes::shadow_depth::PointLightShadowData {
position: [light_position.x, light_position.y, light_position.z],
range: light.range.max(0.1),
bias: light.shadow_bias,
shadow_index: slot_index as i32,
_padding: [0.0; 2],
},
);
if let Some(&lights_index) = entity_to_lights_index.get(entity)
&& lights_index < lights_data.len()
{
lights_data[lights_index].shadow_index = slot_index as i32;
}
}
shadow_data
}