use crate::ecs::skin::systems::GpuSkinData;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use wgpu::util::DeviceExt;
use super::atlas::{full_frustum_planes, sphere_in_frustum};
use super::cascade::{
calculate_cascade_view_projection, get_frustum_corners_world_space, reverse_z_ortho_light,
reverse_z_perspective,
};
use super::pass::ShadowDepthPass;
use super::types::{
CASCADE_ATLAS_SLOTS_PER_ROW, MAX_POINT_LIGHT_SHADOWS, MAX_SPOTLIGHT_SHADOWS,
NUM_SHADOW_CASCADES, POINT_SHADOW_FACE_SIZE, POINT_SHADOW_NUM_FACES, PointLightShadowData,
PointLightShadowSlot, PointShadowUniforms, ShadowCullUniforms, ShadowDrawIndexedIndirect,
ShadowOccluder, ShadowSlotCache, ShadowUniforms, SkinnedShadowCullObject,
SkinnedShadowObjectData, SpotlightShadowSlot, scale_cascade_splits,
};
use crate::render::wgpu::passes::grow_storage_buffer;
pub(super) struct OccluderCacheEntry {
pub(super) center: nalgebra_glm::Vec3,
pub(super) radius: f32,
pub(super) hash: u64,
}
fn sphere_intersects_cube(
center: nalgebra_glm::Vec3,
radius: f32,
cube_center: nalgebra_glm::Vec3,
half_extent: f32,
) -> bool {
let dx = ((center.x - cube_center.x).abs() - half_extent).max(0.0);
let dy = ((center.y - cube_center.y).abs() - half_extent).max(0.0);
let dz = ((center.z - cube_center.z).abs() - half_extent).max(0.0);
dx * dx + dy * dy + dz * dz <= radius * radius
}
fn hash_occluder(mesh_geo_id: u32, model: &[[f32; 4]; 4]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
mesh_geo_id.hash(&mut hasher);
bytemuck::bytes_of(model).hash(&mut hasher);
hasher.finish()
}
fn hash_view_key(bytes: &[u8]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
}
fn cascade_side_planes(view_projection: &[[f32; 4]; 4]) -> [[f32; 4]; 6] {
let row = |index: usize| {
[
view_projection[0][index],
view_projection[1][index],
view_projection[2][index],
view_projection[3][index],
]
};
let row_x = row(0);
let row_y = row(1);
let row_w = row(3);
let combine = |sign: f32, axis: [f32; 4]| {
let plane = [
row_w[0] + sign * axis[0],
row_w[1] + sign * axis[1],
row_w[2] + sign * axis[2],
row_w[3] + sign * axis[3],
];
let length = (plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2])
.sqrt()
.max(1e-8);
[
plane[0] / length,
plane[1] / length,
plane[2] / length,
plane[3] / length,
]
};
let disabled = [0.0, 0.0, 0.0, 1.0e9];
[
combine(1.0, row_x),
combine(-1.0, row_x),
combine(1.0, row_y),
combine(-1.0, row_y),
disabled,
disabled,
]
}
const Y_FLIP_MATRIX: nalgebra_glm::Mat4 = nalgebra_glm::Mat4::new(
1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
);
const CUBEMAP_FACE_DIRECTIONS: [(nalgebra_glm::Vec3, nalgebra_glm::Vec3); POINT_SHADOW_NUM_FACES] = [
(
nalgebra_glm::Vec3::new(1.0, 0.0, 0.0),
nalgebra_glm::Vec3::new(0.0, -1.0, 0.0),
),
(
nalgebra_glm::Vec3::new(-1.0, 0.0, 0.0),
nalgebra_glm::Vec3::new(0.0, -1.0, 0.0),
),
(
nalgebra_glm::Vec3::new(0.0, 1.0, 0.0),
nalgebra_glm::Vec3::new(0.0, 0.0, 1.0),
),
(
nalgebra_glm::Vec3::new(0.0, -1.0, 0.0),
nalgebra_glm::Vec3::new(0.0, 0.0, -1.0),
),
(
nalgebra_glm::Vec3::new(0.0, 0.0, 1.0),
nalgebra_glm::Vec3::new(0.0, -1.0, 0.0),
),
(
nalgebra_glm::Vec3::new(0.0, 0.0, -1.0),
nalgebra_glm::Vec3::new(0.0, -1.0, 0.0),
),
];
impl PassNode<crate::ecs::world::World> for ShadowDepthPass {
fn name(&self) -> &str {
"shadow_depth_pass"
}
fn reads(&self) -> Vec<&str> {
vec![]
}
fn writes(&self) -> Vec<&str> {
vec!["shadow_depth", "spotlight_shadow_atlas"]
}
fn reads_writes(&self) -> Vec<&str> {
vec![]
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
world: &crate::ecs::world::World,
) {
self.sync_meshes(device, queue, &world.resources.assets.mesh_cache);
let directional_light = world
.core
.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
.find_map(|entity| {
if let (Some(light), Some(transform)) = (
world.core.get_light(entity),
world.core.get_global_transform(entity),
) {
if matches!(
light.light_type,
crate::ecs::light::components::LightType::Directional
) && light.cast_shadows
{
Some((light.clone(), *transform))
} else {
None
}
} else {
None
}
});
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
};
let cascade_split_distances = scale_cascade_splits(camera_near);
for cascade_index in 0..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 view_projection = calculate_cascade_view_projection(
&frustum_corners,
&light_direction,
cascade_resolution,
cascade_far,
);
self.cascade_data.view_projections[cascade_index] = view_projection.into();
}
self.cascade_data.split_distances = cascade_split_distances;
} 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, &cascade_far) in
super::types::CASCADE_SPLIT_DISTANCES.iter().enumerate()
{
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,
);
self.cascade_data.view_projections[cascade_index] =
(light_projection * light_view).into();
}
}
let first_cascade_vp: nalgebra_glm::Mat4 = self.cascade_data.view_projections[0].into();
let uniforms = ShadowUniforms {
light_view_projection: first_cascade_vp.into(),
_padding: [[0.0; 4]; 4],
_padding2: [[0.0; 4]; 4],
_padding3: [[0.0; 4]; 4],
_padding4: [0.0; 4],
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
for cascade_index in 0..NUM_SHADOW_CASCADES {
let cascade_vp: nalgebra_glm::Mat4 =
self.cascade_data.view_projections[cascade_index].into();
let cascade_uniforms = ShadowUniforms {
light_view_projection: cascade_vp.into(),
_padding: [[0.0; 4]; 4],
_padding2: [[0.0; 4]; 4],
_padding3: [[0.0; 4]; 4],
_padding4: [0.0; 4],
};
queue.write_buffer(
&self.cascade_uniform_buffers[cascade_index],
0,
bytemuck::cast_slice(&[cascade_uniforms]),
);
}
}
let visibility_signature = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for entity in &self.all_caster_entities {
world
.core
.get_visibility(*entity)
.is_none_or(|visibility| visibility.visible)
.hash(&mut hasher);
}
hasher.finish()
};
let needs_occluder_rebuild = !self.occluders_initialized
|| self.shadow_scene_dirty
|| visibility_signature != self.last_visible_signature;
self.shadow_scene_dirty = false;
if needs_occluder_rebuild {
self.shadow_caster_entities.clear();
self.all_caster_entities.clear();
self.occluders.clear();
self.occluder_transforms.clear();
for entity in world.core.query_entities(
crate::ecs::world::RENDER_MESH
| crate::ecs::world::GLOBAL_TRANSFORM
| crate::ecs::world::CASTS_SHADOW,
) {
let render_layer = world
.core
.get_render_layer(entity)
.map(|layer| layer.0)
.unwrap_or(crate::ecs::primitives::RenderLayer::WORLD);
if render_layer == crate::ecs::primitives::RenderLayer::OVERLAY {
continue;
}
let (Some(render_mesh), Some(transform)) = (
world.core.get_render_mesh(entity),
world.core.get_global_transform(entity),
) else {
continue;
};
let Some(&mesh_geo_id) = self.culling.name_to_geo_id.get(&render_mesh.name) else {
continue;
};
self.all_caster_entities.push(entity);
if world
.core
.get_visibility(entity)
.is_some_and(|visibility| !visibility.visible)
{
continue;
}
let model_matrix: [[f32; 4]; 4] = transform.0.into();
let transform_index = self.occluder_transforms.len() as u32;
self.occluder_transforms.push(model_matrix);
self.occluders.push(ShadowOccluder {
transform_index,
mesh_geo_id,
batch_id: 0,
_pad: 0,
});
self.shadow_caster_entities.push(entity);
}
for entity in world.core.query_entities(
crate::ecs::world::INSTANCED_MESH
| crate::ecs::world::GLOBAL_TRANSFORM
| crate::ecs::world::CASTS_SHADOW,
) {
let render_layer = world
.core
.get_render_layer(entity)
.map(|layer| layer.0)
.unwrap_or(crate::ecs::primitives::RenderLayer::WORLD);
if render_layer == crate::ecs::primitives::RenderLayer::OVERLAY {
continue;
}
let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) else {
continue;
};
let Some(&mesh_geo_id) = self.culling.name_to_geo_id.get(&instanced_mesh.mesh_name)
else {
continue;
};
self.all_caster_entities.push(entity);
if world
.core
.get_visibility(entity)
.is_some_and(|visibility| !visibility.visible)
{
continue;
}
for matrix in instanced_mesh.cached_model_matrices() {
let transform_index = self.occluder_transforms.len() as u32;
self.occluder_transforms.push(matrix.model);
self.occluders.push(ShadowOccluder {
transform_index,
mesh_geo_id,
batch_id: 0,
_pad: 0,
});
self.shadow_caster_entities.push(entity);
}
}
if !self.occluder_transforms.is_empty() {
if grow_storage_buffer::<[[f32; 4]; 4]>(
device,
&mut self.transform_buffer,
&mut self.transform_buffer_size,
self.occluder_transforms.len(),
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
"Shadow Transform Buffer (Resized)",
) {
self.transform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shadow Transform Bind Group"),
layout: &self.transform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: self.transform_buffer.as_entire_binding(),
}],
});
}
queue.write_buffer(
&self.transform_buffer,
0,
bytemuck::cast_slice(&self.occluder_transforms),
);
}
let mut signature_hasher = std::collections::hash_map::DefaultHasher::new();
{
use std::hash::{Hash, Hasher};
for entity in &self.all_caster_entities {
world
.core
.get_visibility(*entity)
.is_none_or(|visibility| visibility.visible)
.hash(&mut signature_hasher);
}
self.last_visible_signature = signature_hasher.finish();
}
self.occluders_initialized = true;
}
self.sync_skinned_meshes(device, queue, &world.resources.assets.mesh_cache);
self.skinned_shadow_caster_entities.clear();
let mut skinned_objects = Vec::new();
let mut skinned_bounds: Vec<[f32; 4]> = Vec::new();
let skinning_data = crate::ecs::skin::systems::collect_skinning_data(world);
let mut buffers_resized = false;
let max_buffer_size = device.limits().max_buffer_size as usize;
let matrix_size = std::mem::size_of::<[[f32; 4]; 4]>();
if !skinning_data.bone_transforms.is_empty() {
let required_size = skinning_data.bone_transforms.len();
if required_size > self.bone_transforms_buffer_size {
let new_size = (required_size as f32 * 2.0).ceil() as usize;
let buffer_bytes = matrix_size * new_size;
if buffer_bytes > max_buffer_size {
tracing::error!(
"Shadow bone transforms buffer would exceed GPU limit: {} > {} bytes",
buffer_bytes,
max_buffer_size
);
} else {
self.bone_transforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Bone Transforms Buffer (Resized)"),
size: buffer_bytes as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.bone_transforms_buffer_size = new_size;
buffers_resized = true;
}
}
if skinning_data.bone_transforms.len() <= self.bone_transforms_buffer_size {
queue.write_buffer(
&self.bone_transforms_buffer,
0,
bytemuck::cast_slice(&skinning_data.bone_transforms),
);
}
}
if !skinning_data.inverse_bind_matrices.is_empty() {
let required_size = skinning_data.inverse_bind_matrices.len();
if required_size > self.inverse_bind_matrices_buffer_size {
let new_size = (required_size as f32 * 2.0).ceil() as usize;
let buffer_bytes = matrix_size * new_size;
if buffer_bytes > max_buffer_size {
tracing::error!(
"Shadow inverse bind matrices buffer would exceed GPU limit: {} > {} bytes",
buffer_bytes,
max_buffer_size
);
} else {
self.inverse_bind_matrices_buffer =
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Inverse Bind Matrices Buffer (Resized)"),
size: buffer_bytes as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.inverse_bind_matrices_buffer_size = new_size;
buffers_resized = true;
}
}
if skinning_data.inverse_bind_matrices.len() <= self.inverse_bind_matrices_buffer_size {
queue.write_buffer(
&self.inverse_bind_matrices_buffer,
0,
bytemuck::cast_slice(&skinning_data.inverse_bind_matrices),
);
}
}
if !skinning_data.skin_data.is_empty() {
let required_size = skinning_data.skin_data.len();
if required_size > self.skin_data_buffer_size {
let new_size = (required_size as f32 * 2.0).ceil() as usize;
let buffer_bytes = std::mem::size_of::<GpuSkinData>() * new_size;
if buffer_bytes > max_buffer_size {
tracing::error!(
"Shadow skin data buffer would exceed GPU limit: {} > {} bytes",
buffer_bytes,
max_buffer_size
);
} else {
self.skin_data_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Skin Data Buffer (Resized)"),
size: buffer_bytes as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.skin_data_buffer_size = new_size;
buffers_resized = true;
}
}
if skinning_data.skin_data.len() <= self.skin_data_buffer_size {
queue.write_buffer(
&self.skin_data_buffer,
0,
bytemuck::cast_slice(&skinning_data.skin_data),
);
}
}
let required_joints = skinning_data.total_joints as usize;
if required_joints > self.joint_matrices_buffer_size {
let new_size = (required_joints as f32 * 2.0).ceil() as usize;
let buffer_bytes = matrix_size * new_size;
if buffer_bytes > max_buffer_size {
tracing::error!(
"Shadow joint matrices buffer would exceed GPU limit: {} > {} bytes",
buffer_bytes,
max_buffer_size
);
} else {
self.joint_matrices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Joint Matrices Buffer (Resized)"),
size: buffer_bytes as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.joint_matrices_buffer_size = new_size;
buffers_resized = true;
self.rebuild_skinned_bind_group(device);
}
}
if buffers_resized {
self.rebuild_skinning_compute_bind_group(device);
}
self.total_joints_to_dispatch = skinning_data.total_joints;
for entity in world.core.query_entities(
crate::ecs::world::SKIN
| crate::ecs::world::RENDER_MESH
| crate::ecs::world::GLOBAL_TRANSFORM
| crate::ecs::world::CASTS_SHADOW,
) {
let render_layer = world
.core
.get_render_layer(entity)
.map(|layer| layer.0)
.unwrap_or(crate::ecs::primitives::RenderLayer::WORLD);
if render_layer == crate::ecs::primitives::RenderLayer::OVERLAY {
continue;
}
if let Some(visibility) = world.core.get_visibility(entity)
&& !visibility.visible
{
continue;
}
if let Some(render_mesh) = world.core.get_render_mesh(entity)
&& let Some(&(vertex_offset, _vertex_count, index_offset, index_count)) =
self.skinned_meshes.get(&render_mesh.name)
{
let skin_index = skinning_data
.entity_skin_indices
.get(&entity)
.copied()
.unwrap_or(0);
let joint_offset = skinning_data.get_joint_offset(skin_index);
skinned_objects.push(SkinnedShadowObjectData {
transform_index: 0,
mesh_id: (index_offset << 16) | index_count,
material_id: vertex_offset,
joint_offset,
});
let margin = world
.core
.get_bounding_volume(entity)
.map(|bv| bv.sphere_radius)
.unwrap_or(2.0);
let bound = skinning_data
.skin_data
.get(skin_index as usize)
.map(|skin| {
crate::ecs::skin::systems::skinned_world_bounds(
&skinning_data.bone_transforms,
skin.base_bone_index as usize,
skin.joint_count as usize,
margin,
)
})
.unwrap_or([0.0, 0.0, 0.0, 1.0e9]);
skinned_bounds.push(bound);
self.skinned_shadow_caster_entities.push(entity);
}
}
let shadows_enabled = world
.resources
.renderer_state
.active_view
.shadow_depth_enabled;
let was_enabled = self.was_enabled_last_frame;
if needs_occluder_rebuild {
let occluder_transforms = &self.occluder_transforms;
let mesh_bounds = &self.culling.mesh_bounds;
let cache: Vec<OccluderCacheEntry> = self
.occluders
.iter()
.map(|occluder| {
let model = occluder_transforms[occluder.transform_index as usize];
let model_matrix: nalgebra_glm::Mat4 = model.into();
let bounds = mesh_bounds[occluder.mesh_geo_id as usize];
let local_center = nalgebra_glm::vec4(
bounds.center[0],
bounds.center[1],
bounds.center[2],
1.0,
);
let world_center = model_matrix * local_center;
let scale_x = (model[0][0] * model[0][0]
+ model[0][1] * model[0][1]
+ model[0][2] * model[0][2])
.sqrt();
let scale_y = (model[1][0] * model[1][0]
+ model[1][1] * model[1][1]
+ model[1][2] * model[1][2])
.sqrt();
let scale_z = (model[2][0] * model[2][0]
+ model[2][1] * model[2][1]
+ model[2][2] * model[2][2])
.sqrt();
let max_scale = scale_x.max(scale_y).max(scale_z);
OccluderCacheEntry {
center: nalgebra_glm::vec3(world_center.x, world_center.y, world_center.z),
radius: bounds.radius * max_scale,
hash: hash_occluder(occluder.mesh_geo_id, &model),
}
})
.collect();
self.occluder_cache = cache;
}
self.spotlight_shadow_slots.clear();
self.spotlight_shadow_data.clear();
let camera_position = world
.resources
.active_camera
.and_then(|cam| world.core.get_global_transform(cam))
.map(|t| nalgebra_glm::vec3(t.0[(0, 3)], t.0[(1, 3)], t.0[(2, 3)]))
.unwrap_or_else(|| nalgebra_glm::vec3(0.0, 0.0, 0.0));
let camera_frustum = if world.resources.window.camera_tile_rects.is_empty() {
crate::ecs::camera::queries::query_active_camera_matrices(world).map(|matrices| {
let view_projection: [[f32; 4]; 4] = (matrices.projection * matrices.view).into();
full_frustum_planes(&view_projection)
})
} else {
None
};
let assignment = &world.resources.shadow_atlas;
self.spotlight_atlas_slot_size = assignment.default_slot_size;
for slot in &assignment.slots {
let view_projection_array: [[f32; 4]; 4] = slot.view_projection.into();
let frustum = full_frustum_planes(&view_projection_array);
let mut content_hash = hash_view_key(bytemuck::bytes_of(&view_projection_array));
content_hash = content_hash.wrapping_add(hash_view_key(bytemuck::bytes_of(&[
slot.atlas_x_pixels,
slot.atlas_y_pixels,
slot.atlas_size_pixels,
])));
for entry in &self.occluder_cache {
if sphere_in_frustum(entry.center, entry.radius, &frustum) {
content_hash = content_hash.wrapping_add(entry.hash);
}
}
let mut skinned_dirty = false;
for bound in &skinned_bounds {
let center = nalgebra_glm::vec3(bound[0], bound[1], bound[2]);
if sphere_in_frustum(center, bound[3], &frustum) {
skinned_dirty = true;
break;
}
}
let cache = &mut self.spotlight_slot_cache[slot.slot_index as usize];
let clean = shadows_enabled
&& was_enabled
&& !skinned_dirty
&& cache.occupant == Some(slot.entity)
&& cache.hash == content_hash;
*cache = ShadowSlotCache {
occupant: Some(slot.entity),
hash: content_hash,
};
self.spotlight_shadow_slots.push(SpotlightShadowSlot {
entity: slot.entity,
view_projection: slot.view_projection,
slot_index: slot.slot_index,
bias: slot.bias,
atlas_x_pixels: slot.atlas_x_pixels,
atlas_y_pixels: slot.atlas_y_pixels,
atlas_size_pixels: slot.atlas_size_pixels,
clean,
});
}
self.spotlight_shadow_data = assignment.data.clone();
self.point_light_shadow_slots.clear();
self.point_light_shadow_data.clear();
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));
if light.shadow_distance > 0.0
&& distance_sq > light.shadow_distance * light.shadow_distance
{
continue;
}
if let Some(planes) = &camera_frustum
&& !sphere_in_frustum(light_pos, light.range.max(1.0), planes)
{
continue;
}
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));
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)],
);
let range = light.range.max(0.1);
let point_key = [light_position.x, light_position.y, light_position.z, range];
let mut content_hash = hash_view_key(bytemuck::bytes_of(&point_key));
for entry in &self.occluder_cache {
if sphere_intersects_cube(entry.center, entry.radius, light_position, range) {
content_hash = content_hash.wrapping_add(entry.hash);
}
}
let mut skinned_dirty = false;
for bound in &skinned_bounds {
let center = nalgebra_glm::vec3(bound[0], bound[1], bound[2]);
if sphere_intersects_cube(center, bound[3], light_position, range) {
skinned_dirty = true;
break;
}
}
let cache = &mut self.point_slot_cache[slot_index];
let clean = shadows_enabled
&& was_enabled
&& !skinned_dirty
&& cache.occupant == Some(*entity)
&& cache.hash == content_hash;
*cache = ShadowSlotCache {
occupant: Some(*entity),
hash: content_hash,
};
self.point_light_shadow_slots.push(PointLightShadowSlot {
entity: *entity,
position: light_position,
range: light.range.max(0.1),
slot_index: slot_index as u32,
bias: light.shadow_bias,
clean,
});
self.point_light_shadow_data.push(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],
});
}
for index in self.spotlight_shadow_slots.len()..MAX_SPOTLIGHT_SHADOWS {
self.spotlight_slot_cache[index] = ShadowSlotCache::default();
}
for index in self.point_light_shadow_slots.len()..MAX_POINT_LIGHT_SHADOWS {
self.point_slot_cache[index] = ShadowSlotCache::default();
}
self.was_enabled_last_frame = shadows_enabled;
if !self.point_light_shadow_slots.is_empty() {
let mut all_uniforms: Vec<PointShadowUniforms> = Vec::new();
for slot in &self.point_light_shadow_slots {
let light_pos = slot.position;
let light_range = slot.range;
let near = 0.1;
let base_projection =
reverse_z_perspective(std::f32::consts::FRAC_PI_2, 1.0, near, light_range);
let projection = Y_FLIP_MATRIX * base_projection;
for (direction, up) in &CUBEMAP_FACE_DIRECTIONS {
let target = light_pos + *direction;
let view = nalgebra_glm::look_at(&light_pos, &target, up);
let view_projection = projection * view;
let mut uniform: PointShadowUniforms = bytemuck::Zeroable::zeroed();
uniform.view_projection = view_projection.into();
uniform.light_position = [light_pos.x, light_pos.y, light_pos.z];
uniform.light_range = light_range;
all_uniforms.push(uniform);
}
}
queue.write_buffer(
&self.point_light_uniform_buffer,
0,
bytemuck::cast_slice(&all_uniforms),
);
}
let num_spotlight_views = self.spotlight_shadow_slots.len();
let num_point_lights = self.point_light_shadow_slots.len();
let view_count =
NUM_SHADOW_CASCADES + num_spotlight_views + num_point_lights * POINT_SHADOW_NUM_FACES;
self.shadow_view_dirty.clear();
for _ in 0..NUM_SHADOW_CASCADES {
self.shadow_view_dirty.push(true);
}
for slot in &self.spotlight_shadow_slots {
self.shadow_view_dirty.push(!slot.clean);
}
for slot in &self.point_light_shadow_slots {
for _ in 0..POINT_SHADOW_NUM_FACES {
self.shadow_view_dirty.push(!slot.clean);
}
}
self.culling.prepare_frame(
device,
queue,
&mut self.occluders,
&self.transform_buffer,
view_count,
needs_occluder_rebuild,
);
for cascade in 0..NUM_SHADOW_CASCADES {
let planes = cascade_side_planes(&self.cascade_data.view_projections[cascade]);
self.culling.set_view_frustum(queue, cascade, planes);
}
for (spotlight_index, slot) in self.spotlight_shadow_slots.iter().enumerate() {
let view_projection: [[f32; 4]; 4] = slot.view_projection.into();
let planes = full_frustum_planes(&view_projection);
self.culling
.set_view_frustum(queue, NUM_SHADOW_CASCADES + spotlight_index, planes);
}
for (light_index, slot) in self.point_light_shadow_slots.iter().enumerate() {
let base_projection =
reverse_z_perspective(std::f32::consts::FRAC_PI_2, 1.0, 0.1, slot.range);
let projection = Y_FLIP_MATRIX * base_projection;
for (face_index, (direction, up)) in CUBEMAP_FACE_DIRECTIONS.iter().enumerate() {
let target = slot.position + *direction;
let view = nalgebra_glm::look_at(&slot.position, &target, up);
let view_projection: [[f32; 4]; 4] = (projection * view).into();
let view_id = NUM_SHADOW_CASCADES
+ num_spotlight_views
+ light_index * POINT_SHADOW_NUM_FACES
+ face_index;
let planes = full_frustum_planes(&view_projection);
self.culling.set_view_frustum(queue, view_id, planes);
}
}
self.prepare_skinned_cull(
device,
queue,
skinned_objects,
skinned_bounds,
num_spotlight_views,
);
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, crate::ecs::world::World>,
) -> crate::render::wgpu::rendergraph::Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
> {
if !context
.configs
.resources
.renderer_state
.active_view
.shadow_depth_enabled
{
return Ok(context.into_sub_graph_commands());
}
let has_directional_shadow = context
.configs
.core
.query_entities(crate::ecs::world::LIGHT)
.any(|entity| {
if let Some(light) = context.configs.core.get_light(entity) {
matches!(
light.light_type,
crate::ecs::light::components::LightType::Directional
) && light.cast_shadows
} else {
false
}
});
let has_spotlight_shadows = !self.spotlight_shadow_slots.is_empty();
let has_point_light_shadows = !self.point_light_shadow_slots.is_empty();
if !has_directional_shadow && !has_spotlight_shadows && !has_point_light_shadows {
return Ok(vec![]);
}
if self.total_joints_to_dispatch > 0 && !self.skinned_shadow_caster_entities.is_empty() {
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Shadow Skinning Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.skinning_compute_pipeline);
compute_pass.set_bind_group(0, &self.skinning_compute_bind_group, &[]);
let num_workgroups = self.total_joints_to_dispatch.div_ceil(64);
compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
}
let (shadow_depth_view, _, _) = context.get_depth_attachment("shadow_depth")?;
let (shadow_atlas_width, _) = context.get_texture_size("shadow_depth")?;
let atlas_size = shadow_atlas_width as f32;
let cascade_slot_size = atlas_size / CASCADE_ATLAS_SLOTS_PER_ROW as f32;
let supports_multi_draw = context
.device
.features()
.contains(wgpu::Features::MULTI_DRAW_INDIRECT_COUNT);
self.culling
.dispatch_cull(context.encoder, &self.shadow_view_dirty);
if self.skinned_occluder_count > 0 && self.skinned_group_count > 0 {
let mut cull_pass = context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Shadow Skinned Cull Pass"),
timestamp_writes: None,
});
cull_pass.set_pipeline(&self.skinned_cull_pipeline);
let workgroups = (self.skinned_occluder_count as u32).div_ceil(64);
for view in 0..self.skinned_cull_view_count {
cull_pass.set_bind_group(0, &self.skinned_cull_bind_groups[view], &[]);
cull_pass.dispatch_workgroups(workgroups, 1, 1);
}
}
for cascade_index in 0..NUM_SHADOW_CASCADES {
let slot_x = (cascade_index as u32) % CASCADE_ATLAS_SLOTS_PER_ROW;
let slot_y = (cascade_index as u32) / CASCADE_ATLAS_SLOTS_PER_ROW;
let load_op = if cascade_index == 0 {
wgpu::LoadOp::Clear(0.0)
} else {
wgpu::LoadOp::Load
};
let mut shadow_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shadow Depth Pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: shadow_depth_view,
depth_ops: Some(wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
shadow_pass.set_viewport(
slot_x as f32 * cascade_slot_size,
slot_y as f32 * cascade_slot_size,
cascade_slot_size,
cascade_slot_size,
0.0,
1.0,
);
shadow_pass.set_scissor_rect(
(slot_x as f32 * cascade_slot_size) as u32,
(slot_y as f32 * cascade_slot_size) as u32,
cascade_slot_size as u32,
cascade_slot_size as u32,
);
shadow_pass.set_pipeline(&self.culling.indirect_pipeline);
shadow_pass.set_bind_group(0, &self.cascade_uniform_bind_groups[cascade_index], &[]);
shadow_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
shadow_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
self.culling
.draw_view(&mut shadow_pass, cascade_index, supports_multi_draw);
if self.skinned_group_count > 0 {
shadow_pass.set_pipeline(&self.skinned_pipeline);
shadow_pass.set_bind_group(
0,
&self.cascade_uniform_bind_groups[cascade_index],
&[],
);
shadow_pass.set_bind_group(1, &self.skinned_bind_group, &[]);
shadow_pass.set_vertex_buffer(0, self.skinned_vertex_buffer.slice(..));
shadow_pass.set_index_buffer(
self.skinned_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
let stride = std::mem::size_of::<ShadowDrawIndexedIndirect>() as u64;
let base = (cascade_index * self.skinned_group_count) as u64 * stride;
let count = self.skinned_group_count as u32;
if supports_multi_draw {
shadow_pass.multi_draw_indexed_indirect(
&self.skinned_indirect_buffer,
base,
count,
);
} else {
for command in 0..count as u64 {
shadow_pass.draw_indexed_indirect(
&self.skinned_indirect_buffer,
base + command * stride,
);
}
}
}
}
if has_spotlight_shadows {
let (spotlight_atlas_view, _, _) =
context.get_depth_attachment("spotlight_shadow_atlas")?;
let (spotlight_atlas_width, _) = context.get_texture_size("spotlight_shadow_atlas")?;
let _ = spotlight_atlas_width;
for slot in &self.spotlight_shadow_slots.clone() {
if slot.clean {
continue;
}
let uniforms = ShadowUniforms {
light_view_projection: slot.view_projection.into(),
_padding: [[0.0; 4]; 4],
_padding2: [[0.0; 4]; 4],
_padding3: [[0.0; 4]; 4],
_padding4: [0.0; 4],
};
let slot_uniform_buffer =
context
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Spotlight Shadow Uniform Buffer"),
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM,
});
let slot_uniform_bind_group =
context
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Spotlight Shadow Uniform Bind Group"),
layout: &self.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: slot_uniform_buffer.as_entire_binding(),
}],
});
let load_op = wgpu::LoadOp::Load;
let mut spotlight_pass =
context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Spotlight Shadow Pass"),
color_attachments: &[],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: spotlight_atlas_view,
depth_ops: Some(wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
spotlight_pass.set_viewport(
slot.atlas_x_pixels as f32,
slot.atlas_y_pixels as f32,
slot.atlas_size_pixels as f32,
slot.atlas_size_pixels as f32,
0.0,
1.0,
);
spotlight_pass.set_scissor_rect(
slot.atlas_x_pixels,
slot.atlas_y_pixels,
slot.atlas_size_pixels,
slot.atlas_size_pixels,
);
spotlight_pass.set_pipeline(&self.depth_clear_pipeline);
spotlight_pass.draw(0..3, 0..1);
spotlight_pass.set_pipeline(&self.culling.indirect_pipeline);
spotlight_pass.set_bind_group(0, &slot_uniform_bind_group, &[]);
spotlight_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
spotlight_pass
.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
self.culling.draw_view(
&mut spotlight_pass,
NUM_SHADOW_CASCADES + slot.slot_index as usize,
supports_multi_draw,
);
let skinned_view_id = NUM_SHADOW_CASCADES + slot.slot_index as usize;
if self.skinned_group_count > 0 && skinned_view_id < self.skinned_cull_view_count {
spotlight_pass.set_pipeline(&self.skinned_pipeline);
spotlight_pass.set_bind_group(0, &slot_uniform_bind_group, &[]);
spotlight_pass.set_bind_group(1, &self.skinned_bind_group, &[]);
spotlight_pass.set_vertex_buffer(0, self.skinned_vertex_buffer.slice(..));
spotlight_pass.set_index_buffer(
self.skinned_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
let stride = std::mem::size_of::<ShadowDrawIndexedIndirect>() as u64;
let base = (skinned_view_id * self.skinned_group_count) as u64 * stride;
let count = self.skinned_group_count as u32;
if supports_multi_draw {
spotlight_pass.multi_draw_indexed_indirect(
&self.skinned_indirect_buffer,
base,
count,
);
} else {
for command in 0..count as u64 {
spotlight_pass.draw_indexed_indirect(
&self.skinned_indirect_buffer,
base + command * stride,
);
}
}
}
}
}
if has_point_light_shadows {
let num_spotlight_views = self.spotlight_shadow_slots.len();
for slot in &self.point_light_shadow_slots {
if slot.clean {
continue;
}
for face_index in 0..POINT_SHADOW_NUM_FACES {
let uniform_index =
slot.slot_index as usize * POINT_SHADOW_NUM_FACES + face_index;
let dynamic_offset =
(uniform_index * std::mem::size_of::<PointShadowUniforms>()) as u32;
let face_view_index =
slot.slot_index as usize * POINT_SHADOW_NUM_FACES + face_index;
let face_view = &self.point_light_face_views[face_view_index];
let mut point_pass =
context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&format!(
"Point Light {} Face {} Shadow Pass",
slot.slot_index, face_index
)),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: face_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.point_light_depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0.0),
store: wgpu::StoreOp::Discard,
}),
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
point_pass.set_viewport(
0.0,
0.0,
POINT_SHADOW_FACE_SIZE as f32,
POINT_SHADOW_FACE_SIZE as f32,
0.0,
1.0,
);
point_pass.set_pipeline(&self.culling.point_indirect_pipeline);
point_pass.set_bind_group(
0,
&self.point_light_uniform_bind_group,
&[dynamic_offset],
);
point_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
point_pass
.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
let view_id = NUM_SHADOW_CASCADES
+ num_spotlight_views
+ slot.slot_index as usize * POINT_SHADOW_NUM_FACES
+ face_index;
self.culling
.draw_view(&mut point_pass, view_id, supports_multi_draw);
}
}
}
Ok(vec![])
}
}
impl ShadowDepthPass {
fn prepare_skinned_cull(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
skinned_objects: Vec<SkinnedShadowObjectData>,
skinned_bounds: Vec<[f32; 4]>,
num_spotlight_views: usize,
) {
self.skinned_group_count = 0;
self.skinned_occluder_count = 0;
self.skinned_cull_view_count = 0;
if skinned_objects.is_empty() {
return;
}
let mut order: Vec<usize> = (0..skinned_objects.len()).collect();
order.sort_by_key(|&index| {
(
skinned_objects[index].mesh_id,
skinned_objects[index].material_id,
)
});
let objects: Vec<SkinnedShadowObjectData> =
order.iter().map(|&index| skinned_objects[index]).collect();
let bounds: Vec<[f32; 4]> = order.iter().map(|&index| skinned_bounds[index]).collect();
let mut command_index_per_object = vec![0u32; objects.len()];
let mut group_meshes: Vec<(u32, u32, u32)> = Vec::new();
let mut index = 0;
while index < objects.len() {
let mesh_id = objects[index].mesh_id;
let material_id = objects[index].material_id;
let start = index as u32;
while index < objects.len()
&& objects[index].mesh_id == mesh_id
&& objects[index].material_id == material_id
{
command_index_per_object[index] = group_meshes.len() as u32;
index += 1;
}
group_meshes.push((mesh_id, material_id, start));
}
let group_count = group_meshes.len();
let occluder_count = objects.len();
let view_count = NUM_SHADOW_CASCADES + num_spotlight_views;
let cull_objects: Vec<SkinnedShadowCullObject> = (0..occluder_count)
.map(|object_index| SkinnedShadowCullObject {
bounds: bounds[object_index],
command_index: command_index_per_object[object_index],
_pad: [0; 3],
})
.collect();
let mut commands: Vec<ShadowDrawIndexedIndirect> =
Vec::with_capacity(view_count * group_count);
for view in 0..view_count {
for &(mesh_id, material_id, group_start) in &group_meshes {
commands.push(ShadowDrawIndexedIndirect {
index_count: mesh_id & 0xFFFF,
instance_count: 0,
first_index: mesh_id >> 16,
base_vertex: material_id as i32,
first_instance: (view * occluder_count) as u32 + group_start,
});
}
}
let mut bind_groups_dirty = false;
if grow_storage_buffer::<SkinnedShadowObjectData>(
device,
&mut self.skinned_object_buffer,
&mut self.skinned_object_buffer_size,
occluder_count,
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
"Shadow Skinned Object Buffer (Resized)",
) {
bind_groups_dirty = true;
}
if grow_storage_buffer::<SkinnedShadowCullObject>(
device,
&mut self.skinned_cull_objects_buffer,
&mut self.skinned_cull_objects_buffer_size,
cull_objects.len(),
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
"Shadow Skinned Cull Objects Buffer (Resized)",
) {
bind_groups_dirty = true;
}
if occluder_count * view_count > self.skinned_visible_indices_buffer_size {
let new_size = (occluder_count * view_count * 2).max(64);
self.skinned_visible_indices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Skinned Visible Indices Buffer (Resized)"),
size: (std::mem::size_of::<u32>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.skinned_visible_indices_buffer_size = new_size;
bind_groups_dirty = true;
}
if commands.len() > self.skinned_indirect_buffer_size {
let new_size = (commands.len() * 2).max(32);
self.skinned_indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shadow Skinned Indirect Buffer (Resized)"),
size: (std::mem::size_of::<ShadowDrawIndexedIndirect>() * new_size) as u64,
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.skinned_indirect_buffer_size = new_size;
bind_groups_dirty = true;
}
if bind_groups_dirty {
self.rebuild_skinned_bind_group(device);
self.rebuild_skinned_cull_bind_groups(device);
}
queue.write_buffer(
&self.skinned_object_buffer,
0,
bytemuck::cast_slice(&objects),
);
queue.write_buffer(
&self.skinned_cull_objects_buffer,
0,
bytemuck::cast_slice(&cull_objects),
);
queue.write_buffer(
&self.skinned_indirect_buffer,
0,
bytemuck::cast_slice(&commands),
);
for view in 0..view_count {
let planes = if view < NUM_SHADOW_CASCADES {
cascade_side_planes(&self.cascade_data.view_projections[view])
} else {
let spotlight_index = view - NUM_SHADOW_CASCADES;
let view_projection: [[f32; 4]; 4] = self.spotlight_shadow_slots[spotlight_index]
.view_projection
.into();
full_frustum_planes(&view_projection)
};
let uniforms = ShadowCullUniforms {
frustum_planes: planes,
occluder_count: occluder_count as u32,
indirect_offset: (view * group_count) as u32,
_pad0: 0,
_pad1: 0,
};
queue.write_buffer(
&self.skinned_cull_uniform_buffers[view],
0,
bytemuck::cast_slice(&[uniforms]),
);
}
self.skinned_group_count = group_count;
self.skinned_occluder_count = occluder_count;
self.skinned_cull_view_count = view_count;
}
}