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(); platform_gltf.wait_recurse_dependencies_load();
let platform_mesh = &platform_gltf.data_ref().unwrap().scenes[0]; 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); drop(resman);
// cube in the air // cube in the air
world.spawn(( /* world.spawn((
cube_mesh.clone(), cube_mesh.clone(),
WorldTransform::default(), WorldTransform::default(),
Transform::from_xyz(0.0, -2.0, -5.0), 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 // cube on the right, on the ground
world.spawn(( world.spawn((
cube_mesh.clone(), cube_mesh.clone(),
@ -149,10 +163,19 @@ fn setup_scene_plugin(game: &mut Game) {
WorldTransform::default(), WorldTransform::default(),
//Transform::from_xyz(0.0, -5.0, -5.0), //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)), 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.scale = Vec3::new(0.5, 0.5, 0.5);
light_tran.rotate_x(math::Angle::Degrees(-45.0)); light_tran.rotate_x(math::Angle::Degrees(-45.0));
light_tran.rotate_y(math::Angle::Degrees(-35.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(); let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); //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(-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())); world.spawn((camera, FreeFlyCamera::default()));
} }

View File

@ -63,6 +63,14 @@ pub enum SlotValue {
} }
impl 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>> { pub fn as_texture_view(&self) -> Option<&Arc<wgpu::TextureView>> {
bind_match!(self, Self::TextureView(v) => v) bind_match!(self, Self::TextureView(v) => v)
} }

View File

@ -115,6 +115,11 @@ impl Node for MeshPass {
.expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler")
.as_sampler() .as_sampler()
.unwrap(); .unwrap();
let atlas_sampler_compare = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasSamplerComparison)
.expect("missing ShadowMapsPassSlots::ShadowAtlasSamplerComparison")
.as_sampler()
.unwrap();
let shadow_settings_buf = graph let shadow_settings_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowSettingsUniform) .slot_value(ShadowMapsPassSlots::ShadowSettingsUniform)
.expect("missing ShadowMapsPassSlots::ShadowSettingsUniform") .expect("missing ShadowMapsPassSlots::ShadowSettingsUniform")
@ -130,6 +135,11 @@ impl Node for MeshPass {
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer")
.as_buffer() .as_buffer()
.unwrap(); .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 { let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_shadows_atlas"), label: Some("bgl_shadows_atlas"),
@ -147,11 +157,17 @@ impl Node for MeshPass {
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
binding: 1, binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None, count: None,
}, },
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
binding: 2, binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
@ -161,7 +177,7 @@ impl Node for MeshPass {
count: None, count: None,
}, },
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
binding: 3, binding: 4,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true }, ty: wgpu::BufferBindingType::Storage { read_only: true },
@ -171,7 +187,17 @@ impl Node for MeshPass {
count: None, count: None,
}, },
wgpu::BindGroupLayoutEntry { 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, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true }, ty: wgpu::BufferBindingType::Storage { read_only: true },
@ -197,6 +223,10 @@ impl Node for MeshPass {
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 2, binding: 2,
resource: wgpu::BindingResource::Sampler(atlas_sampler_compare),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: shadow_settings_buf, buffer: shadow_settings_buf,
offset: 0, offset: 0,
@ -204,7 +234,7 @@ impl Node for MeshPass {
}), }),
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 3, binding: 4,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: light_uniform_buf, buffer: light_uniform_buf,
offset: 0, offset: 0,
@ -212,13 +242,21 @@ impl Node for MeshPass {
}), }),
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 4, binding: 5,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc, buffer: pcf_poisson_disc,
offset: 0, offset: 0,
size: None, 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 fast_poisson::Poisson2D;
use glam::Vec2;
use itertools::Itertools; use itertools::Itertools;
use lyra_ecs::{ use lyra_ecs::{
query::{filter::Has, Entities}, query::{filter::Has, Entities},
@ -25,18 +26,19 @@ use crate::render::{
use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; use super::{MeshBufferStorage, RenderAssets, RenderMeshes};
const PCF_SAMPLES_NUM: u32 = 6; const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096);
const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024);
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub enum ShadowMapsPassSlots { pub enum ShadowMapsPassSlots {
ShadowAtlasTexture, ShadowAtlasTexture,
ShadowAtlasTextureView, ShadowAtlasTextureView,
ShadowAtlasSampler, ShadowAtlasSampler,
ShadowAtlasSamplerComparison,
ShadowAtlasSizeBuffer, ShadowAtlasSizeBuffer,
ShadowLightUniformsBuffer, ShadowLightUniformsBuffer,
ShadowSettingsUniform, ShadowSettingsUniform,
PcfPoissonDiscBuffer, PcfPoissonDiscBuffer,
PcssPoissonDiscBuffer,
} }
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
@ -74,6 +76,7 @@ pub struct ShadowMapsPass {
atlas: LightShadowMapAtlas, atlas: LightShadowMapAtlas,
/// The depth map atlas sampler /// The depth map atlas sampler
atlas_sampler: Rc<wgpu::Sampler>, atlas_sampler: Rc<wgpu::Sampler>,
atlas_sampler_compare: Rc<wgpu::Sampler>,
} }
impl ShadowMapsPass { impl ShadowMapsPass {
@ -98,7 +101,7 @@ impl ShadowMapsPass {
device, device,
wgpu::TextureFormat::Depth32Float, wgpu::TextureFormat::Depth32Float,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, 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 { let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -107,16 +110,28 @@ impl ShadowMapsPass {
contents: bytemuck::bytes_of(&atlas.atlas_size()), 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 { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("sampler_shadow_map_atlas"), label: Some("sampler_shadow_map_atlas"),
address_mode_u: wgpu::AddressMode::ClampToBorder, address_mode_u: wgpu::AddressMode::ClampToBorder,
address_mode_v: wgpu::AddressMode::ClampToBorder, address_mode_v: wgpu::AddressMode::ClampToBorder,
address_mode_w: wgpu::AddressMode::ClampToBorder, address_mode_w: wgpu::AddressMode::ClampToBorder,
mag_filter: wgpu::FilterMode::Linear, mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest,
border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite),
compare: Some(wgpu::CompareFunction::LessEqual),
..Default::default() ..Default::default()
}); });
@ -155,6 +170,7 @@ impl ShadowMapsPass {
point_light_pipeline: None, point_light_pipeline: None,
atlas_sampler: Rc::new(sampler), atlas_sampler: Rc::new(sampler),
atlas_sampler_compare: Rc::new(sampler_compare),
atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))),
} }
} }
@ -183,6 +199,14 @@ impl ShadowMapsPass {
let projection = let projection =
glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); 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( let look_view = glam::Mat4::look_to_rh(
light_pos.translation, light_pos.translation,
light_pos.forward(), light_pos.forward(),
@ -196,7 +220,8 @@ impl ShadowMapsPass {
atlas_frame, atlas_frame,
near_plane: NEAR_PLANE, near_plane: NEAR_PLANE,
far_plane, far_plane,
_padding1: [0; 2], light_size_uv,
_padding1: 0,
light_pos: light_pos.translation, light_pos: light_pos.translation,
_padding2: 0, _padding2: 0,
}; };
@ -284,7 +309,8 @@ impl ShadowMapsPass {
atlas_frame: frames[i], atlas_frame: frames[i],
near_plane: NEAR_PLANE, near_plane: NEAR_PLANE,
far_plane, far_plane,
_padding1: [0; 2], light_size_uv: 0.0,
_padding1: 0,
light_pos: light_trans, light_pos: light_trans,
_padding2: 0, _padding2: 0,
}, },
@ -322,26 +348,29 @@ impl ShadowMapsPass {
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, num_samples: u32) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor { device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label), 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, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false, mapped_at_creation: false,
}) })
} }
/// Generate and write a Poisson disc to `buffer` with `num_pcf_samples.pow(2)` amount of points. /// 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) { fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_samples: u32) {
let num_points = num_pcf_samples.pow(2); //let num_points = num_samples.pow(2);
let num_floats = num_points * 2; // points are vec2f 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_floats as f32).sqrt() / num_floats as f32;
//let min_dist = (num_samples as f32).sqrt() / num_samples as f32;
let mut points = vec![]; let mut points = vec![];
// use a while loop to ensure that the correct number of floats is created // use a while loop to ensure that the correct number of floats is created
while points.len() < num_floats as usize { while points.len() < num_floats as usize {
let poisson = Poisson2D::new() let poisson = Poisson2D::new()
.with_dimensions([1.0, 1.0], min_dist) .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 _); points.truncate(num_floats as _);
@ -377,6 +406,12 @@ impl Node for ShadowMapsPass {
Some(SlotValue::Sampler(self.atlas_sampler.clone())), 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( node.add_buffer_slot(
ShadowMapsPassSlots::ShadowLightUniformsBuffer, ShadowMapsPassSlots::ShadowLightUniformsBuffer,
SlotAttribute::Output, SlotAttribute::Output,
@ -404,11 +439,20 @@ impl Node for ShadowMapsPass {
Some(SlotValue::Buffer(Arc::new(settings_buffer))), Some(SlotValue::Buffer(Arc::new(settings_buffer))),
); );
let def_settings = ShadowSettings::default();
node.add_buffer_slot( node.add_buffer_slot(
ShadowMapsPassSlots::PcfPoissonDiscBuffer, ShadowMapsPassSlots::PcfPoissonDiscBuffer,
SlotAttribute::Output, SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new( 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 // TODO: Update the poisson disc every time the PCF sampling point number changed
if !world.has_resource::<ShadowSettings>() { if !world.has_resource::<ShadowSettings>() {
let def_settings = ShadowSettings::default();
let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
.unwrap().as_buffer().unwrap(); .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 // TODO: only write buffer on changes to resource
@ -717,7 +766,9 @@ pub struct LightShadowUniform {
atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed
near_plane: f32, near_plane: f32,
far_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, light_pos: glam::Vec3,
_padding2: u32, _padding2: u32,
} }
@ -758,13 +809,22 @@ impl LightShadowMapAtlas {
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct ShadowSettings { pub struct ShadowSettings {
/// How many PCF filtering samples are used per dimension.
///
/// A value of 16 is common.
pub pcf_samples_num: u32, 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 { impl Default for ShadowSettings {
fn default() -> Self { fn default() -> Self {
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)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct ShadowSettingsUniform { struct ShadowSettingsUniform {
pcf_samples_num: u32, pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
} }
impl From<ShadowSettings> for ShadowSettingsUniform { impl From<ShadowSettings> for ShadowSettingsUniform {
fn from(value: ShadowSettings) -> Self { fn from(value: ShadowSettings) -> Self {
Self { Self {
pcf_samples_num: value.pcf_samples_num, 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, atlas_frame: TextureAtlasFrame,
near_plane: f32, near_plane: f32,
far_plane: f32, far_plane: f32,
light_size_uv: f32,
light_pos: vec3<f32>, light_pos: vec3<f32>,
} }
struct ShadowSettingsUniform { struct ShadowSettingsUniform {
pcf_samples_num: u32, pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
} }
@group(4) @binding(0) @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) @group(5) @binding(0)
var t_shadow_maps_atlas: texture_depth_2d; var t_shadow_maps_atlas: texture_depth_2d;
@group(5) @binding(1) @group(5) @binding(1)
var s_shadow_maps_atlas: sampler_comparison; var s_shadow_maps_atlas: sampler;
@group(5) @binding(2) @group(5) @binding(2)
var<uniform> u_shadow_settings: ShadowSettingsUniform; var s_shadow_maps_atlas_compare: sampler_comparison;
@group(5) @binding(3) @group(5) @binding(3)
var<storage, read> u_light_shadow: array<LightShadowMapUniform>; var<uniform> u_shadow_settings: ShadowSettingsUniform;
@group(5) @binding(4) @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>>; 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 @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { 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); 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 /// 0 -> UNKNOWN
/// 1 -> right /// 1 -> right
/// 2 -> left /// 2 -> left
@ -194,7 +204,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
/// 4 -> bottom /// 4 -> bottom
/// 5 -> near /// 5 -> near
/// 6 -> far /// 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_x = abs(tex_coord.x);
let abs_y = abs(tex_coord.y); let abs_y = abs(tex_coord.y);
let abs_z = abs(tex_coord.z); 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 = (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.y = 1.0 - res.y;
//res.x = 1.0 - res.x;
return vec3<f32>(res, f32(cube_idx)); 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 // 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; 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 // dont cast shadows outside the light's far plane
if (proj_coords.z > 1.0) { 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; return shadow;
} }
/// Calculate the shadow coefficient using PCF of a directional light // Comes from https://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
fn pcf_dir_light(tex_coords: vec2<f32>, test_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { fn search_width(light_near: f32, uv_light_size: f32, receiver_depth: f32) -> f32 {
let half_filter_size = f32(u_shadow_settings.pcf_samples_num) / 2.0; return uv_light_size * (receiver_depth - light_near) / receiver_depth;
let texel_size = 1.0 / vec2<f32>(f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); }
// Sample PCF /// Convert texture coords to be texture coords of an atlas frame.
var shadow = 0.0; fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2<f32>) -> vec2<f32> {
var i = 0; let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
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) { // get the rect of the frame as a vec4
//let random = u_pcf_poisson_disc[i] * texel_size; 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));
let offset = tex_coords + (u_pcf_poisson_disc[i] + vec2<f32>(x, y)) * texel_size; // put the frame rect in atlas UV space
region_rect /= f32(atlas_dimensions.x);
let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth);
shadow += pcf_depth; // 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;
}
i++; /// 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 { 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; 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; var coords_2d = temp.xy;
let cube_idx = i32(temp.z); 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; var current_depth = length(frag_to_light) - bias;
current_depth /= u.far_plane; 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; return shadow;
} }