render: implement PCF for point lights, support per-light shadow settings

This commit is contained in:
SeanOMik 2024-07-21 12:02:35 -04:00
parent c91ee67961
commit fef709d5f1
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
5 changed files with 425 additions and 115 deletions

View File

@ -5,8 +5,11 @@ use lyra_engine::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
}, },
math::{self, Transform, Vec3}, math::{self, Quat, Transform, Vec3},
render::light::directional::DirectionalLight, render::{
graph::{ShadowCasterSettings, ShadowFilteringMode},
light::{directional::DirectionalLight, PointLight},
},
scene::{ scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, 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) { fn setup_scene_plugin(game: &mut Game) {
let world = game.world_mut(); let world = game.world_mut();
let resman = world.get_resource_mut::<ResourceManager>(); let resman = world.get_resource_mut::<ResourceManager>();
/* let camera_gltf = resman /* let camera_gltf = resman
.request::<Gltf>("../assets/AntiqueCamera.glb") .request::<Gltf>("../assets/AntiqueCamera.glb")
.unwrap(); .unwrap();
@ -189,19 +192,27 @@ fn setup_scene_plugin(game: &mut Game) {
light_tran, light_tran,
)); ));
/* world.spawn(( world.spawn((
//cube_mesh.clone(), cube_mesh.clone(),
PointLight { PointLight {
enabled: true, enabled: true,
color: Vec3::new(0.133, 0.098, 0.91), color: Vec3::new(0.133, 0.098, 0.91),
intensity: 2.0, intensity: 2.0,
range: 9.0, range: 10.0,
..Default::default() ..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(), //cube_mesh.clone(),
PointLight { PointLight {
enabled: true, enabled: true,
@ -216,8 +227,14 @@ 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.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_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())); world.spawn((camera, FreeFlyCamera::default()));
} }

View File

@ -135,6 +135,11 @@ impl Node for MeshPass {
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer")
.as_buffer() .as_buffer()
.unwrap(); .unwrap();
let pcf_poisson_disc_3d = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer3d")
.as_buffer()
.unwrap();
let pcss_poisson_disc = graph let pcss_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer") .expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer")
@ -206,6 +211,16 @@ impl Node for MeshPass {
}, },
count: None, 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 { wgpu::BindGroupEntry {
binding: 6, 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 { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcss_poisson_disc, buffer: pcss_poisson_disc,
offset: 0, offset: 0,

View File

@ -1,8 +1,11 @@
use std::{ 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 glam::Vec2;
use itertools::Itertools; use itertools::Itertools;
use lyra_ecs::{ use lyra_ecs::{
@ -26,7 +29,7 @@ use crate::render::{
use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; 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)] #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub enum ShadowMapsPassSlots { pub enum ShadowMapsPassSlots {
@ -38,6 +41,7 @@ pub enum ShadowMapsPassSlots {
ShadowLightUniformsBuffer, ShadowLightUniformsBuffer,
ShadowSettingsUniform, ShadowSettingsUniform,
PcfPoissonDiscBuffer, PcfPoissonDiscBuffer,
PcfPoissonDiscBuffer3d,
PcssPoissonDiscBuffer, PcssPoissonDiscBuffer,
} }
@ -101,7 +105,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 * 2, SHADOW_SIZE * 8,
); );
let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -182,31 +186,57 @@ impl ShadowMapsPass {
light_type: LightType, light_type: LightType,
entity: Entity, entity: Entity,
light_pos: Transform, light_pos: Transform,
far_plane: f32, are_settings_custom: bool,
shadow_settings: ShadowCasterSettings,
) -> LightDepthMap { ) -> LightDepthMap {
const NEAR_PLANE: f32 = 0.1;
const FAR_PLANE: f32 = 45.0;
let mut atlas = self.atlas.get_mut(); 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 { let (start_atlas_idx, uniform_indices) = match light_type {
LightType::Directional => { LightType::Directional => {
let directional_size = SHADOW_SIZE * 4;
// directional lights require a single map, so allocate that in the atlas. // directional lights require a single map, so allocate that in the atlas.
let atlas_index = 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"); .expect("failed to pack new shadow map into texture atlas");
let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing");
let projection = let projection = glam::Mat4::orthographic_rh(
glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); -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 // 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 // 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); 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? // maybe its better to make this a vec2 on the gpu?
let size_avg = (frustum_size.x + frustum_size.y) / 2.0; let size_avg = (frustum_size.x + frustum_size.y) / 2.0;
let light_size_uv = 0.2 * size_avg; 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(),
@ -218,12 +248,15 @@ impl ShadowMapsPass {
let u = LightShadowUniform { let u = LightShadowUniform {
space_mat: light_proj, space_mat: light_proj,
atlas_frame, atlas_frame,
near_plane: NEAR_PLANE, near_plane: shadow_settings.near_plane,
far_plane, far_plane: shadow_settings.far_plane,
light_size_uv, light_size_uv,
_padding1: 0, _padding1: 0,
light_pos: light_pos.translation, 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); let uniform_index = self.light_uniforms_buffer.insert(queue, &u);
@ -237,8 +270,8 @@ impl ShadowMapsPass {
let projection = glam::Mat4::perspective_rh( let projection = glam::Mat4::perspective_rh(
Angle::Degrees(90.0).to_radians(), Angle::Degrees(90.0).to_radians(),
aspect, aspect,
NEAR_PLANE, shadow_settings.near_plane,
far_plane, shadow_settings.far_plane,
); );
let light_trans = light_pos.translation; let light_trans = light_pos.translation;
@ -307,12 +340,15 @@ impl ShadowMapsPass {
&LightShadowUniform { &LightShadowUniform {
space_mat: views[i], space_mat: views[i],
atlas_frame: frames[i], atlas_frame: frames[i],
near_plane: NEAR_PLANE, near_plane: shadow_settings.near_plane,
far_plane, far_plane: shadow_settings.far_plane,
light_size_uv: 0.0, light_size_uv: 0.0,
_padding1: 0, _padding1: 0,
light_pos: light_trans, 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; indices[i] = uniform_i;
@ -345,34 +381,69 @@ impl ShadowMapsPass {
} }
/// Create the gpu buffer for a poisson disc /// 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 { device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label), label: Some(label),
size: mem::size_of::<glam::Vec2>() as u64 * (num_samples * 2) as u64, size: mem::size_of::<glam::Vec2>() as u64 * (num_samples * dimension) 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_samples: u32) { fn write_poisson_disc(
let num_floats = num_samples * 2; // points are vec2f &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 min_dist = (num_floats as f32).sqrt() / num_floats 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() if dimension == 2 {
.with_dimensions([1.0, 1.0], min_dist) let poisson = Poisson2D::new()
.with_samples(num_samples); .with_dimensions([1.0, 1.0], min_dist)
.with_samples(num_samples);
points = poisson.iter().flatten() points = poisson
.map(|p| p * 2.0 - 1.0) .iter()
.collect_vec(); .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 _); points.truncate(num_floats as _);
queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice())); queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice()));
} }
} }
@ -441,7 +512,25 @@ impl Node for ShadowMapsPass {
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_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, ShadowMapsPassSlots::PcssPoissonDiscBuffer,
SlotAttribute::Output, SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new( 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, world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext, context: &mut crate::render::graph::RenderGraphContext,
) { ) {
world.add_resource_default_if_absent::<ShadowSettings>(); world.add_resource_default_if_absent::<ShadowCasterSettings>();
if world.has_resource_changed::<ShadowSettings>() { if world.has_resource_changed::<ShadowCasterSettings>() {
debug!("Detected change in ShadowSettings, recreating poisson disks"); debug!("Detected change in ShadowSettings, recreating poisson disks");
let settings = world.get_resource::<ShadowSettings>(); let settings = world.get_resource::<ShadowCasterSettings>();
// convert to uniform now since the from impl limits to max values // convert to uniform now since the from impl limits to max values
let uniform = ShadowSettingsUniform::from(*settings); let uniform = ShadowSettingsUniform::from(*settings);
let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) let buffer = graph
.unwrap().as_buffer().unwrap(); .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
self.write_poisson_disc(&context.queue, &buffer, uniform.pcf_samples_num); .unwrap()
.as_buffer()
.unwrap();
self.write_poisson_disc(&context.queue, &buffer, 2, uniform.pcf_samples_num);
let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) let buffer = graph
.unwrap().as_buffer().unwrap(); .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
self.write_poisson_disc(&context.queue, &buffer, uniform.pcss_blocker_search_samples); .unwrap()
.as_buffer()
.unwrap();
self.write_poisson_disc(&context.queue, &buffer, 3, uniform.pcf_samples_num);
context.queue_buffer_write_with( let buffer = graph
ShadowMapsPassSlots::ShadowSettingsUniform, .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
0, .unwrap()
uniform, .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::<ShadowCasterSettings>();
self.render_meshes = world.try_get_resource_data::<RenderMeshes>(); self.render_meshes = world.try_get_resource_data::<RenderMeshes>();
self.transform_buffers = world.try_get_resource_data::<TransformBuffers>(); self.transform_buffers = world.try_get_resource_data::<TransformBuffers>();
@ -494,23 +603,48 @@ impl Node for ShadowMapsPass {
// use a queue for storing atlas ids to add to entities after the entities are iterated // use a queue for storing atlas ids to add to entities after the entities are iterated
let mut index_components_queue = VecDeque::new(); let mut index_components_queue = VecDeque::new();
for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has<DirectionalLight>)>() { for (entity, pos, shadow_settings, _) in world.view_iter::<(
Entities,
&Transform,
Option<&ShadowCasterSettings>,
Has<DirectionalLight>,
)>() {
if !self.depth_maps.contains_key(&entity) { 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( let atlas_index = self.create_depth_map(
&context.queue, &context.queue,
LightType::Directional, LightType::Directional,
entity, entity,
*pos, *pos,
45.0, custom_settings,
shadow_settings,
); );
index_components_queue.push_back((entity, atlas_index)); index_components_queue.push_back((entity, atlas_index));
} }
} }
for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has<PointLight>)>() { for (entity, pos, shadow_settings, _) in world.view_iter::<(
Entities,
&Transform,
Option<&ShadowCasterSettings>,
Has<PointLight>,
)>() {
if !self.depth_maps.contains_key(&entity) { if !self.depth_maps.contains_key(&entity) {
let atlas_index = let (custom_settings, shadow_settings) = shadow_settings
self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0); .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)); 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)] #[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightShadowUniform { pub struct LightShadowUniform {
@ -747,7 +913,11 @@ pub struct LightShadowUniform {
light_size_uv: f32, light_size_uv: f32,
_padding1: u32, _padding1: u32,
light_pos: glam::Vec3, 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. /// 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, Pcss,
} }
#[derive(Debug, Copy, Clone)] /* #[derive(Debug, Copy, Clone)]
pub struct ShadowSettings { pub struct ShadowSettings {
pub filtering_mode: ShadowFilteringMode, pub filtering_mode: ShadowFilteringMode,
/// How many PCF filtering samples are used per dimension. /// How many PCF filtering samples are used per dimension.
/// ///
/// A value of 25 is common, this is maxed to 128. /// A value of 25 is common, this is maxed to 128.
pub pcf_samples_num: u32, pub pcf_samples_num: u32,
/// How many samples are used for the PCSS blocker search step. /// 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. /// 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. /// A value of 25 is common, this is maxed to 128.
pub pcss_blocker_search_samples: u32, pub pcss_blocker_search_samples: u32,
@ -816,13 +986,13 @@ impl Default for ShadowSettings {
pcss_blocker_search_samples: 25, pcss_blocker_search_samples: 25,
} }
} }
} } */
const PCF_SAMPLES_NUM_MAX: u32 = 128; const PCF_SAMPLES_NUM_MAX: u32 = 128;
const PCSS_SAMPLES_NUM_MAX: u32 = 128; const PCSS_SAMPLES_NUM_MAX: u32 = 128;
/// Uniform version of [`ShadowSettings`]. /// 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 zero, PCF and PCSS will be disabled.
/// If `pcf_samples_num` is set to 2, ONLY hardware 2x2 PCF will be used. /// 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. /// If `pcss_blocker_search_samples` is set to zero, PCSS will be disabled.
@ -834,12 +1004,27 @@ struct ShadowSettingsUniform {
pcss_blocker_search_samples: u32, pcss_blocker_search_samples: u32,
} }
impl From<ShadowSettings> for ShadowSettingsUniform { impl From<ShadowCasterSettings> for ShadowSettingsUniform {
fn from(value: ShadowSettings) -> Self { fn from(value: ShadowCasterSettings) -> Self {
let raw_pcf_samples = value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); Self::new(
let raw_pcss_samples = value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); 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::None => (0, 0),
ShadowFilteringMode::Pcf2x2 => (2, 0), ShadowFilteringMode::Pcf2x2 => (2, 0),
ShadowFilteringMode::Pcf => (raw_pcf_samples, 0), ShadowFilteringMode::Pcf => (raw_pcf_samples, 0),

View File

@ -119,6 +119,10 @@ struct LightShadowMapUniform {
far_plane: f32, far_plane: f32,
light_size_uv: f32, light_size_uv: f32,
light_pos: vec3<f32>, light_pos: vec3<f32>,
/// boolean casted as u32
has_shadow_settings: u32,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
} }
struct ShadowSettingsUniform { struct ShadowSettingsUniform {
@ -144,6 +148,8 @@ var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
@group(5) @binding(5) @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) @group(5) @binding(6)
var<storage, read> u_pcf_poisson_disc_3d: array<vec3<f32>>;
@group(5) @binding(7)
var<storage, read> u_pcss_poisson_disc: array<vec2<f32>>; var<storage, read> u_pcss_poisson_disc: array<vec2<f32>>;
@fragment @fragment
@ -180,7 +186,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u); 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); 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) { } 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); 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) { } 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); 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<f32>) -> vec3<f32> {
return vec3<f32>(res, f32(cube_idx)); return vec3<f32>(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<u32> {
if shadow_u.has_shadow_settings == 1u {
return vec2<u32>(shadow_u.pcf_samples_num, shadow_u.pcss_blocker_search_samples);
} else {
return vec2<u32>(u_shadow_settings.pcf_samples_num, u_shadow_settings.pcss_blocker_search_samples);
}
}
fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>, atlas_dimensions: vec2<i32>, shadow_u: LightShadowMapUniform) -> f32 { fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>, atlas_dimensions: vec2<i32>, shadow_u: LightShadowMapUniform) -> f32 {
var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w;
// for some reason the y component is flipped after transforming // for some reason the y component is flipped after transforming
@ -261,21 +277,28 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light
let bias = 0.005;//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;
// 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; 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); shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u);
} }
// hardware 2x2 PCF via camparison sampler // only PCF
else if u_shadow_settings.pcf_samples_num == 2u { else if pcf_samples_num > 0u {
let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); let texel_size = 1.0 / f32(shadow_u.atlas_frame.width);
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)
shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size); 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 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); 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); shadow = select(1.0, 0.0, current_depth > closest_depth);
@ -304,14 +327,19 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2<f32>) ->
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
// get the rect of the frame as a vec4 // 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)); 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 // put the frame rect in atlas UV space
region_rect /= f32(atlas_dimensions.x); 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 // lerp input coords
let region_coords = vec2<f32>( let region_coords = vec2<f32>(
mix(region_rect.x, region_rect.x + region_rect.z, coords.x), mix(region_rect.x + texel_size, region_rect.x + region_rect.z - texel_size, coords.x),
mix(region_rect.y, region_rect.y + region_rect.w, coords.y) mix(region_rect.y + texel_size, region_rect.y + region_rect.w - texel_size, coords.y)
); );
return region_coords; return region_coords;
@ -372,45 +400,96 @@ fn pcf_dir_light(tex_coords: vec2<f32>, test_depth: f32, shadow_u: LightShadowMa
return saturate(shadow); 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_light(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 = coords_to_cube_atlas(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);
/// if an unknown cube side was returned, something is broken
/*if cube_idx == 0 {
return 0.0;
}*/
var indices = light.light_shadow_uniform_index; var indices = light.light_shadow_uniform_index;
let i = indices[cube_idx - 1]; let i = indices[cube_idx - 1];
let u: LightShadowMapUniform = u_light_shadow[i]; let u: LightShadowMapUniform = u_light_shadow[i];
let uniforms = array<LightShadowMapUniform, 6>(
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 var current_depth = length(frag_to_light);
// z is width, w is height
var region_coords = vec4<f32>(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;
current_depth /= u.far_plane; 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; return shadow;
} }
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_point_light(tex_coords: vec3<f32>, test_depth: f32, shadow_us: array<LightShadowMapUniform, 6>, 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<f32>, 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<f32> { fn debug_grid(in: VertexOutput) -> vec4<f32> {
let tile_index_float: vec2<f32> = in.clip_position.xy / 16.0; let tile_index_float: vec2<f32> = in.clip_position.xy / 16.0;
let tile_index = vec2<u32>(floor(tile_index_float)); let tile_index = vec2<u32>(floor(tile_index_float));
@ -485,7 +564,8 @@ fn blinn_phong_point_light(world_pos: vec3<f32>, world_norm: vec3<f32>, point_li
diffuse_color *= attenuation; diffuse_color *= attenuation;
specular_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<f32>, world_norm: vec3<f32>, spot_light: Light, material: Material, specular_factor: vec3<f32>) -> vec3<f32> { fn blinn_phong_spot_light(world_pos: vec3<f32>, world_norm: vec3<f32>, spot_light: Light, material: Material, specular_factor: vec3<f32>) -> vec3<f32> {

View File

@ -13,7 +13,12 @@ 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>,
/// boolean casted as u32
has_shadow_settings: u32,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
} }
@group(0) @binding(0) @group(0) @binding(0)