diff --git a/examples/assets/shadows-platform-palmtree.glb b/examples/assets/shadows-platform-palmtree.glb new file mode 100644 index 0000000..66a3dfd Binary files /dev/null and b/examples/assets/shadows-platform-palmtree.glb differ diff --git a/examples/assets/wood-platform/model.bin b/examples/assets/wood-platform/model.bin new file mode 100644 index 0000000..d3bdc51 Binary files /dev/null and b/examples/assets/wood-platform/model.bin differ diff --git a/examples/assets/wood-platform/model.gltf b/examples/assets/wood-platform/model.gltf new file mode 100644 index 0000000..5b1d496 --- /dev/null +++ b/examples/assets/wood-platform/model.gltf @@ -0,0 +1,142 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.1.63", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Cube", + "scale":[ + 10, + 0.25, + 10 + ] + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Material.001", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0, + "roughnessFactor":0.5 + } + } + ], + "meshes":[ + { + "name":"Cube.001", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/jpeg", + "name":"wood1", + "uri":"wood1.jpg" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":24, + "max":[ + 1, + 1, + 1 + ], + "min":[ + -1, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":24, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":24, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":36, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":288, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":288, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":576, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":768, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":840, + "uri":"model.bin" + } + ] +} diff --git a/examples/assets/wood-platform/wood1.jpg b/examples/assets/wood-platform/wood1.jpg new file mode 100644 index 0000000..7fc7cef Binary files /dev/null and b/examples/assets/wood-platform/wood1.jpg differ diff --git a/examples/assets/wood-platform/wood1OLD.jpg b/examples/assets/wood-platform/wood1OLD.jpg new file mode 100644 index 0000000..2cbb3d3 Binary files /dev/null and b/examples/assets/wood-platform/wood1OLD.jpg differ diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 56a31c0..ec37a89 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -128,15 +128,29 @@ fn setup_scene_plugin(game: &mut Game) { platform_gltf.wait_recurse_dependencies_load(); let platform_mesh = &platform_gltf.data_ref().unwrap().scenes[0]; + let palm_tree_platform_gltf = resman + .request::("../assets/shadows-platform-palmtree.glb") + .unwrap(); + + palm_tree_platform_gltf.wait_recurse_dependencies_load(); + let palm_tree_platform_mesh = &palm_tree_platform_gltf.data_ref().unwrap().scenes[0]; + drop(resman); // cube in the air - world.spawn(( + /* world.spawn(( cube_mesh.clone(), WorldTransform::default(), Transform::from_xyz(0.0, -2.0, -5.0), )); + // cube really high in the air + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(-6.0, 0.0, -5.0), + )); + // cube on the right, on the ground world.spawn(( cube_mesh.clone(), @@ -149,10 +163,19 @@ fn setup_scene_plugin(game: &mut Game) { WorldTransform::default(), //Transform::from_xyz(0.0, -5.0, -5.0), Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)), + )); */ + + world.spawn(( + palm_tree_platform_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(5.0, -15.0, 0.0), + //Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)), )); + //shadows-platform-palmtree.glb + { - let mut light_tran = Transform::from_xyz(0.0, 2.5, 0.0); + let mut light_tran = Transform::from_xyz(0.0, 0.0, 0.0); light_tran.scale = Vec3::new(0.5, 0.5, 0.5); light_tran.rotate_x(math::Angle::Degrees(-45.0)); light_tran.rotate_y(math::Angle::Degrees(-35.0)); @@ -192,7 +215,9 @@ 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.rotate_x(math::Angle::Degrees(-17.0)); + //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.rotate_x(math::Angle::Degrees(-27.0)); + camera.transform.rotate_y(math::Angle::Degrees(-55.0)); world.spawn((camera, FreeFlyCamera::default())); } \ No newline at end of file diff --git a/lyra-game/src/render/graph/node.rs b/lyra-game/src/render/graph/node.rs index f0cdcfe..7a39a4b 100644 --- a/lyra-game/src/render/graph/node.rs +++ b/lyra-game/src/render/graph/node.rs @@ -63,6 +63,14 @@ pub enum SlotValue { } impl SlotValue { + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + pub fn is_lazy(&self) -> bool { + matches!(self, Self::Lazy) + } + pub fn as_texture_view(&self) -> Option<&Arc> { bind_match!(self, Self::TextureView(v) => v) } diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 99d127d..73edf51 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -115,6 +115,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .as_sampler() .unwrap(); + let atlas_sampler_compare = graph + .slot_value(ShadowMapsPassSlots::ShadowAtlasSamplerComparison) + .expect("missing ShadowMapsPassSlots::ShadowAtlasSamplerComparison") + .as_sampler() + .unwrap(); let shadow_settings_buf = graph .slot_value(ShadowMapsPassSlots::ShadowSettingsUniform) .expect("missing ShadowMapsPassSlots::ShadowSettingsUniform") @@ -130,6 +135,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") .as_buffer() .unwrap(); + let pcss_poisson_disc = graph + .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer") + .as_buffer() + .unwrap(); let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bgl_shadows_atlas"), @@ -147,11 +157,17 @@ impl Node for MeshPass { wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, @@ -161,7 +177,7 @@ impl Node for MeshPass { count: None, }, wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 4, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, @@ -171,7 +187,17 @@ impl Node for MeshPass { count: None, }, wgpu::BindGroupLayoutEntry { - binding: 4, + binding: 5, + 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, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, @@ -197,6 +223,10 @@ impl Node for MeshPass { }, wgpu::BindGroupEntry { binding: 2, + resource: wgpu::BindingResource::Sampler(atlas_sampler_compare), + }, + wgpu::BindGroupEntry { + binding: 3, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: shadow_settings_buf, offset: 0, @@ -204,7 +234,7 @@ impl Node for MeshPass { }), }, wgpu::BindGroupEntry { - binding: 3, + binding: 4, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: light_uniform_buf, offset: 0, @@ -212,13 +242,21 @@ impl Node for MeshPass { }), }, wgpu::BindGroupEntry { - binding: 4, + binding: 5, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: pcf_poisson_disc, offset: 0, size: None, }), }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: pcss_poisson_disc, + offset: 0, + size: None, + }), + }, ], }); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index b9b418f..e4e1b61 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -3,6 +3,7 @@ use std::{ }; use fast_poisson::Poisson2D; +use glam::Vec2; use itertools::Itertools; use lyra_ecs::{ query::{filter::Has, Entities}, @@ -25,18 +26,19 @@ use crate::render::{ use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const PCF_SAMPLES_NUM: u32 = 6; -const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub enum ShadowMapsPassSlots { ShadowAtlasTexture, ShadowAtlasTextureView, ShadowAtlasSampler, + ShadowAtlasSamplerComparison, ShadowAtlasSizeBuffer, ShadowLightUniformsBuffer, ShadowSettingsUniform, PcfPoissonDiscBuffer, + PcssPoissonDiscBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -74,6 +76,7 @@ pub struct ShadowMapsPass { atlas: LightShadowMapAtlas, /// The depth map atlas sampler atlas_sampler: Rc, + atlas_sampler_compare: Rc, } impl ShadowMapsPass { @@ -98,7 +101,7 @@ impl ShadowMapsPass { device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE * 8, + SHADOW_SIZE * 2, ); let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -107,16 +110,28 @@ impl ShadowMapsPass { contents: bytemuck::bytes_of(&atlas.atlas_size()), }); + let sampler_compare = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("compare_sampler_shadow_map_atlas"), + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler_shadow_map_atlas"), address_mode_u: wgpu::AddressMode::ClampToBorder, address_mode_v: wgpu::AddressMode::ClampToBorder, address_mode_w: wgpu::AddressMode::ClampToBorder, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), - compare: Some(wgpu::CompareFunction::LessEqual), ..Default::default() }); @@ -155,6 +170,7 @@ impl ShadowMapsPass { point_light_pipeline: None, atlas_sampler: Rc::new(sampler), + atlas_sampler_compare: Rc::new(sampler_compare), atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), } } @@ -183,6 +199,14 @@ impl ShadowMapsPass { let projection = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, 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(), @@ -196,7 +220,8 @@ impl ShadowMapsPass { atlas_frame, near_plane: NEAR_PLANE, far_plane, - _padding1: [0; 2], + light_size_uv, + _padding1: 0, light_pos: light_pos.translation, _padding2: 0, }; @@ -284,7 +309,8 @@ impl ShadowMapsPass { atlas_frame: frames[i], near_plane: NEAR_PLANE, far_plane, - _padding1: [0; 2], + light_size_uv: 0.0, + _padding1: 0, light_pos: light_trans, _padding2: 0, }, @@ -322,26 +348,29 @@ impl ShadowMapsPass { fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), - size: mem::size_of::() as u64 * (num_samples.pow(2)) as u64, + size: mem::size_of::() as u64 * (num_samples * 2) 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_pcf_samples: u32) { - let num_points = num_pcf_samples.pow(2); - let num_floats = num_points * 2; // points are vec2f + fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_samples: u32) { + //let num_points = num_samples.pow(2); + let num_floats = num_samples * 2; // points are vec2f let min_dist = (num_floats as f32).sqrt() / num_floats as f32; + //let min_dist = (num_samples as f32).sqrt() / num_samples 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_pcf_samples); + .with_samples(num_samples); - points = poisson.iter().flatten().collect_vec(); + points = poisson.iter().flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); } points.truncate(num_floats as _); @@ -377,6 +406,12 @@ impl Node for ShadowMapsPass { Some(SlotValue::Sampler(self.atlas_sampler.clone())), ); + node.add_sampler_slot( + ShadowMapsPassSlots::ShadowAtlasSamplerComparison, + SlotAttribute::Output, + Some(SlotValue::Sampler(self.atlas_sampler_compare.clone())), + ); + node.add_buffer_slot( ShadowMapsPassSlots::ShadowLightUniformsBuffer, SlotAttribute::Output, @@ -404,11 +439,20 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(Arc::new(settings_buffer))), ); + let def_settings = ShadowSettings::default(); node.add_buffer_slot( ShadowMapsPassSlots::PcfPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM), + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", def_settings.pcf_samples_num), + ))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcssPoissonDiscBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", def_settings.pcss_blocker_search_samples), ))), ); @@ -424,9 +468,14 @@ impl Node for ShadowMapsPass { { // TODO: Update the poisson disc every time the PCF sampling point number changed if !world.has_resource::() { + let def_settings = ShadowSettings::default(); let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, ShadowSettings::default().pcf_samples_num); + self.write_poisson_disc(&context.queue, &buffer, def_settings.pcf_samples_num); + + let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .unwrap().as_buffer().unwrap(); + self.write_poisson_disc(&context.queue, &buffer, def_settings.pcss_blocker_search_samples); } // TODO: only write buffer on changes to resource @@ -717,7 +766,9 @@ pub struct LightShadowUniform { atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed near_plane: f32, far_plane: f32, - _padding1: [u32; 2], + /// Light size in UV space (light_size / frustum_size) + light_size_uv: f32, + _padding1: u32, light_pos: glam::Vec3, _padding2: u32, } @@ -758,13 +809,22 @@ impl LightShadowMapAtlas { #[derive(Debug, Copy, Clone)] pub struct ShadowSettings { + /// How many PCF filtering samples are used per dimension. + /// + /// A value of 16 is common. 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 16 is common. + pub pcss_blocker_search_samples: u32, } impl Default for ShadowSettings { fn default() -> Self { Self { - pcf_samples_num: PCF_SAMPLES_NUM, + pcf_samples_num: 64, + pcss_blocker_search_samples: 36, } } } @@ -774,12 +834,14 @@ impl Default for ShadowSettings { #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct ShadowSettingsUniform { pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } impl From for ShadowSettingsUniform { fn from(value: ShadowSettings) -> Self { Self { pcf_samples_num: value.pcf_samples_num, + pcss_blocker_search_samples: value.pcss_blocker_search_samples, } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 0e026f2..3d8a627 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -117,11 +117,13 @@ struct LightShadowMapUniform { atlas_frame: TextureAtlasFrame, near_plane: f32, far_plane: f32, + light_size_uv: f32, light_pos: vec3, } struct ShadowSettingsUniform { pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } @group(4) @binding(0) @@ -132,13 +134,17 @@ var t_light_grid: texture_storage_2d; // rg32uint = vec2 u_shadow_settings: ShadowSettingsUniform; +var s_shadow_maps_atlas_compare: sampler_comparison; @group(5) @binding(3) -var u_light_shadow: array; +var u_shadow_settings: ShadowSettingsUniform; @group(5) @binding(4) +var u_light_shadow: array; +@group(5) @binding(5) var u_pcf_poisson_disc: array>; +@group(5) @binding(6) +var u_pcss_poisson_disc: array>; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -185,8 +191,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } -/// Get the cube map side index of a 3d texture coord +/// Convert 3d coords for an unwrapped cubemap to 2d coords and a side index of the cube map. /// +/// The `xy` components are the 2d coordinates in the side of the cube, and `z` is the cube +/// map side index. +/// +/// Cube map index results: /// 0 -> UNKNOWN /// 1 -> right /// 2 -> left @@ -194,7 +204,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { /// 4 -> bottom /// 5 -> near /// 6 -> far -fn get_side_idx(tex_coord: vec3) -> vec3 { +fn coords_to_cube_atlas(tex_coord: vec3) -> vec3 { let abs_x = abs(tex_coord.x); let abs_y = abs(tex_coord.y); let abs_z = abs(tex_coord.z); @@ -234,14 +244,7 @@ fn get_side_idx(tex_coord: vec3) -> vec3 { } res = (res / abs(major_axis) + 1.0) * 0.5; - //res = normalize(res); - //res.y = 1.0-res.y; // invert y because wgsl - //let t = res.x; - //res.x = res.y; - - //res.y = 1.0 - t; res.y = 1.0 - res.y; - //res.x = 1.0 - res.x; return vec3(res, f32(cube_idx)); } @@ -264,10 +267,11 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light ); // use a bias to avoid shadow acne - let bias = max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); + let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); let current_depth = proj_coords.z - bias; - var shadow = pcf_dir_light(region_coords, current_depth, shadow_u); + //var shadow = pcf_dir_light(region_coords, current_depth, shadow_u, 1.0); + var shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u); // dont cast shadows outside the light's far plane if (proj_coords.z > 1.0) { @@ -282,35 +286,87 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light return shadow; } -/// Calculate the shadow coefficient using PCF of a directional light -fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { - let half_filter_size = f32(u_shadow_settings.pcf_samples_num) / 2.0; - let texel_size = 1.0 / vec2(f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); +// Comes from https://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf +fn search_width(light_near: f32, uv_light_size: f32, receiver_depth: f32) -> f32 { + return uv_light_size * (receiver_depth - light_near) / receiver_depth; +} - // Sample PCF - var shadow = 0.0; - var i = 0; - for (var x = -half_filter_size; x <= half_filter_size; x += 1.0) { - for (var y = -half_filter_size; y <= half_filter_size; y += 1.0) { - //let random = u_pcf_poisson_disc[i] * texel_size; - let offset = tex_coords + (u_pcf_poisson_disc[i] + vec2(x, y)) * texel_size; - - let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth); - shadow += pcf_depth; +/// Convert texture coords to be texture coords of an atlas frame. +fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2) -> 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)); + // put the frame rect in atlas UV space + region_rect /= f32(atlas_dimensions.x); + + // 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) + ); + + return region_coords; +} - i++; +/// 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 { + 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 offset_coords = tex_coords + u_pcss_poisson_disc[i] * search_width; + let new_coords = to_atlas_frame_coords(shadow_u, offset_coords); + let z = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, new_coords, 0.0); + + if z < (receiver_depth - bias) { + blockers += 1; + avg_dist += z; } } - shadow /= pow(f32(u_shadow_settings.pcf_samples_num), 2.0); - // ensure the shadow value does not go above 1.0 - shadow = min(shadow, 1.0); - return shadow; + let b = f32(blockers); + 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); + + // If no blockers were found, exit now to save in filtering + if blocker_search.y == 0.0 { + return 1.0; + } + let blocker_depth = blocker_search.x; + + // penumbra estimation + let penumbra_width = (receiver_depth - blocker_depth) / blocker_depth; + + // 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); +} + +/// 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 { + 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); + + 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 calc_shadow_point(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 = get_side_idx(normalize(frag_to_light)); + let temp = coords_to_cube_atlas(normalize(frag_to_light)); var coords_2d = temp.xy; let cube_idx = i32(temp.z); @@ -342,7 +398,7 @@ fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: v var current_depth = length(frag_to_light) - bias; current_depth /= u.far_plane; - var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, current_depth); + var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, coords_2d, current_depth); return shadow; }