render: point light shadows in texture atlas, fix bug with unaligned GpuSlotBuffer
This commit is contained in:
parent
40fa9c09da
commit
b45c2f4fab
|
@ -161,13 +161,13 @@ fn setup_scene_plugin(game: &mut Game) {
|
||||||
DirectionalLight {
|
DirectionalLight {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
color: Vec3::new(1.0, 0.95, 0.9),
|
color: Vec3::new(1.0, 0.95, 0.9),
|
||||||
intensity: 1.0,
|
intensity: 0.5,
|
||||||
},
|
},
|
||||||
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),
|
||||||
|
@ -177,6 +177,18 @@ fn setup_scene_plugin(game: &mut Game) {
|
||||||
},
|
},
|
||||||
Transform::from_xyz(5.0, -2.5, -3.3),
|
Transform::from_xyz(5.0, -2.5, -3.3),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
world.spawn((
|
||||||
|
//cube_mesh.clone(),
|
||||||
|
PointLight {
|
||||||
|
enabled: true,
|
||||||
|
color: Vec3::new(0.278, 0.984, 0.0),
|
||||||
|
intensity: 2.0,
|
||||||
|
range: 9.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(-0.5, 2.0, -5.0),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut camera = CameraComponent::new_3d();
|
let mut camera = CameraComponent::new_3d();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
mem,
|
mem,
|
||||||
num::NonZeroU64,
|
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
|
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||||
};
|
};
|
||||||
|
@ -13,13 +12,13 @@ use lyra_ecs::{
|
||||||
use lyra_game_derive::RenderGraphLabel;
|
use lyra_game_derive::RenderGraphLabel;
|
||||||
use lyra_math::{Angle, Transform};
|
use lyra_math::{Angle, Transform};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use tracing::{debug, warn};
|
use tracing::warn;
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
use crate::render::{
|
use crate::render::{
|
||||||
graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue},
|
graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue},
|
||||||
light::{directional::DirectionalLight, LightType, PointLight},
|
light::{directional::DirectionalLight, LightType, PointLight},
|
||||||
resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState},
|
resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState},
|
||||||
transform_buffer_storage::TransformBuffers,
|
transform_buffer_storage::TransformBuffers,
|
||||||
vertex::Vertex,
|
vertex::Vertex,
|
||||||
AtlasFrame, GpuSlotBuffer, TextureAtlas,
|
AtlasFrame, GpuSlotBuffer, TextureAtlas,
|
||||||
|
@ -68,6 +67,7 @@ pub struct ShadowMapsPass {
|
||||||
render_meshes: Option<ResourceData>,
|
render_meshes: Option<ResourceData>,
|
||||||
mesh_buffers: Option<ResourceData>,
|
mesh_buffers: Option<ResourceData>,
|
||||||
pipeline: Option<RenderPipeline>,
|
pipeline: Option<RenderPipeline>,
|
||||||
|
point_light_pipeline: Option<RenderPipeline>,
|
||||||
|
|
||||||
atlas: LightShadowMapAtlas,
|
atlas: LightShadowMapAtlas,
|
||||||
/// The depth map atlas sampler
|
/// The depth map atlas sampler
|
||||||
|
@ -84,10 +84,8 @@ impl ShadowMapsPass {
|
||||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: true,
|
has_dynamic_offset: false,
|
||||||
min_binding_size: Some(
|
min_binding_size: None,
|
||||||
NonZeroU64::new(mem::size_of::<LightShadowUniform>() as _).unwrap(),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
}],
|
}],
|
||||||
|
@ -98,7 +96,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 * 4,
|
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 {
|
||||||
|
@ -121,12 +119,11 @@ impl ShadowMapsPass {
|
||||||
|
|
||||||
let cap = device.limits().max_storage_buffer_binding_size as u64
|
let cap = device.limits().max_storage_buffer_binding_size as u64
|
||||||
/ mem::size_of::<LightShadowUniform>() as u64;
|
/ mem::size_of::<LightShadowUniform>() as u64;
|
||||||
let uniforms_buffer = GpuSlotBuffer::new_aligned(
|
let uniforms_buffer = GpuSlotBuffer::new(
|
||||||
device,
|
device,
|
||||||
Some("buffer_shadow_maps_light"),
|
Some("buffer_shadow_maps_light"),
|
||||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
cap,
|
cap,
|
||||||
256,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let uniforms_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let uniforms_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
@ -137,7 +134,7 @@ impl ShadowMapsPass {
|
||||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
buffer: uniforms_buffer.buffer(),
|
buffer: uniforms_buffer.buffer(),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
size: Some(NonZeroU64::new(mem::size_of::<LightShadowUniform>() as _).unwrap()),
|
size: None,
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
@ -152,6 +149,7 @@ impl ShadowMapsPass {
|
||||||
render_meshes: None,
|
render_meshes: None,
|
||||||
mesh_buffers: None,
|
mesh_buffers: None,
|
||||||
pipeline: None,
|
pipeline: None,
|
||||||
|
point_light_pipeline: None,
|
||||||
|
|
||||||
atlas_sampler: Rc::new(sampler),
|
atlas_sampler: Rc::new(sampler),
|
||||||
atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))),
|
atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))),
|
||||||
|
@ -165,6 +163,7 @@ impl ShadowMapsPass {
|
||||||
light_type: LightType,
|
light_type: LightType,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
light_pos: Transform,
|
light_pos: Transform,
|
||||||
|
far_plane: f32,
|
||||||
) -> LightDepthMap {
|
) -> LightDepthMap {
|
||||||
const NEAR_PLANE: f32 = 0.1;
|
const NEAR_PLANE: f32 = 0.1;
|
||||||
const FAR_PLANE: f32 = 45.0;
|
const FAR_PLANE: f32 = 45.0;
|
||||||
|
@ -193,6 +192,11 @@ impl ShadowMapsPass {
|
||||||
let u = LightShadowUniform {
|
let u = LightShadowUniform {
|
||||||
space_mat: light_proj,
|
space_mat: light_proj,
|
||||||
atlas_frame,
|
atlas_frame,
|
||||||
|
near_plane: NEAR_PLANE,
|
||||||
|
far_plane,
|
||||||
|
_padding1: [0; 2],
|
||||||
|
light_pos: light_pos.translation,
|
||||||
|
_padding2: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let uniform_index = self.light_uniforms_buffer.insert(queue, &u);
|
let uniform_index = self.light_uniforms_buffer.insert(queue, &u);
|
||||||
|
@ -207,10 +211,11 @@ impl ShadowMapsPass {
|
||||||
Angle::Degrees(90.0).to_radians(),
|
Angle::Degrees(90.0).to_radians(),
|
||||||
aspect,
|
aspect,
|
||||||
NEAR_PLANE,
|
NEAR_PLANE,
|
||||||
FAR_PLANE,
|
far_plane,
|
||||||
);
|
);
|
||||||
|
|
||||||
let light_trans = light_pos.translation;
|
let light_trans = light_pos.translation;
|
||||||
|
// right, left, top, bottom, near, and far
|
||||||
let views = [
|
let views = [
|
||||||
projection
|
projection
|
||||||
* glam::Mat4::look_at_rh(
|
* glam::Mat4::look_at_rh(
|
||||||
|
@ -287,6 +292,11 @@ impl ShadowMapsPass {
|
||||||
&LightShadowUniform {
|
&LightShadowUniform {
|
||||||
space_mat: views[i],
|
space_mat: views[i],
|
||||||
atlas_frame: frames[i],
|
atlas_frame: frames[i],
|
||||||
|
near_plane: NEAR_PLANE,
|
||||||
|
far_plane,
|
||||||
|
_padding1: [0; 2],
|
||||||
|
light_pos: light_trans,
|
||||||
|
_padding2: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
indices[i] = uniform_i;
|
indices[i] = uniform_i;
|
||||||
|
@ -380,19 +390,18 @@ impl Node for ShadowMapsPass {
|
||||||
|
|
||||||
/* for (entity, pos, (has_dir, has_point)) in world.view_iter::<(Entities, &Transform, Or<Has<DirectionalLight>, Has<PointLight>>)>() {
|
/* for (entity, pos, (has_dir, has_point)) in world.view_iter::<(Entities, &Transform, Or<Has<DirectionalLight>, Has<PointLight>>)>() {
|
||||||
if !self.depth_maps.contains_key(&entity) {
|
if !self.depth_maps.contains_key(&entity) {
|
||||||
let light_type = if has_dir.is_some() {
|
// TODO: calculate far plane
|
||||||
LightType::Directional
|
let (light_type, far_plane) = if has_dir.is_some() {
|
||||||
|
(LightType::Directional, 45.0)
|
||||||
} else if has_point.is_some() {
|
} else if has_point.is_some() {
|
||||||
LightType::Point
|
(LightType::Point, 45.0)
|
||||||
} else {
|
} else {
|
||||||
todo!("Spot lights")
|
todo!("Spot lights")
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Creating depth map for {light_type:?}");
|
|
||||||
|
|
||||||
// TODO: dont pack the textures as they're added
|
// TODO: dont pack the textures as they're added
|
||||||
let atlas_index =
|
let atlas_index =
|
||||||
self.create_depth_map(&context.queue, light_type, entity, *pos);
|
self.create_depth_map(&context.queue, light_type, entity, *pos, far_plane);
|
||||||
index_components_queue.push_back((entity, atlas_index));
|
index_components_queue.push_back((entity, atlas_index));
|
||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
@ -401,7 +410,7 @@ impl Node for ShadowMapsPass {
|
||||||
if !self.depth_maps.contains_key(&entity) {
|
if !self.depth_maps.contains_key(&entity) {
|
||||||
// TODO: dont pack the textures as they're added
|
// TODO: dont pack the textures as they're added
|
||||||
let atlas_index =
|
let atlas_index =
|
||||||
self.create_depth_map(&context.queue, LightType::Directional, entity, *pos);
|
self.create_depth_map(&context.queue, LightType::Directional, entity, *pos, 45.0);
|
||||||
index_components_queue.push_back((entity, atlas_index));
|
index_components_queue.push_back((entity, atlas_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,7 +419,7 @@ impl Node for ShadowMapsPass {
|
||||||
if !self.depth_maps.contains_key(&entity) {
|
if !self.depth_maps.contains_key(&entity) {
|
||||||
// TODO: dont pack the textures as they're added
|
// TODO: dont pack the textures as they're added
|
||||||
let atlas_index =
|
let atlas_index =
|
||||||
self.create_depth_map(&context.queue, LightType::Point, entity, *pos);
|
self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0);
|
||||||
index_components_queue.push_back((entity, atlas_index));
|
index_components_queue.push_back((entity, atlas_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -439,18 +448,14 @@ impl Node for ShadowMapsPass {
|
||||||
&graph.device,
|
&graph.device,
|
||||||
&RenderPipelineDescriptor {
|
&RenderPipelineDescriptor {
|
||||||
label: Some("pipeline_shadows".into()),
|
label: Some("pipeline_shadows".into()),
|
||||||
layouts: vec![bgl, transforms],
|
layouts: vec![bgl.clone(), transforms.clone()],
|
||||||
push_constant_ranges: vec![],
|
push_constant_ranges: vec![],
|
||||||
vertex: VertexState {
|
vertex: VertexState {
|
||||||
module: shader.clone(),
|
module: shader.clone(),
|
||||||
entry_point: "vs_main".into(),
|
entry_point: "vs_main".into(),
|
||||||
buffers: vec![Vertex::position_desc().into()],
|
buffers: vec![Vertex::position_desc().into()],
|
||||||
},
|
},
|
||||||
fragment: None, /* Some(FragmentState {
|
fragment: None,
|
||||||
module: shader,
|
|
||||||
entry_point: "fs_main".into(),
|
|
||||||
targets: vec![],
|
|
||||||
}), */
|
|
||||||
depth_stencil: Some(wgpu::DepthStencilState {
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
format: wgpu::TextureFormat::Depth32Float,
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
depth_write_enabled: true,
|
depth_write_enabled: true,
|
||||||
|
@ -459,6 +464,40 @@ impl Node for ShadowMapsPass {
|
||||||
bias: wgpu::DepthBiasState::default(),
|
bias: wgpu::DepthBiasState::default(),
|
||||||
}),
|
}),
|
||||||
primitive: wgpu::PrimitiveState {
|
primitive: wgpu::PrimitiveState {
|
||||||
|
//cull_mode: Some(wgpu::Face::Front),
|
||||||
|
cull_mode: Some(wgpu::Face::Back),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
self.point_light_pipeline = Some(RenderPipeline::create(
|
||||||
|
&graph.device,
|
||||||
|
&RenderPipelineDescriptor {
|
||||||
|
label: Some("pipeline_point_light_shadows".into()),
|
||||||
|
layouts: vec![bgl, transforms],
|
||||||
|
push_constant_ranges: vec![],
|
||||||
|
vertex: VertexState {
|
||||||
|
module: shader.clone(),
|
||||||
|
entry_point: "vs_main".into(),
|
||||||
|
buffers: vec![Vertex::position_desc().into()],
|
||||||
|
},
|
||||||
|
fragment: Some(FragmentState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: "fs_point_light_main".into(),
|
||||||
|
targets: vec![],
|
||||||
|
}),
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: true,
|
||||||
|
depth_compare: wgpu::CompareFunction::Less,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
//cull_mode: Some(wgpu::Face::Front),
|
||||||
cull_mode: Some(wgpu::Face::Back),
|
cull_mode: Some(wgpu::Face::Back),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -477,6 +516,7 @@ impl Node for ShadowMapsPass {
|
||||||
) {
|
) {
|
||||||
let encoder = context.encoder.as_mut().unwrap();
|
let encoder = context.encoder.as_mut().unwrap();
|
||||||
let pipeline = self.pipeline.as_ref().unwrap();
|
let pipeline = self.pipeline.as_ref().unwrap();
|
||||||
|
let point_light_pipeline = self.point_light_pipeline.as_ref().unwrap();
|
||||||
|
|
||||||
let render_meshes = self.render_meshes();
|
let render_meshes = self.render_meshes();
|
||||||
let mesh_buffers = self.mesh_buffers();
|
let mesh_buffers = self.mesh_buffers();
|
||||||
|
@ -495,17 +535,15 @@ impl Node for ShadowMapsPass {
|
||||||
stencil_ops: None,
|
stencil_ops: None,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
pass.set_pipeline(&pipeline);
|
|
||||||
|
|
||||||
for light_depth_map in self.depth_maps.values() {
|
for light_depth_map in self.depth_maps.values() {
|
||||||
|
|
||||||
match light_depth_map.light_type {
|
match light_depth_map.light_type {
|
||||||
LightType::Directional => {
|
LightType::Directional => {
|
||||||
let frame = atlas.texture_frame(light_depth_map.atlas_index)
|
pass.set_pipeline(&pipeline);
|
||||||
.expect("missing atlas frame of light");
|
|
||||||
let u_offset = self.light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32;
|
|
||||||
|
|
||||||
//debug!("Rendering directional light with atlas {} uniform index {} and offset {}, in viewport {:?}", light_depth_map.atlas_index, light_depth_map.uniform_index[0], u_offset, frame);
|
let frame = atlas.texture_frame(light_depth_map.atlas_index)
|
||||||
|
.expect("missing atlas frame for light");
|
||||||
|
|
||||||
light_shadow_pass_impl(
|
light_shadow_pass_impl(
|
||||||
&mut pass,
|
&mut pass,
|
||||||
|
@ -514,17 +552,16 @@ impl Node for ShadowMapsPass {
|
||||||
&mesh_buffers,
|
&mesh_buffers,
|
||||||
&transforms,
|
&transforms,
|
||||||
&frame,
|
&frame,
|
||||||
u_offset,
|
light_depth_map.uniform_index[0] as _,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
LightType::Point => {
|
LightType::Point => {
|
||||||
|
pass.set_pipeline(&point_light_pipeline);
|
||||||
|
|
||||||
for side in 0..6 {
|
for side in 0..6 {
|
||||||
let frame = atlas.texture_frame(light_depth_map.atlas_index + side)
|
let frame = atlas.texture_frame(light_depth_map.atlas_index + side)
|
||||||
.expect("missing atlas frame of light");
|
.expect("missing atlas frame of light");
|
||||||
let ui = light_depth_map.uniform_index[side as usize];
|
let ui = light_depth_map.uniform_index[side as usize];
|
||||||
let u_offset = self.light_uniforms_buffer.offset_of(ui) as u32;
|
|
||||||
|
|
||||||
//debug!("Rendering point light side {side} with atlas {} uniform index {ui} and offset {u_offset} and viewport {:?}", light_depth_map.atlas_index + side, frame);
|
|
||||||
|
|
||||||
light_shadow_pass_impl(
|
light_shadow_pass_impl(
|
||||||
&mut pass,
|
&mut pass,
|
||||||
|
@ -533,7 +570,7 @@ impl Node for ShadowMapsPass {
|
||||||
&mesh_buffers,
|
&mesh_buffers,
|
||||||
&transforms,
|
&transforms,
|
||||||
&frame,
|
&frame,
|
||||||
u_offset,
|
ui as _,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -550,7 +587,7 @@ fn light_shadow_pass_impl<'a>(
|
||||||
mesh_buffers: &'a RenderAssets<MeshBufferStorage>,
|
mesh_buffers: &'a RenderAssets<MeshBufferStorage>,
|
||||||
transforms: &'a TransformBuffers,
|
transforms: &'a TransformBuffers,
|
||||||
shadow_atlas_viewport: &AtlasFrame,
|
shadow_atlas_viewport: &AtlasFrame,
|
||||||
uniform_offset: u32,
|
uniform_index: u32,
|
||||||
) {
|
) {
|
||||||
// only render to the light's map in the atlas
|
// only render to the light's map in the atlas
|
||||||
pass.set_viewport(
|
pass.set_viewport(
|
||||||
|
@ -579,7 +616,7 @@ fn light_shadow_pass_impl<'a>(
|
||||||
let buffers = buffers.unwrap();
|
let buffers = buffers.unwrap();
|
||||||
|
|
||||||
//let uniform_index = light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32;
|
//let uniform_index = light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32;
|
||||||
pass.set_bind_group(0, &uniforms_bind_group, &[uniform_offset]);
|
pass.set_bind_group(0, &uniforms_bind_group, &[]);
|
||||||
|
|
||||||
// Get the bindgroup for job's transform and bind to it using an offset.
|
// Get the bindgroup for job's transform and bind to it using an offset.
|
||||||
let bindgroup = transforms.bind_group(job.transform_id);
|
let bindgroup = transforms.bind_group(job.transform_id);
|
||||||
|
@ -595,7 +632,7 @@ fn light_shadow_pass_impl<'a>(
|
||||||
buffers.buffer_vertex.buffer().slice(..),
|
buffers.buffer_vertex.buffer().slice(..),
|
||||||
);
|
);
|
||||||
pass.set_index_buffer(indices.buffer().slice(..), *idx_type);
|
pass.set_index_buffer(indices.buffer().slice(..), *idx_type);
|
||||||
pass.draw_indexed(0..indices_len, 0, 0..1);
|
pass.draw_indexed(0..indices_len, 0, uniform_index..uniform_index + 1);
|
||||||
} else {
|
} else {
|
||||||
let vertex_count = buffers.buffer_vertex.count();
|
let vertex_count = buffers.buffer_vertex.count();
|
||||||
|
|
||||||
|
@ -603,7 +640,7 @@ fn light_shadow_pass_impl<'a>(
|
||||||
buffers.buffer_vertex.slot(),
|
buffers.buffer_vertex.slot(),
|
||||||
buffers.buffer_vertex.buffer().slice(..),
|
buffers.buffer_vertex.buffer().slice(..),
|
||||||
);
|
);
|
||||||
pass.draw(0..vertex_count as u32, 0..1);
|
pass.draw(0..vertex_count as u32, uniform_index..uniform_index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -613,6 +650,11 @@ fn light_shadow_pass_impl<'a>(
|
||||||
pub struct LightShadowUniform {
|
pub struct LightShadowUniform {
|
||||||
space_mat: glam::Mat4,
|
space_mat: glam::Mat4,
|
||||||
atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed
|
atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed
|
||||||
|
near_plane: f32,
|
||||||
|
far_plane: f32,
|
||||||
|
_padding1: [u32; 2],
|
||||||
|
light_pos: glam::Vec3,
|
||||||
|
_padding2: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
@ -648,8 +690,3 @@ impl LightShadowMapAtlas {
|
||||||
self.0.write().unwrap()
|
self.0.write().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fn uniform_index_offset(limits: &wgpu::Limits, uniform_idx: u64) -> u32 {
|
|
||||||
let t = uniform_idx as u32 % (limits.max_storage_buffer_binding_size / mem::size_of::<LightShadowUniform>() as u32);
|
|
||||||
t * limits.min_uniform_buffer_offset_alignment
|
|
||||||
} */
|
|
||||||
|
|
|
@ -23,8 +23,12 @@ struct VertexOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TextureAtlasFrame {
|
struct TextureAtlasFrame {
|
||||||
offset: vec2<u32>,
|
/*offset: vec2<u32>,
|
||||||
size: vec2<u32>,
|
size: vec2<u32>,*/
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransformData {
|
struct TransformData {
|
||||||
|
@ -111,11 +115,9 @@ var s_diffuse: sampler;
|
||||||
struct LightShadowMapUniform {
|
struct LightShadowMapUniform {
|
||||||
light_space_matrix: mat4x4<f32>,
|
light_space_matrix: mat4x4<f32>,
|
||||||
atlas_frame: TextureAtlasFrame,
|
atlas_frame: TextureAtlasFrame,
|
||||||
}
|
near_plane: f32,
|
||||||
|
far_plane: f32,
|
||||||
struct LightShadowMapUniformAligned {
|
light_pos: vec3<f32>,
|
||||||
@align(256)
|
|
||||||
inner: LightShadowMapUniform
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(4) @binding(0)
|
@group(4) @binding(0)
|
||||||
|
@ -130,7 +132,7 @@ var s_shadow_maps_atlas: sampler;
|
||||||
@group(5) @binding(2)
|
@group(5) @binding(2)
|
||||||
var<uniform> u_shadow_maps_atlas_size: vec2<u32>;
|
var<uniform> u_shadow_maps_atlas_size: vec2<u32>;
|
||||||
@group(5) @binding(3)
|
@group(5) @binding(3)
|
||||||
var<storage, read> u_light_shadow: array<LightShadowMapUniformAligned>;
|
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
@ -152,21 +154,22 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let light_offset = tile.x;
|
let light_offset = tile.x;
|
||||||
let light_count = tile.y;
|
let light_count = tile.y;
|
||||||
|
|
||||||
let atlas_dimensions: vec2<i32> = textureDimensions(t_shadow_maps_atlas);
|
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
|
||||||
|
|
||||||
for (var i = 0u; i < light_count; i++) {
|
for (var i = 0u; i < light_count; i++) {
|
||||||
let light_index = u_light_indices[light_offset + i];
|
let light_index = u_light_indices[light_offset + i];
|
||||||
let light: Light = u_lights.data[light_index];
|
let light: Light = u_lights.data[light_index];
|
||||||
|
let light_dir = normalize(-light.direction);
|
||||||
|
|
||||||
if (light.light_ty == LIGHT_TY_DIRECTIONAL) {
|
if (light.light_ty == LIGHT_TY_DIRECTIONAL) {
|
||||||
let light_dir = normalize(-light.direction);
|
let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]];
|
||||||
let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]].inner;
|
|
||||||
let frag_pos_light_space = shadow_u.light_space_matrix * vec4<f32>(in.world_position, 1.0);
|
let frag_pos_light_space = shadow_u.light_space_matrix * vec4<f32>(in.world_position, 1.0);
|
||||||
|
|
||||||
let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame);
|
let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame);
|
||||||
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) {
|
||||||
light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color);
|
let shadow = calc_shadow_point(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) {
|
} 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);
|
||||||
}
|
}
|
||||||
|
@ -176,9 +179,70 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
return vec4<f32>(light_object_res, object_color.a);
|
return vec4<f32>(light_object_res, object_color.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the cube map side index of a 3d texture coord
|
||||||
|
///
|
||||||
|
/// 0 -> UNKNOWN
|
||||||
|
/// 1 -> right
|
||||||
|
/// 2 -> left
|
||||||
|
/// 3 -> top
|
||||||
|
/// 4 -> bottom
|
||||||
|
/// 5 -> near
|
||||||
|
/// 6 -> far
|
||||||
|
fn get_side_idx(tex_coord: vec3<f32>) -> vec3<f32> {
|
||||||
|
let abs_x = abs(tex_coord.x);
|
||||||
|
let abs_y = abs(tex_coord.y);
|
||||||
|
let abs_z = abs(tex_coord.z);
|
||||||
|
|
||||||
|
var major_axis: f32 = 0.0;
|
||||||
|
var cube_idx: i32 = 0;
|
||||||
|
var res = vec2<f32>(0.0);
|
||||||
|
|
||||||
|
// Determine the dominant axis
|
||||||
|
if (abs_x >= abs_y && abs_x >= abs_z) {
|
||||||
|
major_axis = tex_coord.x;
|
||||||
|
if (tex_coord.x > 0.0) {
|
||||||
|
cube_idx = 1;
|
||||||
|
res = vec2<f32>(-tex_coord.z, -tex_coord.y);
|
||||||
|
} else {
|
||||||
|
cube_idx = 2;
|
||||||
|
res = vec2<f32>(tex_coord.z, -tex_coord.y);
|
||||||
|
}
|
||||||
|
} else if (abs_y >= abs_x && abs_y >= abs_z) {
|
||||||
|
major_axis = tex_coord.y;
|
||||||
|
if (tex_coord.y > 0.0) {
|
||||||
|
cube_idx = 3;
|
||||||
|
res = vec2<f32>(tex_coord.x, tex_coord.z);
|
||||||
|
} else {
|
||||||
|
cube_idx = 4;
|
||||||
|
res = vec2<f32>(tex_coord.x, -tex_coord.z);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
major_axis = tex_coord.z;
|
||||||
|
if (tex_coord.z > 0.0) {
|
||||||
|
cube_idx = 5;
|
||||||
|
res = vec2<f32>(tex_coord.x, -tex_coord.y);
|
||||||
|
} else {
|
||||||
|
cube_idx = 6;
|
||||||
|
res = vec2<f32>(-tex_coord.x, -tex_coord.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = (res / abs(major_axis) + 1.0) * 0.5;
|
||||||
|
//res = normalize(res);
|
||||||
|
//res.y = 1.0-res.y; // invert y because wgsl
|
||||||
|
//let t = res.x;
|
||||||
|
//res.x = res.y;
|
||||||
|
|
||||||
|
//res.y = 1.0 - t;
|
||||||
|
res.y = 1.0 - res.y;
|
||||||
|
//res.x = 1.0 - res.x;
|
||||||
|
|
||||||
|
return vec3<f32>(res, f32(cube_idx));
|
||||||
|
}
|
||||||
|
|
||||||
fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>, atlas_dimensions: vec2<i32>, atlas_region: TextureAtlasFrame) -> f32 {
|
fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>, atlas_dimensions: vec2<i32>, atlas_region: TextureAtlasFrame) -> 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 clipped after transforming
|
// for some reason the y component is flipped after transforming
|
||||||
proj_coords.y = -proj_coords.y;
|
proj_coords.y = -proj_coords.y;
|
||||||
|
|
||||||
// dont cast shadows outside the light's far plane
|
// dont cast shadows outside the light's far plane
|
||||||
|
@ -190,8 +254,8 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light
|
||||||
let xy_remapped = proj_coords.xy * 0.5 + 0.5;
|
let xy_remapped = proj_coords.xy * 0.5 + 0.5;
|
||||||
|
|
||||||
// no need to get the y since the maps are square
|
// no need to get the y since the maps are square
|
||||||
let atlas_start = f32(atlas_region.offset.x) / f32(atlas_dimensions.x);
|
let atlas_start = f32(atlas_region.x) / f32(atlas_dimensions.x);
|
||||||
let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(atlas_dimensions.x);
|
let atlas_end = f32(atlas_region.x + atlas_region.width) / f32(atlas_dimensions.x);
|
||||||
// lerp the tex coords to the shadow map for this light.
|
// lerp the tex coords to the shadow map for this light.
|
||||||
proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x);
|
proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x);
|
||||||
proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y);
|
proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y);
|
||||||
|
@ -204,7 +268,7 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light
|
||||||
|
|
||||||
// must manually apply offset to the texture coords since `textureSampleLevel` requires a
|
// must manually apply offset to the texture coords since `textureSampleLevel` requires a
|
||||||
// const value.
|
// const value.
|
||||||
let offset_coords = proj_coords.xy + (vec2<f32>(atlas_region.offset) / vec2<f32>(atlas_dimensions));
|
let offset_coords = proj_coords.xy + (vec2<f32>(f32(atlas_region.x), f32(atlas_region.y)) / vec2<f32>(atlas_dimensions));
|
||||||
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0);
|
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0);
|
||||||
let current_depth = proj_coords.z;
|
let current_depth = proj_coords.z;
|
||||||
|
|
||||||
|
@ -218,8 +282,50 @@ fn calc_shadow_dir_light(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light
|
||||||
return shadow;
|
return shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calc_shadow_point(world_pos: vec3<f32>, atlas_dimensions: vec2<i32>, atlas_regions: array<TextureAtlasFrame, 6>) -> f32 {
|
fn calc_shadow_point(world_pos: vec3<f32>, world_normal: vec3<f32>, light_dir: vec3<f32>, light: Light, atlas_dimensions: vec2<i32>) -> f32 {
|
||||||
|
var frag_to_light = world_pos - light.position;
|
||||||
|
let temp = get_side_idx(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;
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, 0.0);
|
||||||
|
let current_depth = length(frag_to_light);
|
||||||
|
|
||||||
|
// convert depth from [0; 1] to the original depth value
|
||||||
|
closest_depth *= u.far_plane;
|
||||||
|
|
||||||
|
// use a bias to avoid shadow acne
|
||||||
|
let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005);
|
||||||
|
|
||||||
|
var shadow = 0.0;
|
||||||
|
if current_depth - bias > closest_depth {
|
||||||
|
shadow = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_grid(in: VertexOutput) -> vec4<f32> {
|
fn debug_grid(in: VertexOutput) -> vec4<f32> {
|
||||||
|
@ -266,7 +372,7 @@ fn blinn_phong_dir_light(world_pos: vec3<f32>, world_norm: vec3<f32>, dir_light:
|
||||||
return (ambient_color + (1.0 - shadow) * (diffuse_color + specular_color)) * dir_light.intensity;
|
return (ambient_color + (1.0 - shadow) * (diffuse_color + specular_color)) * dir_light.intensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blinn_phong_point_light(world_pos: vec3<f32>, world_norm: vec3<f32>, point_light: Light, material: Material, specular_factor: vec3<f32>) -> vec3<f32> {
|
fn blinn_phong_point_light(world_pos: vec3<f32>, world_norm: vec3<f32>, point_light: Light, material: Material, specular_factor: vec3<f32>, shadow: f32) -> vec3<f32> {
|
||||||
let light_color = point_light.color.xyz;
|
let light_color = point_light.color.xyz;
|
||||||
let light_pos = point_light.position.xyz;
|
let light_pos = point_light.position.xyz;
|
||||||
let camera_view_pos = u_camera.position;
|
let camera_view_pos = u_camera.position;
|
||||||
|
@ -296,7 +402,7 @@ 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 + diffuse_color + specular_color) * point_light.intensity;
|
return (ambient_color + (1.0 - shadow) * (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> {
|
||||||
|
|
|
@ -11,23 +11,54 @@ struct TextureAtlasFrame {
|
||||||
struct LightShadowMapUniform {
|
struct LightShadowMapUniform {
|
||||||
light_space_matrix: mat4x4<f32>,
|
light_space_matrix: mat4x4<f32>,
|
||||||
atlas_frame: TextureAtlasFrame,
|
atlas_frame: TextureAtlasFrame,
|
||||||
|
near_plane: f32,
|
||||||
|
far_plane: f32,
|
||||||
|
light_pos: vec3<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<storage, read> u_light_shadow: LightShadowMapUniform;
|
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
|
||||||
|
/*@group(0) @binding(1)
|
||||||
|
var<uniform> u_light_pos: vec3<f32>;
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> u_light_far_plane: f32;*/
|
||||||
|
|
||||||
@group(1) @binding(0)
|
@group(1) @binding(0)
|
||||||
var<uniform> u_model_transform_data: TransformData;
|
var<uniform> u_model_transform_data: TransformData;
|
||||||
|
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position)
|
@builtin(position)
|
||||||
clip_position: vec4<f32>,
|
clip_position: vec4<f32>,
|
||||||
|
@location(0) world_pos: vec3<f32>,
|
||||||
|
@location(1) instance_index: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(
|
fn vs_main(
|
||||||
@location(0) position: vec3<f32>
|
@location(0) position: vec3<f32>,
|
||||||
|
@builtin(instance_index) instance_index: u32,
|
||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
let pos = u_light_shadow.light_space_matrix * u_model_transform_data.transform * vec4<f32>(position, 1.0);
|
let world_pos = u_model_transform_data.transform * vec4<f32>(position, 1.0);
|
||||||
return VertexOutput(pos);
|
let pos = u_light_shadow[instance_index].light_space_matrix * world_pos;
|
||||||
|
return VertexOutput(pos, world_pos.xyz, instance_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FragmentOutput {
|
||||||
|
@builtin(frag_depth) depth: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fragment shader used for point lights (or other perspective lights) to create linear depth
|
||||||
|
@fragment
|
||||||
|
fn fs_point_light_main(
|
||||||
|
in: VertexOutput
|
||||||
|
) -> FragmentOutput {
|
||||||
|
let u = u_light_shadow[in.instance_index];
|
||||||
|
|
||||||
|
var light_dis = length(in.world_pos - u.light_pos);
|
||||||
|
|
||||||
|
// map to [0; 1] range by dividing by far plane
|
||||||
|
light_dis = light_dis / u.far_plane;
|
||||||
|
|
||||||
|
return FragmentOutput(light_dis);
|
||||||
}
|
}
|
|
@ -54,19 +54,11 @@ impl<T: bytemuck::Pod + bytemuck::Zeroable> GpuSlotBuffer<T> {
|
||||||
|
|
||||||
/// Calculates the byte offset in the buffer of the element at `i`.
|
/// Calculates the byte offset in the buffer of the element at `i`.
|
||||||
pub fn offset_of(&self, i: u64) -> u64 {
|
pub fn offset_of(&self, i: u64) -> u64 {
|
||||||
/* let offset = i * mem::size_of::<T>() as u64;
|
|
||||||
|
|
||||||
if let Some(align) = self.alignment {
|
|
||||||
round_mult::up(offset, NonZeroU64::new(align).unwrap()).unwrap()
|
|
||||||
} else {
|
|
||||||
offset
|
|
||||||
} */
|
|
||||||
|
|
||||||
if let Some(align) = self.alignment {
|
if let Some(align) = self.alignment {
|
||||||
let transform_index = i % self.capacity;
|
let transform_index = i % self.capacity;
|
||||||
transform_index * align
|
transform_index * align
|
||||||
} else {
|
} else {
|
||||||
mem::size_of::<T>() as u64
|
i * mem::size_of::<T>() as u64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue