Implement Shadows #24
|
@ -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,
|
||||
|
@ -99,7 +102,7 @@ async fn main() {
|
|||
fn setup_scene_plugin(game: &mut Game) {
|
||||
let world = game.world_mut();
|
||||
let resman = world.get_resource_mut::<ResourceManager>();
|
||||
|
||||
|
||||
/* let camera_gltf = resman
|
||||
.request::<Gltf>("../assets/AntiqueCamera.glb")
|
||||
.unwrap();
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,31 +186,57 @@ 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
|
||||
let frustum_size = Vec2::new(0.5 * projection.col(0).x, 0.5 * projection.col(1).y);
|
||||
// maybe its better to make this a vec2 on the gpu?
|
||||
let size_avg = (frustum_size.x + frustum_size.y) / 2.0;
|
||||
let light_size_uv = 0.2 * size_avg;
|
||||
|
||||
|
||||
let look_view = glam::Mat4::look_to_rh(
|
||||
light_pos.translation,
|
||||
light_pos.forward(),
|
||||
|
@ -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,34 +381,69 @@ 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 _);
|
||||
|
||||
|
||||
queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice()));
|
||||
}
|
||||
}
|
||||
|
@ -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,15 +964,15 @@ 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.
|
||||
///
|
||||
///
|
||||
/// 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,
|
||||
|
@ -816,13 +986,13 @@ impl Default for ShadowSettings {
|
|||
pcss_blocker_search_samples: 25,
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
const PCF_SAMPLES_NUM_MAX: u32 = 128;
|
||||
const PCSS_SAMPLES_NUM_MAX: u32 = 128;
|
||||
|
||||
/// 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 2, ONLY hardware 2x2 PCF will be used.
|
||||
/// If `pcss_blocker_search_samples` is set to zero, PCSS will be disabled.
|
||||
|
@ -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),
|
||||
|
|
|
@ -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];
|
||||
|
||||
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
|
||||
// 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;
|
||||
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> {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue