diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ec37a89..ebf9987 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -5,8 +5,11 @@ use lyra_engine::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, - math::{self, Transform, Vec3}, - render::light::directional::DirectionalLight, + math::{self, Quat, Transform, Vec3}, + render::{ + graph::{ShadowCasterSettings, ShadowFilteringMode}, + light::{directional::DirectionalLight, PointLight}, + }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, @@ -99,7 +102,7 @@ async fn main() { fn setup_scene_plugin(game: &mut Game) { let world = game.world_mut(); let resman = world.get_resource_mut::(); - + /* let camera_gltf = resman .request::("../assets/AntiqueCamera.glb") .unwrap(); @@ -189,19 +192,27 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - /* world.spawn(( - //cube_mesh.clone(), + world.spawn(( + cube_mesh.clone(), PointLight { enabled: true, color: Vec3::new(0.133, 0.098, 0.91), intensity: 2.0, - range: 9.0, + range: 10.0, ..Default::default() }, - Transform::from_xyz(5.0, -2.5, -3.3), + ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcf, + ..Default::default() + }, + Transform::new( + Vec3::new(4.0 - 1.43, -13.0, 1.53), + Quat::IDENTITY, + Vec3::new(0.5, 0.5, 0.5), + ), )); - world.spawn(( + /* world.spawn(( //cube_mesh.clone(), PointLight { enabled: true, @@ -216,8 +227,14 @@ 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(-3.0, -8.0, -3.0); 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(-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)); + world.spawn((camera, FreeFlyCamera::default())); -} \ No newline at end of file +} diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 73edf51..ba2858a 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -135,6 +135,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") .as_buffer() .unwrap(); + let pcf_poisson_disc_3d = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d) + .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer3d") + .as_buffer() + .unwrap(); let pcss_poisson_disc = graph .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) .expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer") @@ -206,6 +211,16 @@ impl Node for MeshPass { }, count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); @@ -251,6 +266,14 @@ impl Node for MeshPass { }, wgpu::BindGroupEntry { binding: 6, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: pcf_poisson_disc_3d, + offset: 0, + size: None, + }), + }, + wgpu::BindGroupEntry { + binding: 7, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: pcss_poisson_disc, offset: 0, diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 8ab48f4..8ff4ef3 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,8 +1,11 @@ use std::{ - collections::VecDeque, mem, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard} + collections::VecDeque, + mem, + rc::Rc, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; -use fast_poisson::Poisson2D; +use fast_poisson::{Poisson2D, Poisson3D}; use glam::Vec2; use itertools::Itertools; use lyra_ecs::{ @@ -26,7 +29,7 @@ use crate::render::{ use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096); +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub enum ShadowMapsPassSlots { @@ -38,6 +41,7 @@ pub enum ShadowMapsPassSlots { ShadowLightUniformsBuffer, ShadowSettingsUniform, PcfPoissonDiscBuffer, + PcfPoissonDiscBuffer3d, PcssPoissonDiscBuffer, } @@ -101,7 +105,7 @@ impl ShadowMapsPass { device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE * 2, + SHADOW_SIZE * 8, ); let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -182,31 +186,57 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, - far_plane: f32, + are_settings_custom: bool, + shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { - const NEAR_PLANE: f32 = 0.1; - const FAR_PLANE: f32 = 45.0; - let mut atlas = self.atlas.get_mut(); + let u = ShadowSettingsUniform::new( + shadow_settings.filtering_mode, + shadow_settings.pcf_samples_num, + shadow_settings.pcss_blocker_search_samples, + ); + + 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 => { + let directional_size = SHADOW_SIZE * 4; // directional lights require a single map, so allocate that in the atlas. let atlas_index = atlas - .pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) + .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 projection = - glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); - + let projection = glam::Mat4::orthographic_rh( + -10.0, + 10.0, + -10.0, + 10.0, + 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 look_view = glam::Mat4::look_to_rh( light_pos.translation, light_pos.forward(), @@ -218,12 +248,15 @@ impl ShadowMapsPass { let u = LightShadowUniform { space_mat: light_proj, atlas_frame, - near_plane: NEAR_PLANE, - far_plane, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, light_size_uv, _padding1: 0, light_pos: light_pos.translation, - _padding2: 0, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + _padding2: [0; 2], }; let uniform_index = self.light_uniforms_buffer.insert(queue, &u); @@ -237,8 +270,8 @@ impl ShadowMapsPass { let projection = glam::Mat4::perspective_rh( Angle::Degrees(90.0).to_radians(), aspect, - NEAR_PLANE, - far_plane, + shadow_settings.near_plane, + shadow_settings.far_plane, ); let light_trans = light_pos.translation; @@ -307,12 +340,15 @@ impl ShadowMapsPass { &LightShadowUniform { space_mat: views[i], atlas_frame: frames[i], - near_plane: NEAR_PLANE, - far_plane, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, light_size_uv: 0.0, _padding1: 0, light_pos: light_trans, - _padding2: 0, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + _padding2: [0; 2], }, ); indices[i] = uniform_i; @@ -345,34 +381,69 @@ impl ShadowMapsPass { } /// Create the gpu buffer for a poisson disc - fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer { + fn create_poisson_disc_buffer( + &self, + device: &wgpu::Device, + label: &str, + dimension: u32, + num_samples: u32, + ) -> wgpu::Buffer { + debug_assert!( + dimension == 2 || dimension == 3, + "unknown dimension {dimension}, expected 2 (2d) or 3 (3d)" + ); + device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), - size: mem::size_of::() as u64 * (num_samples * 2) as u64, + size: mem::size_of::() as u64 * (num_samples * dimension) as u64, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }) } /// Generate and write a Poisson disc to `buffer` with `num_pcf_samples.pow(2)` amount of points. - fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_samples: u32) { - let num_floats = num_samples * 2; // points are vec2f + fn write_poisson_disc( + &self, + queue: &wgpu::Queue, + buffer: &wgpu::Buffer, + dimension: u32, + num_samples: u32, + ) { + debug_assert!( + dimension == 2 || dimension == 3, + "unknown dimension {dimension}, expected 2 (2d) or 3 (3d)" + ); + + let num_floats = num_samples * dimension; // points are vec2f let min_dist = (num_floats as f32).sqrt() / num_floats as f32; let mut points = vec![]; // use a while loop to ensure that the correct number of floats is created while points.len() < num_floats as usize { - let poisson = Poisson2D::new() - .with_dimensions([1.0, 1.0], min_dist) - .with_samples(num_samples); + if dimension == 2 { + let poisson = Poisson2D::new() + .with_dimensions([1.0, 1.0], min_dist) + .with_samples(num_samples); - points = poisson.iter().flatten() - .map(|p| p * 2.0 - 1.0) - .collect_vec(); - + points = poisson + .iter() + .flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); + } else if dimension == 3 { + let poisson = Poisson3D::new() + .with_dimensions([1.0, 1.0, 1.0], min_dist) + .with_samples(num_samples); + + points = poisson + .iter() + .flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); + } } points.truncate(num_floats as _); - + queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice())); } } @@ -441,7 +512,25 @@ impl Node for ShadowMapsPass { ShadowMapsPassSlots::PcfPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM_MAX), + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcf", + 2, + PCF_SAMPLES_NUM_MAX, + ), + ))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcfPoissonDiscBuffer3d, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcf_3d", + 3, + PCF_SAMPLES_NUM_MAX, + ), ))), ); @@ -449,7 +538,12 @@ impl Node for ShadowMapsPass { ShadowMapsPassSlots::PcssPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", PCSS_SAMPLES_NUM_MAX), + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcss", + 2, + PCSS_SAMPLES_NUM_MAX, + ), ))), ); @@ -462,28 +556,43 @@ impl Node for ShadowMapsPass { world: &mut lyra_ecs::World, context: &mut crate::render::graph::RenderGraphContext, ) { - world.add_resource_default_if_absent::(); - if world.has_resource_changed::() { + world.add_resource_default_if_absent::(); + if world.has_resource_changed::() { debug!("Detected change in ShadowSettings, recreating poisson disks"); - let settings = world.get_resource::(); + let settings = world.get_resource::(); // convert to uniform now since the from impl limits to max values let uniform = ShadowSettingsUniform::from(*settings); - let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) - .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, uniform.pcf_samples_num); + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc(&context.queue, &buffer, 2, uniform.pcf_samples_num); - let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) - .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, uniform.pcss_blocker_search_samples); + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc(&context.queue, &buffer, 3, uniform.pcf_samples_num); - context.queue_buffer_write_with( - ShadowMapsPassSlots::ShadowSettingsUniform, - 0, - uniform, + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc( + &context.queue, + &buffer, + 2, + uniform.pcss_blocker_search_samples, ); + + context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, uniform); } + let settings = *world.get_resource::(); self.render_meshes = world.try_get_resource_data::(); self.transform_buffers = world.try_get_resource_data::(); @@ -494,23 +603,48 @@ impl Node for ShadowMapsPass { // use a queue for storing atlas ids to add to entities after the entities are iterated let mut index_components_queue = VecDeque::new(); - for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { + for (entity, pos, shadow_settings, _) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + Has, + )>() { 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::Directional, entity, *pos, - 45.0, + custom_settings, + shadow_settings, ); index_components_queue.push_back((entity, atlas_index)); } } - for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { + for (entity, pos, shadow_settings, _) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + Has, + )>() { if !self.depth_maps.contains_key(&entity) { - let atlas_index = - self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0); + 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::Point, + entity, + *pos, + custom_settings, + shadow_settings, + ); index_components_queue.push_back((entity, atlas_index)); } } @@ -736,6 +870,38 @@ fn light_shadow_pass_impl<'a>( } } +/// Shadow casting settings for a light caster. +/// +/// Put this on an entity with a light source to override the global shadow +/// settings, the [`ShadowSettings`] resource. +#[derive(Debug, Copy, Clone, Component)] +pub struct ShadowCasterSettings { + 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, + pub near_plane: f32, + pub far_plane: f32, +} + +impl Default for ShadowCasterSettings { + fn default() -> Self { + Self { + filtering_mode: ShadowFilteringMode::default(), + pcf_samples_num: 25, + pcss_blocker_search_samples: 25, + near_plane: 0.1, + far_plane: 45.0, + } + } +} + #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct LightShadowUniform { @@ -747,7 +913,11 @@ pub struct LightShadowUniform { light_size_uv: f32, _padding1: u32, light_pos: glam::Vec3, - _padding2: u32, + /// Boolean casted as integer + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + _padding2: [u32; 2], } /// A component that stores the ID of a shadow map in the shadow map atlas for the entities. @@ -794,15 +964,15 @@ pub enum ShadowFilteringMode { Pcss, } -#[derive(Debug, Copy, Clone)] +/* #[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, @@ -816,13 +986,13 @@ impl Default for ShadowSettings { pcss_blocker_search_samples: 25, } } -} +} */ const PCF_SAMPLES_NUM_MAX: u32 = 128; const PCSS_SAMPLES_NUM_MAX: u32 = 128; /// Uniform version of [`ShadowSettings`]. -/// +/// /// If `pcf_samples_num` is set to zero, PCF and PCSS will be disabled. /// If `pcf_samples_num` is set to 2, ONLY hardware 2x2 PCF will be used. /// If `pcss_blocker_search_samples` is set to zero, PCSS will be disabled. @@ -834,12 +1004,27 @@ struct ShadowSettingsUniform { pcss_blocker_search_samples: u32, } -impl From for ShadowSettingsUniform { - fn from(value: ShadowSettings) -> Self { - let raw_pcf_samples = value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); - let raw_pcss_samples = value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); +impl From for ShadowSettingsUniform { + fn from(value: ShadowCasterSettings) -> Self { + Self::new( + value.filtering_mode, + value.pcf_samples_num, + value.pcss_blocker_search_samples, + ) + } +} - let (pcf_samples, pcss_samples) = match value.filtering_mode { +impl ShadowSettingsUniform { + /// Create a new shadow settings uniform. + pub fn new( + filter_mode: ShadowFilteringMode, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + ) -> Self { + let raw_pcf_samples = pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); + let raw_pcss_samples = pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); + + let (pcf_samples, pcss_samples) = match filter_mode { ShadowFilteringMode::None => (0, 0), ShadowFilteringMode::Pcf2x2 => (2, 0), ShadowFilteringMode::Pcf => (raw_pcf_samples, 0), diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 3c7b17a..0ad3344 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -119,6 +119,10 @@ struct LightShadowMapUniform { far_plane: f32, light_size_uv: f32, light_pos: vec3, + /// boolean casted as u32 + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } struct ShadowSettingsUniform { @@ -144,6 +148,8 @@ var u_light_shadow: array; @group(5) @binding(5) var u_pcf_poisson_disc: array>; @group(5) @binding(6) +var u_pcf_poisson_disc_3d: array>; +@group(5) @binding(7) var u_pcss_poisson_disc: array>; @fragment @@ -180,7 +186,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u); light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } else if (light.light_ty == LIGHT_TY_POINT) { - let shadow = calc_shadow_point(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); + 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); @@ -249,6 +255,16 @@ fn coords_to_cube_atlas(tex_coord: vec3) -> vec3 { return vec3(res, f32(cube_idx)); } +/// Get shadow settings for a light. +/// Returns x as `pcf_samples_num` and y as `pcss_blocker_search_samples`. +fn get_shadow_settings(shadow_u: LightShadowMapUniform) -> vec2 { + if shadow_u.has_shadow_settings == 1u { + return vec2(shadow_u.pcf_samples_num, shadow_u.pcss_blocker_search_samples); + } else { + return vec2(u_shadow_settings.pcf_samples_num, u_shadow_settings.pcss_blocker_search_samples); + } +} + fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, shadow_u: LightShadowMapUniform) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is flipped after transforming @@ -261,21 +277,28 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); let current_depth = proj_coords.z - bias; + // get settings + let settings = get_shadow_settings(shadow_u); + let pcf_samples_num = settings.x; + let pcss_blocker_search_samples = settings.y; + var shadow = 0.0; - if u_shadow_settings.pcf_samples_num > 0u && u_shadow_settings.pcss_blocker_search_samples > 0u { + // hardware 2x2 PCF via camparison sampler + if pcf_samples_num == 2u { + let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); + 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(xy_remapped, current_depth, shadow_u); } - // hardware 2x2 PCF via camparison sampler - else if u_shadow_settings.pcf_samples_num == 2u { - let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); - shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth); - } else if u_shadow_settings.pcf_samples_num > 0u { - let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); - // TODO: should texel size be using the entire atlas dimensions, or just the frame? - let texel_size = 1.0 / f32(atlas_dimensions.x); // f32(shadow_u.atlas_frame.width) - + // only PCF + else if pcf_samples_num > 0u { + let texel_size = 1.0 / f32(shadow_u.atlas_frame.width); shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size); - } else { // pcf_samples_num == 0 + } + // no filtering + else { let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); 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); @@ -304,14 +327,19 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2) -> let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); // get the rect of the frame as a vec4 - var region_rect = vec4(f32(shadow_u.atlas_frame.x), f32(shadow_u.atlas_frame.y), f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); + var region_rect = vec4(f32(shadow_u.atlas_frame.x), f32(shadow_u.atlas_frame.y), + f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); // put the frame rect in atlas UV space region_rect /= f32(atlas_dimensions.x); + + // calculate a relatively tiny offset to avoid getting the end of the frame and causing + // linear or nearest filtering to bleed to the adjacent frame. + let texel_size = (1.0 / f32(shadow_u.atlas_frame.x)) * 4.0; // lerp input coords let region_coords = vec2( - mix(region_rect.x, region_rect.x + region_rect.z, coords.x), - mix(region_rect.y, region_rect.y + region_rect.w, coords.y) + mix(region_rect.x + texel_size, region_rect.x + region_rect.z - texel_size, coords.x), + mix(region_rect.y + texel_size, region_rect.y + region_rect.w - texel_size, coords.y) ); return region_coords; @@ -372,45 +400,96 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa return saturate(shadow); } -fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> f32 { +fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> f32 { var frag_to_light = world_pos - light.position; let temp = coords_to_cube_atlas(normalize(frag_to_light)); var coords_2d = temp.xy; let cube_idx = i32(temp.z); - - /// if an unknown cube side was returned, something is broken - /*if cube_idx == 0 { - return 0.0; - }*/ var indices = light.light_shadow_uniform_index; let i = indices[cube_idx - 1]; let u: LightShadowMapUniform = u_light_shadow[i]; + + let uniforms = array( + u_light_shadow[indices[0]], + u_light_shadow[indices[1]], + u_light_shadow[indices[2]], + u_light_shadow[indices[3]], + u_light_shadow[indices[4]], + u_light_shadow[indices[5]] + ); - // get the atlas frame in [0; 1] in the atlas texture - // z is width, w is height - var region_coords = vec4(f32(u.atlas_frame.x), f32(u.atlas_frame.y), f32(u.atlas_frame.width), f32(u.atlas_frame.height)); - region_coords /= f32(atlas_dimensions.x); - - // simulate `ClampToBorder`, not creating shadows past the shadow map regions - /*if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) { - return 0.0; - }*/ - - // get the coords inside of the region - coords_2d.x = mix(region_coords.x, region_coords.x + region_coords.z, coords_2d.x); - coords_2d.y = mix(region_coords.y, region_coords.y + region_coords.w, coords_2d.y); - - // use a bias to avoid shadow acne - let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005); - var current_depth = length(frag_to_light) - bias; + var current_depth = length(frag_to_light); current_depth /= u.far_plane; + current_depth -= 0.005; // TODO: find a better way to calculate bias - var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, coords_2d, current_depth); + // get settings + let settings = get_shadow_settings(u); + 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(u, coords_2d); + 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 + 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); + } + // no filtering + else { + let region_coords = to_atlas_frame_coords(u, coords_2d); + 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); + } return shadow; } +/// Calculate the shadow coefficient using PCF of a directional light +fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, samples_num: u32, uv_radius: f32) -> f32 { + var shadow_unis = shadow_us; + + var shadow = 0.0; + for (var i = 0; i < i32(samples_num); i++) { + var temp = coords_to_cube_atlas(tex_coords); + var coords_2d = temp.xy; + var cube_idx = i32(temp.z); + var shadow_u = shadow_unis[cube_idx - 1]; + + coords_2d += u_pcf_poisson_disc[i] * uv_radius; + + let new_coords = to_atlas_frame_coords(shadow_u, coords_2d); + shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth); + } + shadow /= f32(samples_num); + + // clamp shadow to [0; 1] + return saturate(shadow); +} + +/*fn pcf_point_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { + var shadow = 0.0; + for (var i = 0; i < i32(samples_num); i++) { + let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius; + let new_coords = to_atlas_frame_coords(shadow_u, offset); + + shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth); + } + shadow /= f32(samples_num); + + // clamp shadow to [0; 1] + return saturate(shadow); +}*/ + fn debug_grid(in: VertexOutput) -> vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; let tile_index = vec2(floor(tile_index_float)); @@ -485,7 +564,8 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li diffuse_color *= attenuation; specular_color *= attenuation; - return (ambient_color + (1.0 - shadow) * (diffuse_color + specular_color)) * point_light.intensity; + //return (ambient_color + shadow * (diffuse_color + specular_color)) * point_light.intensity; + 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 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index b8e17fc..4e9bf16 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -13,7 +13,12 @@ struct LightShadowMapUniform { atlas_frame: TextureAtlasFrame, near_plane: f32, far_plane: f32, + light_size_uv: f32, light_pos: vec3, + /// boolean casted as u32 + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } @group(0) @binding(0)