diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ba929aa..1391c6f 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -8,7 +8,7 @@ use lyra_engine::{ math::{self, Quat, Transform, Vec3}, render::{ graph::{ShadowCasterSettings, ShadowFilteringMode}, - light::{directional::DirectionalLight, PointLight}, + light::{directional::DirectionalLight, PointLight, SpotLight}, }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, @@ -184,12 +184,15 @@ fn setup_scene_plugin(game: &mut Game) { }, ShadowCasterSettings { filtering_mode: ShadowFilteringMode::Pcss, + pcf_samples_num: 64, + pcss_blocker_search_samples: 36, + constant_depth_bias_scale: 5.0, ..Default::default() }, light_tran, )); - world.spawn(( + /* world.spawn(( cube_mesh.clone(), PointLight { enabled: true, @@ -207,31 +210,40 @@ fn setup_scene_plugin(game: &mut Game) { Quat::IDENTITY, Vec3::new(0.5, 0.5, 0.5), ), - )); + )); */ - /* world.spawn(( - //cube_mesh.clone(), - PointLight { + 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(0.278, 0.984, 0.0), - intensity: 2.0, - range: 9.0, + color: Vec3::new(1.0, 0.0, 0.0), + intensity: 3.0, + range: 4.5, + //cutoff: math::Angle::Degrees(45.0), ..Default::default() }, - Transform::from_xyz(-0.5, 2.0, -5.0), - )); */ + /* ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcf, + ..Default::default() + }, */ + WorldTransform::from(t), + t, + //cube_mesh.clone(), + )); } 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.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..3df7011 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 { @@ -265,7 +266,58 @@ impl ShadowMapsPass { indices[0] = uniform_index; (atlas_index, indices) } - LightType::Spotlight => todo!(), + LightType::Spotlight => { + let directional_size = SHADOW_SIZE * 4; + // directional lights require a single map, so allocate that in the atlas. + let atlas_index = atlas + .pack(directional_size.x as _, directional_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( + (light_half_outer_angle.unwrap() * 2.0).to_radians(), + aspect, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + // honestly no clue why this works, but I got it from here and the results are good + // https://github.com/asylum2010/Asylum_Tutorials/blob/423e5edfaee7b5ea450a450e65f2eabf641b2482/ShaderTutors/43_ShadowMapFiltering/main.cpp#L323 + /* let frustum_size = Vec2::new(0.5 * projection.col(0).x, 0.5 * projection.col(1).y); + // maybe its better to make this a vec2 on the gpu? + let size_avg = (frustum_size.x + frustum_size.y) / 2.0; + let light_size_uv = 0.2 * size_avg; */ + + let light_trans = light_pos.translation; + let look_view = glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(1.0, 0.0, 0.0), + glam::vec3(0.0, -1.0, 0.0), + ); + + let light_proj = projection * look_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 +678,7 @@ impl Node for ShadowMapsPass { LightType::Directional, entity, *pos, + None, custom_settings, shadow_settings, ); @@ -649,6 +702,31 @@ impl Node for ShadowMapsPass { LightType::Point, 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, ); @@ -807,7 +885,23 @@ impl Node for ShadowMapsPass { ); } } - LightType::Spotlight => todo!(), + LightType::Spotlight => { + pass.set_pipeline(&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 _, + ); + }, } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 2bab59e..0b2f233 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -190,7 +190,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, atlas_dimensions); + light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } } @@ -482,11 +483,62 @@ 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 calc_shadow_spot_light(world_pos: vec3, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> 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; - for (var i = 0; i < i32(samples_num); i++) { + // 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; +} + +/// Calculate the shadow coefficient using PCF of a directional light +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 < 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 +546,7 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; @@ -574,7 +626,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; @@ -609,13 +661,13 @@ fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_ligh let distance = length(light_pos - world_pos); let attenuation = calc_attenuation(spot_light, distance); - ambient_color *= attenuation * spot_light.intensity * cone; - diffuse_color *= attenuation * spot_light.intensity * cone; - specular_color *= attenuation * spot_light.intensity * cone; + ambient_color *= attenuation * cone; + diffuse_color *= attenuation * cone; + specular_color *= attenuation * cone; //// 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)) * spot_light.intensity; } fn calc_attenuation(light: Light, distance: f32) -> f32 { 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