render: implement PCSS for directional lights

This commit is contained in:
SeanOMik 2024-07-18 23:43:08 -04:00
parent 4c6c6c4dd5
commit 4449172c2b
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
10 changed files with 394 additions and 63 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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::<Gltf>("../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()));
}

View File

@ -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<wgpu::TextureView>> {
bind_match!(self, Self::TextureView(v) => v)
}

View File

@ -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,
}),
},
],
});

View File

@ -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<wgpu::Sampler>,
atlas_sampler_compare: Rc<wgpu::Sampler>,
}
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::<glam::Vec2>() as u64 * (num_samples.pow(2)) as u64,
size: mem::size_of::<glam::Vec2>() 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::<ShadowSettings>() {
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<ShadowSettings> for ShadowSettingsUniform {
fn from(value: ShadowSettings) -> Self {
Self {
pcf_samples_num: value.pcf_samples_num,
pcss_blocker_search_samples: value.pcss_blocker_search_samples,
}
}
}

View File

@ -117,11 +117,13 @@ struct LightShadowMapUniform {
atlas_frame: TextureAtlasFrame,
near_plane: f32,
far_plane: f32,
light_size_uv: f32,
light_pos: vec3<f32>,
}
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, read_write>; // rg32uint = vec2<u
@group(5) @binding(0)
var t_shadow_maps_atlas: texture_depth_2d;
@group(5) @binding(1)
var s_shadow_maps_atlas: sampler_comparison;
var s_shadow_maps_atlas: sampler;
@group(5) @binding(2)
var<uniform> u_shadow_settings: ShadowSettingsUniform;
var s_shadow_maps_atlas_compare: sampler_comparison;
@group(5) @binding(3)
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
var<uniform> u_shadow_settings: ShadowSettingsUniform;
@group(5) @binding(4)
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
@group(5) @binding(5)
var<storage, read> u_pcf_poisson_disc: array<vec2<f32>>;
@group(5) @binding(6)
var<storage, read> u_pcss_poisson_disc: array<vec2<f32>>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
@ -185,8 +191,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(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<f32> {
/// 4 -> bottom
/// 5 -> near
/// 6 -> far
fn get_side_idx(tex_coord: vec3<f32>) -> vec3<f32> {
fn coords_to_cube_atlas(tex_coord: vec3<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
}
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<f32>(res, f32(cube_idx));
}
@ -264,10 +267,11 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, 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<f32>, light_dir: vec3<f32>, frag_pos_light
return shadow;
}
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_dir_light(tex_coords: vec2<f32>, 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>(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<f32>(x, y)) * texel_size;
/// Convert texture coords to be texture coords of an atlas frame.
fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2<f32>) -> vec2<f32> {
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth);
shadow += pcf_depth;
// get the rect of the frame as a vec4
var region_rect = vec4<f32>(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);
i++;
// lerp input coords
let region_coords = vec2<f32>(
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;
}
/// Find the average blocker distance for a directiona llight
fn find_blocker_distance_dir_light(tex_coords: vec2<f32>, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2<f32> {
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<f32>(avg_dist / b, b);
}
fn pcss_dir_light(tex_coords: vec2<f32>, 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<f32>, 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<f32>, world_normal: vec3<f32>, light_dir: vec3<f32>, light: Light, atlas_dimensions: vec2<i32>) -> 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<f32>, world_normal: vec3<f32>, 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;
}