From 8c1738334c2ee46317fa279ea9dd8acb61a8c366 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 24 Jul 2024 20:10:32 -0400 Subject: [PATCH] render: shadow maps and PCF for spot lights --- examples/shadows/src/main.rs | 42 ++++-- lyra-game/src/render/graph/passes/shadows.rs | 137 +++++++++++++------ lyra-game/src/render/light/spotlight.rs | 2 + lyra-game/src/render/shaders/base.wgsl | 95 +++++++++---- lyra-game/src/render/shaders/shadows.wgsl | 5 - lyra-math/src/angle.rs | 16 ++- 6 files changed, 218 insertions(+), 79 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ba929aa..30ad34a 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -5,10 +5,10 @@ use lyra_engine::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, - math::{self, Quat, Transform, Vec3}, + math::{self, Angle, Quat, Transform, Vec3}, render::{ graph::{ShadowCasterSettings, ShadowFilteringMode}, - light::{directional::DirectionalLight, PointLight}, + light::{directional::DirectionalLight, PointLight, SpotLight}, }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, @@ -189,7 +189,7 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - world.spawn(( + /* world.spawn(( cube_mesh.clone(), PointLight { enabled: true, @@ -207,6 +207,33 @@ fn setup_scene_plugin(game: &mut Game) { Quat::IDENTITY, Vec3::new(0.5, 0.5, 0.5), ), + )); */ + + let t = Transform::new( + Vec3::new(4.0 - 1.43, -13.0, 0.0), + //Vec3::new(-5.0, 1.0, -0.28), + //Vec3::new(-10.0, 0.94, -0.28), + + Quat::from_euler(math::EulerRot::XYZ, 0.0, math::Angle::Degrees(-45.0).to_radians(), 0.0), + Vec3::new(0.15, 0.15, 0.15), + ); + + world.spawn(( + SpotLight { + enabled: true, + color: Vec3::new(1.0, 0.0, 0.0), + intensity: 3.0, + range: 4.5, + //cutoff: math::Angle::Degrees(45.0), + ..Default::default() + }, + /* ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcf, + ..Default::default() + }, */ + WorldTransform::from(t), + t, + //cube_mesh.clone(), )); /* world.spawn(( @@ -224,14 +251,13 @@ fn setup_scene_plugin(game: &mut Game) { let mut camera = CameraComponent::new_3d(); //camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); - /* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); + camera.transform.translation = math::Vec3::new(-1.0, -10.0, -1.5); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - camera.transform.rotate_y(math::Angle::Degrees(-55.0)); */ + camera.transform.rotate_y(math::Angle::Degrees(-90.0)); - camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); + /* camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - //camera.transform.rotate_y(math::Angle::Degrees(-90.0)); - camera.transform.rotate_y(math::Angle::Degrees(90.0)); + camera.transform.rotate_y(math::Angle::Degrees(90.0)); */ world.spawn((camera, FreeFlyCamera::default())); } diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 3e207a9..5e59f59 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -20,7 +20,7 @@ use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::{directional::DirectionalLight, LightType, PointLight}, + light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, @@ -186,6 +186,7 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, + light_half_outer_angle: Option, are_settings_custom: bool, shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { @@ -200,17 +201,6 @@ impl ShadowMapsPass { let has_shadow_settings = if are_settings_custom { 1 } else { 0 }; - /* let (has_shadow_settings, pcf_samples_num, pcss_samples_num) = if are_settings_custom { - - (1, u.pcf_samples_num, u.pcss_blocker_search_samples) - } else { - (0, , 0) - }; */ - - /* shadow_settings.map(|ss| { - let u = ShadowSettingsUniform::new(ss.filtering_mode, ss.pcf_samples_num, ss.pcss_blocker_search_samples); - (1, u.pcf_samples_num, u.pcss_blocker_search_samples) - }).unwrap_or((0, 0, 0)); */ let (start_atlas_idx, uniform_indices) = match light_type { LightType::Directional => { @@ -265,7 +255,49 @@ impl ShadowMapsPass { indices[0] = uniform_index; (atlas_index, indices) } - LightType::Spotlight => todo!(), + LightType::Spotlight => { + // allocate a single frame in the shadow map atlas + let atlas_index = atlas + .pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) + .expect("failed to pack new shadow map into texture atlas"); + let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); + + let aspect = SHADOW_SIZE.x as f32 / SHADOW_SIZE.y as f32; + let projection = glam::Mat4::perspective_rh( + //Angle::Degrees(90.0).to_radians(), + (light_half_outer_angle.unwrap() * 2.0).to_radians(), + aspect, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + let light_trans = light_pos.translation; + let forward = light_pos.forward(); + let up = light_pos.up(); + let view = glam::Mat4::look_to_rh(light_trans, forward, up); + + let light_proj = projection * view; + + let u = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, + light_size_uv: 0.0, + _padding1: 0, + light_pos: light_pos.translation, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + constant_depth_bias: DEFAULT_CONSTANT_DEPTH_BIAS * shadow_settings.constant_depth_bias_scale, + _padding2: 0, + }; + + let uniform_index = self.light_uniforms_buffer.insert(queue, &u); + let mut indices = [0; 6]; + indices[0] = uniform_index; + (atlas_index, indices) + }, LightType::Point => { let aspect = SHADOW_SIZE.x as f32 / SHADOW_SIZE.y as f32; let projection = glam::Mat4::perspective_rh( @@ -626,6 +658,31 @@ impl Node for ShadowMapsPass { LightType::Directional, entity, *pos, + None, + custom_settings, + shadow_settings, + ); + index_components_queue.push_back((entity, atlas_index)); + } + } + + for (entity, pos, shadow_settings, spot) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + &SpotLight, + )>() { + if !self.depth_maps.contains_key(&entity) { + let (custom_settings, shadow_settings) = shadow_settings + .map(|ss| (true, ss.clone())) + .unwrap_or((false, settings)); + + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Spotlight, + entity, + *pos, + Some(spot.outer_cutoff), custom_settings, shadow_settings, ); @@ -649,6 +706,7 @@ impl Node for ShadowMapsPass { LightType::Point, entity, *pos, + None, custom_settings, shadow_settings, ); @@ -786,7 +844,7 @@ impl Node for ShadowMapsPass { &frame, light_depth_map.uniform_index[0] as _, ); - } + }, LightType::Point => { pass.set_pipeline(&point_light_pipeline); @@ -806,8 +864,25 @@ impl Node for ShadowMapsPass { ui as _, ); } + }, + LightType::Spotlight => { + pass.set_pipeline(&pipeline); + //pass.set_pipeline(&point_light_pipeline); + + let frame = atlas + .texture_frame(light_depth_map.atlas_index) + .expect("missing atlas frame for light"); + + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + light_depth_map.uniform_index[0] as _, + ); } - LightType::Spotlight => todo!(), } } } @@ -976,35 +1051,17 @@ pub enum ShadowFilteringMode { None, /// Uses hardware features for 2x2 PCF. Pcf2x2, - Pcf, #[default] + Pcf, + /// Percentage-Closer Soft Shadows + /// https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf + /// + /// PCSS is only implemented for directional lights. Use PCF for point and spot lights instead. + /// PCSS is expensive per-frame, so it has not been implemented for them. If you use this for + /// point and/or spot lights, the renderer will fall back to PCF. Pcss, } -/* #[derive(Debug, Copy, Clone)] -pub struct ShadowSettings { - pub filtering_mode: ShadowFilteringMode, - /// How many PCF filtering samples are used per dimension. - /// - /// A value of 25 is common, this is maxed to 128. - pub pcf_samples_num: u32, - /// How many samples are used for the PCSS blocker search step. - /// - /// Multiple samples are required to avoid holes int he penumbra due to missing blockers. - /// A value of 25 is common, this is maxed to 128. - pub pcss_blocker_search_samples: u32, -} - -impl Default for ShadowSettings { - fn default() -> Self { - Self { - filtering_mode: ShadowFilteringMode::default(), - pcf_samples_num: 25, - pcss_blocker_search_samples: 25, - } - } -} */ - const PCF_SAMPLES_NUM_MAX: u32 = 128; const PCSS_SAMPLES_NUM_MAX: u32 = 128; diff --git a/lyra-game/src/render/light/spotlight.rs b/lyra-game/src/render/light/spotlight.rs index fa89c5a..4d5d8bf 100644 --- a/lyra-game/src/render/light/spotlight.rs +++ b/lyra-game/src/render/light/spotlight.rs @@ -9,6 +9,8 @@ pub struct SpotLight { pub range: f32, pub intensity: f32, pub smoothness: f32, + /// Cutoff angle that specifies the light radius. + /// This is half of the light's FOV. pub cutoff: math::Angle, pub outer_cutoff: math::Angle, } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 2bab59e..3d6ecc0 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -182,7 +182,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (light.light_ty == LIGHT_TY_DIRECTIONAL) { let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]]; - let frag_pos_light_space = shadow_u.light_space_matrix * vec4(in.world_position, 1.0); let shadow = calc_shadow_dir_light(in.world_position, in.world_normal, light_dir, light); light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); @@ -190,7 +189,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let shadow = calc_shadow_point_light(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } else if (light.light_ty == LIGHT_TY_SPOT) { - light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color); + let shadow = calc_shadow_spot_light(in.world_position, in.world_normal, light_dir, light); + light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } } @@ -293,12 +293,12 @@ fn calc_shadow_dir_light(world_pos: vec3, world_normal: vec3, light_di } // PCSS else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u { - shadow = pcss_dir_light(xy_remapped, current_depth, map_data); + shadow = pcss_dir_light(xy_remapped, current_depth, i32(pcss_blocker_search_samples), i32(pcf_samples_num), map_data); } // only PCF else if pcf_samples_num > 0u { let texel_size = 1.0 / f32(map_data.atlas_frame.width); - shadow = pcf_dir_light(xy_remapped, current_depth, map_data, texel_size); + shadow = pcf_dir_light(xy_remapped, current_depth, map_data, i32(pcf_samples_num), texel_size); } // no filtering else { @@ -352,13 +352,13 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2, saf } /// Find the average blocker distance for a directiona llight -fn find_blocker_distance_dir_light(tex_coords: vec2, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2 { +fn find_blocker_distance_dir_light(tex_coords: vec2, search_samples: i32, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2 { let search_width = search_width(shadow_u.near_plane, shadow_u.light_size_uv, receiver_depth); var blockers = 0; var avg_dist = 0.0; - let samples = i32(u_shadow_settings.pcss_blocker_search_samples); - for (var i = 0; i < samples; i++) { + //let samples = i32(u_shadow_settings.pcss_blocker_search_samples); + for (var i = 0; i < search_samples; i++) { let offset_coords = tex_coords + u_pcss_poisson_disc[i] * search_width; let new_coords = to_atlas_frame_coords(shadow_u, offset_coords, false); let z = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, new_coords, 0.0); @@ -373,8 +373,8 @@ fn find_blocker_distance_dir_light(tex_coords: vec2, receiver_depth: f32, b return vec2(avg_dist / b, b); } -fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { - let blocker_search = find_blocker_distance_dir_light(tex_coords, receiver_depth, 0.0, shadow_u); +fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, pcss_blocker_samples: i32, pcf_samples_num: i32, shadow_u: LightShadowMapUniform) -> f32 { + let blocker_search = find_blocker_distance_dir_light(tex_coords, pcss_blocker_samples, receiver_depth, 0.0, shadow_u); // If no blockers were found, exit now to save in filtering if blocker_search.y == 0.0 { @@ -387,13 +387,12 @@ fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, shadow_u: LightSha // PCF let uv_radius = penumbra_width * shadow_u.light_size_uv * shadow_u.near_plane / receiver_depth; - return pcf_dir_light(tex_coords, receiver_depth, shadow_u, uv_radius); + return pcf_dir_light(tex_coords, receiver_depth, shadow_u, pcf_samples_num, uv_radius); } /// Calculate the shadow coefficient using PCF of a directional light -fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, uv_radius: f32) -> f32 { +fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: i32, uv_radius: f32) -> f32 { var shadow = 0.0; - let samples_num = i32(u_shadow_settings.pcf_samples_num); for (var i = 0; i < samples_num; i++) { let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius; let new_coords = to_atlas_frame_coords(shadow_u, offset, false); @@ -440,15 +439,10 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ let region_coords = to_atlas_frame_coords(u, coords_2d, true); shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth); } - // PCSS - else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u { - shadow = pcss_dir_light(coords_2d, current_depth, u); - } - // only PCF + // only PCF, PCSS is not supported so no need to check for it else if pcf_samples_num > 0u { let texel_size = 1.0 / f32(u.atlas_frame.width); - shadow = pcf_point_light(frag_to_light, current_depth, uniforms, pcf_samples_num, 0.007); - //shadow = pcf_point_light(coords_2d, current_depth, u, pcf_samples_num, texel_size); + shadow = pcf_point_light(frag_to_light, current_depth, uniforms, pcf_samples_num, texel_size); } // no filtering else { @@ -482,11 +476,11 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { +fn pcf_spot_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: i32, uv_radius: f32) -> f32 { var shadow = 0.0; - for (var i = 0; i < i32(samples_num); i++) { + for (var i = 0; i < samples_num; i++) { let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius; - let new_coords = to_atlas_frame_coords(shadow_u, offset); + let new_coords = to_atlas_frame_coords(shadow_u, offset, false); shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth); } @@ -494,7 +488,57 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, world_normal: vec3, light_dir: vec3, light: Light) -> f32 { + let map_data: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]]; + let frag_pos_light_space = map_data.light_space_matrix * vec4(world_pos, 1.0); + + var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; + // for some reason the y component is flipped after transforming + proj_coords.y = -proj_coords.y; + + // Remap xy to [0.0, 1.0] + let xy_remapped = proj_coords.xy * 0.5 + 0.5; + + // use a bias to avoid shadow acne + let current_depth = proj_coords.z - map_data.constant_depth_bias; + + // get settings + let settings = get_shadow_settings(map_data); + let pcf_samples_num = settings.x; + let pcss_blocker_search_samples = settings.y; + + var shadow = 0.0; + // hardware 2x2 PCF via camparison sampler + if pcf_samples_num == 2u { + let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false); + shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth); + } + // only PCF is supported for spot lights + else if pcf_samples_num > 0u { + let texel_size = 1.0 / f32(map_data.atlas_frame.width); + shadow = pcf_spot_light(xy_remapped, current_depth, map_data, i32(pcf_samples_num), texel_size); + } + // no filtering + else { + let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false); + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0); + shadow = select(1.0, 0.0, current_depth > closest_depth); + } + + // dont cast shadows outside the light's far plane + if (proj_coords.z > 1.0) { + shadow = 1.0; + } + + // dont cast shadows if the texture coords would go past the shadow maps + if (xy_remapped.x > 1.0 || xy_remapped.x < 0.0 || xy_remapped.y > 1.0 || xy_remapped.y < 0.0) { + shadow = 1.0; + } + + return shadow; +} fn debug_grid(in: VertexOutput) -> vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; @@ -574,7 +618,7 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li return (shadow * (ambient_color + diffuse_color + specular_color)) * point_light.intensity; } -fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_light: Light, material: Material, specular_factor: vec3) -> vec3 { +fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_light: Light, material: Material, specular_factor: vec3, shadow: f32) -> vec3 { let light_color = spot_light.color; let light_pos = spot_light.position; let camera_view_pos = u_camera.position; @@ -615,7 +659,8 @@ fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_ligh //// end of spot light attenuation //// - return /*ambient_color +*/ diffuse_color + specular_color; + //return /*ambient_color +*/ diffuse_color + specular_color; + return (shadow * (diffuse_color + specular_color)); } fn calc_attenuation(light: Light, distance: f32) -> f32 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index 58be2c5..b924227 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -24,15 +24,10 @@ struct LightShadowMapUniform { @group(0) @binding(0) var u_light_shadow: array; -/*@group(0) @binding(1) -var u_light_pos: vec3; -@group(0) @binding(2) -var u_light_far_plane: f32;*/ @group(1) @binding(0) var u_model_transform_data: TransformData; - struct VertexOutput { @builtin(position) clip_position: vec4, diff --git a/lyra-math/src/angle.rs b/lyra-math/src/angle.rs index 30d541c..71ef507 100755 --- a/lyra-math/src/angle.rs +++ b/lyra-math/src/angle.rs @@ -10,7 +10,7 @@ pub fn radians_to_degrees(radians: f32) -> f32 { radians * 180.0 / PI } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Angle { Degrees(f32), Radians(f32), @@ -68,4 +68,18 @@ impl std::ops::SubAssign for Angle { Angle::Radians(r) => *r -= rhs.to_radians(), } } +} + +impl std::ops::Mul for Angle { + type Output = Angle; + + fn mul(self, rhs: f32) -> Self::Output { + Angle::Radians(self.to_radians() * rhs) + } +} + +impl std::ops::MulAssign for Angle { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } } \ No newline at end of file