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,
InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput,
},
math::{self, Transform, Vec3},
render::light::directional::DirectionalLight,
math::{self, Quat, Transform, Vec3},
render::{
graph::{ShadowCasterSettings, ShadowFilteringMode},
light::{directional::DirectionalLight, PointLight},
},
scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
@ -189,19 +192,27 @@ fn setup_scene_plugin(game: &mut Game) {
light_tran,
));
/* world.spawn((
//cube_mesh.clone(),
world.spawn((
cube_mesh.clone(),
PointLight {
enabled: true,
color: Vec3::new(0.133, 0.098, 0.91),
intensity: 2.0,
range: 9.0,
range: 10.0,
..Default::default()
},
Transform::from_xyz(5.0, -2.5, -3.3),
ShadowCasterSettings {
filtering_mode: ShadowFilteringMode::Pcf,
..Default::default()
},
Transform::new(
Vec3::new(4.0 - 1.43, -13.0, 1.53),
Quat::IDENTITY,
Vec3::new(0.5, 0.5, 0.5),
),
));
world.spawn((
/* world.spawn((
//cube_mesh.clone(),
PointLight {
enabled: true,
@ -216,8 +227,14 @@ fn setup_scene_plugin(game: &mut Game) {
let mut camera = CameraComponent::new_3d();
//camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5);
camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0);
/* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0);
camera.transform.rotate_x(math::Angle::Degrees(-27.0));
camera.transform.rotate_y(math::Angle::Degrees(-55.0));
camera.transform.rotate_y(math::Angle::Degrees(-55.0)); */
camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0);
camera.transform.rotate_x(math::Angle::Degrees(-27.0));
//camera.transform.rotate_y(math::Angle::Degrees(-90.0));
camera.transform.rotate_y(math::Angle::Degrees(90.0));
world.spawn((camera, FreeFlyCamera::default()));
}

View File

@ -135,6 +135,11 @@ impl Node for MeshPass {
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer")
.as_buffer()
.unwrap();
let pcf_poisson_disc_3d = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer3d")
.as_buffer()
.unwrap();
let pcss_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer")
@ -206,6 +211,16 @@ impl Node for MeshPass {
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
@ -251,6 +266,14 @@ impl Node for MeshPass {
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc_3d,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcss_poisson_disc,
offset: 0,

View File

@ -1,8 +1,11 @@
use std::{
collections::VecDeque, mem, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}
collections::VecDeque,
mem,
rc::Rc,
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use fast_poisson::Poisson2D;
use fast_poisson::{Poisson2D, Poisson3D};
use glam::Vec2;
use itertools::Itertools;
use lyra_ecs::{
@ -26,7 +29,7 @@ use crate::render::{
use super::{MeshBufferStorage, RenderAssets, RenderMeshes};
const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096);
const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024);
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub enum ShadowMapsPassSlots {
@ -38,6 +41,7 @@ pub enum ShadowMapsPassSlots {
ShadowLightUniformsBuffer,
ShadowSettingsUniform,
PcfPoissonDiscBuffer,
PcfPoissonDiscBuffer3d,
PcssPoissonDiscBuffer,
}
@ -101,7 +105,7 @@ impl ShadowMapsPass {
device,
wgpu::TextureFormat::Depth32Float,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
SHADOW_SIZE * 2,
SHADOW_SIZE * 8,
);
let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -182,23 +186,49 @@ impl ShadowMapsPass {
light_type: LightType,
entity: Entity,
light_pos: Transform,
far_plane: f32,
are_settings_custom: bool,
shadow_settings: ShadowCasterSettings,
) -> LightDepthMap {
const NEAR_PLANE: f32 = 0.1;
const FAR_PLANE: f32 = 45.0;
let mut atlas = self.atlas.get_mut();
let u = ShadowSettingsUniform::new(
shadow_settings.filtering_mode,
shadow_settings.pcf_samples_num,
shadow_settings.pcss_blocker_search_samples,
);
let has_shadow_settings = if are_settings_custom {
1
} else { 0 };
/* let (has_shadow_settings, pcf_samples_num, pcss_samples_num) = if are_settings_custom {
(1, u.pcf_samples_num, u.pcss_blocker_search_samples)
} else {
(0, , 0)
}; */
/* shadow_settings.map(|ss| {
let u = ShadowSettingsUniform::new(ss.filtering_mode, ss.pcf_samples_num, ss.pcss_blocker_search_samples);
(1, u.pcf_samples_num, u.pcss_blocker_search_samples)
}).unwrap_or((0, 0, 0)); */
let (start_atlas_idx, uniform_indices) = match light_type {
LightType::Directional => {
let directional_size = SHADOW_SIZE * 4;
// directional lights require a single map, so allocate that in the atlas.
let atlas_index = atlas
.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _)
.pack(directional_size.x as _, directional_size.y as _)
.expect("failed to pack new shadow map into texture atlas");
let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing");
let projection =
glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE);
let projection = glam::Mat4::orthographic_rh(
-10.0,
10.0,
-10.0,
10.0,
shadow_settings.near_plane,
shadow_settings.far_plane,
);
// honestly no clue why this works, but I got it from here and the results are good
// https://github.com/asylum2010/Asylum_Tutorials/blob/423e5edfaee7b5ea450a450e65f2eabf641b2482/ShaderTutors/43_ShadowMapFiltering/main.cpp#L323
@ -218,12 +248,15 @@ impl ShadowMapsPass {
let u = LightShadowUniform {
space_mat: light_proj,
atlas_frame,
near_plane: NEAR_PLANE,
far_plane,
near_plane: shadow_settings.near_plane,
far_plane: shadow_settings.far_plane,
light_size_uv,
_padding1: 0,
light_pos: light_pos.translation,
_padding2: 0,
has_shadow_settings,
pcf_samples_num: u.pcf_samples_num,
pcss_blocker_search_samples: u.pcss_blocker_search_samples,
_padding2: [0; 2],
};
let uniform_index = self.light_uniforms_buffer.insert(queue, &u);
@ -237,8 +270,8 @@ impl ShadowMapsPass {
let projection = glam::Mat4::perspective_rh(
Angle::Degrees(90.0).to_radians(),
aspect,
NEAR_PLANE,
far_plane,
shadow_settings.near_plane,
shadow_settings.far_plane,
);
let light_trans = light_pos.translation;
@ -307,12 +340,15 @@ impl ShadowMapsPass {
&LightShadowUniform {
space_mat: views[i],
atlas_frame: frames[i],
near_plane: NEAR_PLANE,
far_plane,
near_plane: shadow_settings.near_plane,
far_plane: shadow_settings.far_plane,
light_size_uv: 0.0,
_padding1: 0,
light_pos: light_trans,
_padding2: 0,
has_shadow_settings,
pcf_samples_num: u.pcf_samples_num,
pcss_blocker_search_samples: u.pcss_blocker_search_samples,
_padding2: [0; 2],
},
);
indices[i] = uniform_i;
@ -345,31 +381,66 @@ impl ShadowMapsPass {
}
/// Create the gpu buffer for a poisson disc
fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer {
fn create_poisson_disc_buffer(
&self,
device: &wgpu::Device,
label: &str,
dimension: u32,
num_samples: u32,
) -> wgpu::Buffer {
debug_assert!(
dimension == 2 || dimension == 3,
"unknown dimension {dimension}, expected 2 (2d) or 3 (3d)"
);
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: mem::size_of::<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,
mapped_at_creation: false,
})
}
/// Generate and write a Poisson disc to `buffer` with `num_pcf_samples.pow(2)` amount of points.
fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_samples: u32) {
let num_floats = num_samples * 2; // points are vec2f
fn write_poisson_disc(
&self,
queue: &wgpu::Queue,
buffer: &wgpu::Buffer,
dimension: u32,
num_samples: u32,
) {
debug_assert!(
dimension == 2 || dimension == 3,
"unknown dimension {dimension}, expected 2 (2d) or 3 (3d)"
);
let num_floats = num_samples * dimension; // points are vec2f
let min_dist = (num_floats as f32).sqrt() / num_floats as f32;
let mut points = vec![];
// use a while loop to ensure that the correct number of floats is created
while points.len() < num_floats as usize {
let poisson = Poisson2D::new()
.with_dimensions([1.0, 1.0], min_dist)
.with_samples(num_samples);
if dimension == 2 {
let poisson = Poisson2D::new()
.with_dimensions([1.0, 1.0], min_dist)
.with_samples(num_samples);
points = poisson.iter().flatten()
.map(|p| p * 2.0 - 1.0)
.collect_vec();
points = poisson
.iter()
.flatten()
.map(|p| p * 2.0 - 1.0)
.collect_vec();
} else if dimension == 3 {
let poisson = Poisson3D::new()
.with_dimensions([1.0, 1.0, 1.0], min_dist)
.with_samples(num_samples);
points = poisson
.iter()
.flatten()
.map(|p| p * 2.0 - 1.0)
.collect_vec();
}
}
points.truncate(num_floats as _);
@ -441,7 +512,25 @@ impl Node for ShadowMapsPass {
ShadowMapsPassSlots::PcfPoissonDiscBuffer,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(
self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM_MAX),
self.create_poisson_disc_buffer(
device,
"buffer_poisson_disc_pcf",
2,
PCF_SAMPLES_NUM_MAX,
),
))),
);
node.add_buffer_slot(
ShadowMapsPassSlots::PcfPoissonDiscBuffer3d,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(
self.create_poisson_disc_buffer(
device,
"buffer_poisson_disc_pcf_3d",
3,
PCF_SAMPLES_NUM_MAX,
),
))),
);
@ -449,7 +538,12 @@ impl Node for ShadowMapsPass {
ShadowMapsPassSlots::PcssPoissonDiscBuffer,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(
self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", PCSS_SAMPLES_NUM_MAX),
self.create_poisson_disc_buffer(
device,
"buffer_poisson_disc_pcss",
2,
PCSS_SAMPLES_NUM_MAX,
),
))),
);
@ -462,28 +556,43 @@ impl Node for ShadowMapsPass {
world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext,
) {
world.add_resource_default_if_absent::<ShadowSettings>();
if world.has_resource_changed::<ShadowSettings>() {
world.add_resource_default_if_absent::<ShadowCasterSettings>();
if world.has_resource_changed::<ShadowCasterSettings>() {
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
let uniform = ShadowSettingsUniform::from(*settings);
let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
.unwrap().as_buffer().unwrap();
self.write_poisson_disc(&context.queue, &buffer, uniform.pcf_samples_num);
let buffer = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
.unwrap()
.as_buffer()
.unwrap();
self.write_poisson_disc(&context.queue, &buffer, 2, uniform.pcf_samples_num);
let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.unwrap().as_buffer().unwrap();
self.write_poisson_disc(&context.queue, &buffer, uniform.pcss_blocker_search_samples);
let buffer = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
.unwrap()
.as_buffer()
.unwrap();
self.write_poisson_disc(&context.queue, &buffer, 3, uniform.pcf_samples_num);
context.queue_buffer_write_with(
ShadowMapsPassSlots::ShadowSettingsUniform,
0,
uniform,
let buffer = graph
.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.unwrap()
.as_buffer()
.unwrap();
self.write_poisson_disc(
&context.queue,
&buffer,
2,
uniform.pcss_blocker_search_samples,
);
context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, uniform);
}
let settings = *world.get_resource::<ShadowCasterSettings>();
self.render_meshes = world.try_get_resource_data::<RenderMeshes>();
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
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) {
let (custom_settings, shadow_settings) = shadow_settings
.map(|ss| (true, ss.clone()))
.unwrap_or((false, settings));
let atlas_index = self.create_depth_map(
&context.queue,
LightType::Directional,
entity,
*pos,
45.0,
custom_settings,
shadow_settings,
);
index_components_queue.push_back((entity, atlas_index));
}
}
for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has<PointLight>)>() {
for (entity, pos, shadow_settings, _) in world.view_iter::<(
Entities,
&Transform,
Option<&ShadowCasterSettings>,
Has<PointLight>,
)>() {
if !self.depth_maps.contains_key(&entity) {
let atlas_index =
self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0);
let (custom_settings, shadow_settings) = shadow_settings
.map(|ss| (true, ss.clone()))
.unwrap_or((false, settings));
let atlas_index = self.create_depth_map(
&context.queue,
LightType::Point,
entity,
*pos,
custom_settings,
shadow_settings,
);
index_components_queue.push_back((entity, atlas_index));
}
}
@ -736,6 +870,38 @@ fn light_shadow_pass_impl<'a>(
}
}
/// Shadow casting settings for a light caster.
///
/// Put this on an entity with a light source to override the global shadow
/// settings, the [`ShadowSettings`] resource.
#[derive(Debug, Copy, Clone, Component)]
pub struct ShadowCasterSettings {
pub filtering_mode: ShadowFilteringMode,
/// How many PCF filtering samples are used per dimension.
///
/// A value of 25 is common, this is maxed to 128.
pub pcf_samples_num: u32,
/// How many samples are used for the PCSS blocker search step.
///
/// Multiple samples are required to avoid holes int he penumbra due to missing blockers.
/// A value of 25 is common, this is maxed to 128.
pub pcss_blocker_search_samples: u32,
pub near_plane: f32,
pub far_plane: f32,
}
impl Default for ShadowCasterSettings {
fn default() -> Self {
Self {
filtering_mode: ShadowFilteringMode::default(),
pcf_samples_num: 25,
pcss_blocker_search_samples: 25,
near_plane: 0.1,
far_plane: 45.0,
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightShadowUniform {
@ -747,7 +913,11 @@ pub struct LightShadowUniform {
light_size_uv: f32,
_padding1: u32,
light_pos: glam::Vec3,
_padding2: u32,
/// Boolean casted as integer
has_shadow_settings: u32,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
_padding2: [u32; 2],
}
/// A component that stores the ID of a shadow map in the shadow map atlas for the entities.
@ -794,7 +964,7 @@ pub enum ShadowFilteringMode {
Pcss,
}
#[derive(Debug, Copy, Clone)]
/* #[derive(Debug, Copy, Clone)]
pub struct ShadowSettings {
pub filtering_mode: ShadowFilteringMode,
/// How many PCF filtering samples are used per dimension.
@ -816,7 +986,7 @@ impl Default for ShadowSettings {
pcss_blocker_search_samples: 25,
}
}
}
} */
const PCF_SAMPLES_NUM_MAX: u32 = 128;
const PCSS_SAMPLES_NUM_MAX: u32 = 128;
@ -834,12 +1004,27 @@ struct ShadowSettingsUniform {
pcss_blocker_search_samples: u32,
}
impl From<ShadowSettings> for ShadowSettingsUniform {
fn from(value: ShadowSettings) -> Self {
let raw_pcf_samples = value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX);
let raw_pcss_samples = value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX);
impl From<ShadowCasterSettings> for ShadowSettingsUniform {
fn from(value: ShadowCasterSettings) -> Self {
Self::new(
value.filtering_mode,
value.pcf_samples_num,
value.pcss_blocker_search_samples,
)
}
}
let (pcf_samples, pcss_samples) = match value.filtering_mode {
impl ShadowSettingsUniform {
/// Create a new shadow settings uniform.
pub fn new(
filter_mode: ShadowFilteringMode,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
) -> Self {
let raw_pcf_samples = pcf_samples_num.min(PCF_SAMPLES_NUM_MAX);
let raw_pcss_samples = pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX);
let (pcf_samples, pcss_samples) = match filter_mode {
ShadowFilteringMode::None => (0, 0),
ShadowFilteringMode::Pcf2x2 => (2, 0),
ShadowFilteringMode::Pcf => (raw_pcf_samples, 0),

View File

@ -119,6 +119,10 @@ struct LightShadowMapUniform {
far_plane: f32,
light_size_uv: f32,
light_pos: vec3<f32>,
/// boolean casted as u32
has_shadow_settings: u32,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
}
struct ShadowSettingsUniform {
@ -144,6 +148,8 @@ 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_pcf_poisson_disc_3d: array<vec3<f32>>;
@group(5) @binding(7)
var<storage, read> u_pcss_poisson_disc: array<vec2<f32>>;
@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);
light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow);
} else if (light.light_ty == LIGHT_TY_POINT) {
let shadow = calc_shadow_point(in.world_position, in.world_normal, light_dir, light, atlas_dimensions);
let shadow = calc_shadow_point_light(in.world_position, in.world_normal, light_dir, light, atlas_dimensions);
light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow);
} else if (light.light_ty == LIGHT_TY_SPOT) {
light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color);
@ -249,6 +255,16 @@ fn coords_to_cube_atlas(tex_coord: vec3<f32>) -> vec3<f32> {
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 {
var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w;
// for some reason the y component is flipped after transforming
@ -261,21 +277,28 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light
let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005);
let current_depth = proj_coords.z - bias;
// get settings
let settings = get_shadow_settings(shadow_u);
let pcf_samples_num = settings.x;
let pcss_blocker_search_samples = settings.y;
var shadow = 0.0;
if u_shadow_settings.pcf_samples_num > 0u && u_shadow_settings.pcss_blocker_search_samples > 0u {
// hardware 2x2 PCF via camparison sampler
if pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped);
shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
}
// PCSS
else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u {
shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u);
}
// hardware 2x2 PCF via camparison sampler
else if u_shadow_settings.pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped);
shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
} else if u_shadow_settings.pcf_samples_num > 0u {
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
// TODO: should texel size be using the entire atlas dimensions, or just the frame?
let texel_size = 1.0 / f32(atlas_dimensions.x); // f32(shadow_u.atlas_frame.width)
// only PCF
else if pcf_samples_num > 0u {
let texel_size = 1.0 / f32(shadow_u.atlas_frame.width);
shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size);
} else { // pcf_samples_num == 0
}
// no filtering
else {
let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0);
shadow = select(1.0, 0.0, current_depth > closest_depth);
@ -304,14 +327,19 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2<f32>) ->
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
// 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
region_rect /= f32(atlas_dimensions.x);
// calculate a relatively tiny offset to avoid getting the end of the frame and causing
// linear or nearest filtering to bleed to the adjacent frame.
let texel_size = (1.0 / f32(shadow_u.atlas_frame.x)) * 4.0;
// lerp input coords
let region_coords = vec2<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)
mix(region_rect.x + texel_size, region_rect.x + region_rect.z - texel_size, coords.x),
mix(region_rect.y + texel_size, region_rect.y + region_rect.w - texel_size, coords.y)
);
return region_coords;
@ -372,45 +400,96 @@ fn pcf_dir_light(tex_coords: vec2<f32>, test_depth: f32, shadow_u: LightShadowMa
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;
let temp = coords_to_cube_atlas(normalize(frag_to_light));
var coords_2d = temp.xy;
let cube_idx = i32(temp.z);
/// if an unknown cube side was returned, something is broken
/*if cube_idx == 0 {
return 0.0;
}*/
var indices = light.light_shadow_uniform_index;
let i = indices[cube_idx - 1];
let u: LightShadowMapUniform = u_light_shadow[i];
// get the atlas frame in [0; 1] in the atlas texture
// 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);
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]]
);
// simulate `ClampToBorder`, not creating shadows past the shadow map regions
/*if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) {
return 0.0;
}*/
// get the coords inside of the region
coords_2d.x = mix(region_coords.x, region_coords.x + region_coords.z, coords_2d.x);
coords_2d.y = mix(region_coords.y, region_coords.y + region_coords.w, coords_2d.y);
// use a bias to avoid shadow acne
let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005);
var current_depth = length(frag_to_light) - bias;
var current_depth = length(frag_to_light);
current_depth /= u.far_plane;
current_depth -= 0.005; // TODO: find a better way to calculate bias
var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, coords_2d, current_depth);
// get settings
let settings = get_shadow_settings(u);
let pcf_samples_num = settings.x;
let pcss_blocker_search_samples = settings.y;
var shadow = 0.0;
// hardware 2x2 PCF via camparison sampler
if pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(u, coords_2d);
shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
}
// PCSS
else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u {
shadow = pcss_dir_light(coords_2d, current_depth, u);
}
// only PCF
else if pcf_samples_num > 0u {
let texel_size = 1.0 / f32(u.atlas_frame.width);
shadow = pcf_point_light(frag_to_light, current_depth, uniforms, pcf_samples_num, 0.007);
//shadow = pcf_point_light(coords_2d, current_depth, u, pcf_samples_num, texel_size);
}
// no filtering
else {
let region_coords = to_atlas_frame_coords(u, coords_2d);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0);
shadow = select(1.0, 0.0, current_depth > closest_depth);
}
return shadow;
}
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_point_light(tex_coords: vec3<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> {
let tile_index_float: vec2<f32> = in.clip_position.xy / 16.0;
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;
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> {

View File

@ -13,7 +13,12 @@ struct LightShadowMapUniform {
atlas_frame: TextureAtlasFrame,
near_plane: f32,
far_plane: f32,
light_size_uv: 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)