render: implement packed texture atlas for shadow maps

This commit is contained in:
SeanOMik 2024-07-10 20:16:21 -04:00
parent e2b554b4ef
commit a4ce4cb432
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
7 changed files with 322 additions and 147 deletions

7
Cargo.lock generated
View File

@ -1881,6 +1881,7 @@ dependencies = [
"lyra-scene", "lyra-scene",
"petgraph", "petgraph",
"quote", "quote",
"rectangle-pack",
"rustc-hash", "rustc-hash",
"syn 2.0.51", "syn 2.0.51",
"thiserror", "thiserror",
@ -2767,6 +2768,12 @@ dependencies = [
"rand_core 0.3.1", "rand_core 0.3.1",
] ]
[[package]]
name = "rectangle-pack"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.3.5" version = "0.3.5"

View File

@ -38,6 +38,7 @@ unique = "0.9.1"
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
petgraph = { version = "0.6.5", features = ["matrix_graph"] } petgraph = { version = "0.6.5", features = ["matrix_graph"] }
bind_match = "0.1.2" bind_match = "0.1.2"
rectangle-pack = "0.4.2"
[features] [features]
tracy = ["dep:tracing-tracy"] tracy = ["dep:tracing-tracy"]

View File

@ -116,9 +116,14 @@ impl Node for MeshPass {
.expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler")
.as_sampler() .as_sampler()
.unwrap(); .unwrap();
let dir_light_projection_buf = graph let atlas_size_buf = graph
.slot_value(ShadowMapsPassSlots::DirLightProjectionBuffer) .slot_value(ShadowMapsPassSlots::ShadowAtlasSizeBuffer)
.expect("missing ShadowMapsPassSlots::DirLightProjectionBuffer") .expect("missing ShadowMapsPassSlots::ShadowAtlasSizeBuffer")
.as_buffer()
.unwrap();
let light_uniform_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowLightUniformsBuffer)
.expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer")
.as_buffer() .as_buffer()
.unwrap(); .unwrap();
@ -151,6 +156,16 @@ impl Node for MeshPass {
}, },
count: None, count: None,
}, },
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
], ],
}); });
@ -169,7 +184,15 @@ impl Node for MeshPass {
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 2, binding: 2,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: dir_light_projection_buf, buffer: atlas_size_buf,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: light_uniform_buf,
offset: 0, offset: 0,
size: None, size: None,
}), }),

View File

@ -1,9 +1,7 @@
use std::{mem, num::NonZeroU64, rc::Rc, sync::Arc}; use std::{collections::VecDeque, mem, num::NonZeroU64, ops::Deref, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}};
use glam::UVec2;
use lyra_ecs::{ use lyra_ecs::{
query::{filter::Has, Entities}, query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData
AtomicRef, Entity, ResourceData,
}; };
use lyra_game_derive::RenderGraphLabel; use lyra_game_derive::RenderGraphLabel;
use lyra_math::Transform; use lyra_math::Transform;
@ -12,24 +10,20 @@ use tracing::{debug, 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, resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasViewport, TextureAtlas
light::directional::DirectionalLight,
resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState},
transform_buffer_storage::TransformBuffers,
vertex::Vertex,
TextureAtlas,
}; };
use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; use super::{MeshBufferStorage, RenderAssets, RenderMeshes};
const SHADOW_SIZE: glam::UVec2 = glam::UVec2::new(1024, 1024); const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024);
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub enum ShadowMapsPassSlots { pub enum ShadowMapsPassSlots {
ShadowAtlasTexture, ShadowAtlasTexture,
ShadowAtlasTextureView, ShadowAtlasTextureView,
ShadowAtlasSampler, ShadowAtlasSampler,
DirLightProjectionBuffer, ShadowAtlasSizeBuffer,
ShadowLightUniformsBuffer,
} }
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
@ -38,10 +32,12 @@ pub struct ShadowMapsPassLabel;
struct LightDepthMap { struct LightDepthMap {
light_projection_buffer: Arc<wgpu::Buffer>, light_projection_buffer: Arc<wgpu::Buffer>,
bindgroup: wgpu::BindGroup, bindgroup: wgpu::BindGroup,
atlas_index: u64,
} }
pub struct ShadowMapsPass { pub struct ShadowMapsPass {
bgl: Arc<wgpu::BindGroupLayout>, bgl: Arc<wgpu::BindGroupLayout>,
atlas_size_buffer: Arc<wgpu::Buffer>,
/// depth maps for a light owned by an entity. /// depth maps for a light owned by an entity.
depth_maps: FxHashMap<Entity, LightDepthMap>, depth_maps: FxHashMap<Entity, LightDepthMap>,
@ -52,7 +48,7 @@ pub struct ShadowMapsPass {
mesh_buffers: Option<ResourceData>, mesh_buffers: Option<ResourceData>,
pipeline: Option<RenderPipeline>, pipeline: Option<RenderPipeline>,
atlas: Arc<TextureAtlas>, atlas: LightShadowMapAtlas,
/// The depth map atlas sampler /// The depth map atlas sampler
atlas_sampler: Rc<wgpu::Sampler>, atlas_sampler: Rc<wgpu::Sampler>,
} }
@ -61,50 +57,38 @@ impl ShadowMapsPass {
pub fn new(device: &wgpu::Device) -> Self { pub fn new(device: &wgpu::Device) -> Self {
let bgl = Arc::new( let bgl = Arc::new(
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_shadows_light_projection"), label: Some("bgl_shadow_maps_lights"),
entries: &[wgpu::BindGroupLayoutEntry { entries: &[
binding: 0, wgpu::BindGroupLayoutEntry {
visibility: wgpu::ShaderStages::VERTEX, binding: 0,
ty: wgpu::BindingType::Buffer { visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false, ty: wgpu::BufferBindingType::Uniform,
min_binding_size: Some( has_dynamic_offset: false,
NonZeroU64::new(mem::size_of::<glam::Mat4>() as _).unwrap(), min_binding_size: Some(
), NonZeroU64::new(mem::size_of::<LightShadowUniform>() as _).unwrap(),
}, ),
count: None, },
}], count: None,
}
],
}), }),
); );
/* let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("texture_shadow_map_atlas"),
size: wgpu::Extent3d {
width: SHADOW_SIZE.x,
height: SHADOW_SIZE.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor {
label: Some("shadows_map_view"),
..Default::default()
}); */
let atlas = TextureAtlas::new( let atlas = TextureAtlas::new(
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, SHADOW_SIZE * 4,
UVec2::new(4, 4),
); );
let atlas_size_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("buffer_shadow_maps_atlas_size"),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
contents: bytemuck::bytes_of(&atlas.atlas_size()),
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("sampler_shadow_map_atlas"), label: Some("sampler_shadow_map_atlas"),
address_mode_u: wgpu::AddressMode::ClampToBorder, address_mode_u: wgpu::AddressMode::ClampToBorder,
@ -119,6 +103,7 @@ impl ShadowMapsPass {
Self { Self {
bgl, bgl,
atlas_size_buffer: Arc::new(atlas_size_buffer),
depth_maps: Default::default(), depth_maps: Default::default(),
transform_buffers: None, transform_buffers: None,
render_meshes: None, render_meshes: None,
@ -126,14 +111,20 @@ impl ShadowMapsPass {
pipeline: None, pipeline: None,
atlas_sampler: Rc::new(sampler), atlas_sampler: Rc::new(sampler),
atlas: Arc::new(atlas), atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))),
} }
} }
fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { /// Create a depth map and return the id of the depth map in the texture atlas.
fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) -> u64 {
const NEAR_PLANE: f32 = 0.1; const NEAR_PLANE: f32 = 0.1;
const FAR_PLANE: f32 = 45.0; const FAR_PLANE: f32 = 45.0;
let mut atlas = self.atlas.get_mut();
let atlas_index = atlas.pack_new_texture(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _)
.expect("failed to pack new shadow map into texture atlas");
let atlas_frame = atlas.texture_viewport(atlas_index);
let ortho_proj = let ortho_proj =
glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE);
@ -141,25 +132,31 @@ impl ShadowMapsPass {
glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up()); glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up());
let light_proj = ortho_proj * look_view; let light_proj = ortho_proj * look_view;
let uniform = LightShadowUniform {
space_mat: light_proj,
atlas_frame,
};
let light_projection_buffer = let light_projection_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor { device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("shadows_light_view_mat_buffer"), label: Some("buffer_shadow_maps_light"),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
contents: bytemuck::bytes_of(&light_proj), contents: bytemuck::bytes_of(&uniform),
}); });
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadows_bind_group"), label: Some("shadow_maps_bind_group"),
layout: &self.bgl, layout: &self.bgl,
entries: &[wgpu::BindGroupEntry { entries: &[
binding: 0, wgpu::BindGroupEntry {
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { binding: 0,
buffer: &light_projection_buffer, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
offset: 0, buffer: &light_projection_buffer,
size: None, offset: 0,
}), size: None,
}], }),
}
],
}); });
self.depth_maps.insert( self.depth_maps.insert(
@ -167,8 +164,11 @@ impl ShadowMapsPass {
LightDepthMap { LightDepthMap {
light_projection_buffer: Arc::new(light_projection_buffer), light_projection_buffer: Arc::new(light_projection_buffer),
bindgroup: bg, bindgroup: bg,
atlas_index
}, },
); );
atlas_index
} }
fn transform_buffers(&self) -> AtomicRef<TransformBuffers> { fn transform_buffers(&self) -> AtomicRef<TransformBuffers> {
@ -191,16 +191,18 @@ impl Node for ShadowMapsPass {
) -> crate::render::graph::NodeDesc { ) -> crate::render::graph::NodeDesc {
let mut node = NodeDesc::new(NodeType::Render, None, vec![]); let mut node = NodeDesc::new(NodeType::Render, None, vec![]);
let atlas = self.atlas.get();
node.add_texture_slot( node.add_texture_slot(
ShadowMapsPassSlots::ShadowAtlasTexture, ShadowMapsPassSlots::ShadowAtlasTexture,
SlotAttribute::Output, SlotAttribute::Output,
Some(SlotValue::Texture(self.atlas.texture().clone())), Some(SlotValue::Texture(atlas.texture().clone())),
); );
node.add_texture_view_slot( node.add_texture_view_slot(
ShadowMapsPassSlots::ShadowAtlasTextureView, ShadowMapsPassSlots::ShadowAtlasTextureView,
SlotAttribute::Output, SlotAttribute::Output,
Some(SlotValue::TextureView(self.atlas.view().clone())), Some(SlotValue::TextureView(atlas.view().clone())),
); );
node.add_sampler_slot( node.add_sampler_slot(
@ -210,11 +212,17 @@ impl Node for ShadowMapsPass {
); );
node.add_sampler_slot( node.add_sampler_slot(
ShadowMapsPassSlots::DirLightProjectionBuffer, ShadowMapsPassSlots::ShadowLightUniformsBuffer,
SlotAttribute::Output, SlotAttribute::Output,
Some(SlotValue::Lazy), Some(SlotValue::Lazy),
); );
node.add_buffer_slot(
ShadowMapsPassSlots::ShadowAtlasSizeBuffer,
SlotAttribute::Output,
Some(SlotValue::Buffer(self.atlas_size_buffer.clone())),
);
node node
} }
@ -230,17 +238,29 @@ impl Node for ShadowMapsPass {
world.add_resource(self.atlas.clone()); world.add_resource(self.atlas.clone());
// 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, _) in world.view_iter::<(Entities, &Transform, Has<DirectionalLight>)>() {
if !self.depth_maps.contains_key(&entity) { if !self.depth_maps.contains_key(&entity) {
self.create_depth_map(graph.device(), entity, *pos);
// TODO: dont pack the textures as they're added
let atlas_index = self.create_depth_map(graph.device(), entity, *pos);
index_components_queue.push_back((entity, atlas_index));
debug!("Created depth map for {:?} light entity", entity); debug!("Created depth map for {:?} light entity", entity);
} }
} }
// now consume from the queue adding the components to the entities
while let Some((entity, atlas_id)) = index_components_queue.pop_front() {
world.insert(entity, LightShadowMapId(atlas_id));
}
// update the light projection buffer slot // update the light projection buffer slot
let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap();
let val = graph let val = graph
.slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) .slot_value_mut(ShadowMapsPassSlots::ShadowLightUniformsBuffer)
.unwrap(); .unwrap();
*val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone());
@ -313,11 +333,12 @@ impl Node for ShadowMapsPass {
.expect("missing directional light in scene"); .expect("missing directional light in scene");
{ {
let atlas = self.atlas.get();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("pass_shadow_map"), label: Some("pass_shadow_map"),
color_attachments: &[], color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: self.atlas.view(), view: atlas.view(),
depth_ops: Some(wgpu::Operations { depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0), load: wgpu::LoadOp::Clear(1.0),
store: true, store: true,
@ -326,7 +347,7 @@ impl Node for ShadowMapsPass {
}), }),
}); });
pass.set_pipeline(&pipeline); pass.set_pipeline(&pipeline);
let viewport = self.atlas.texture_viewport(0); let viewport = atlas.texture_viewport(dir_depth_map.atlas_index);
// only render to the light's map in the atlas // only render to the light's map in the atlas
pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0); pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0);
// only clear the light map in the atlas // only clear the light map in the atlas
@ -371,3 +392,39 @@ impl Node for ShadowMapsPass {
} }
} }
} }
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct LightShadowUniform {
space_mat: glam::Mat4,
atlas_frame: AtlasViewport, // 2xUVec2 (4xf32), so no padding needed
}
/// A component that stores the ID of a shadow map in the shadow map atlas for the entities.
///
/// An entity owns a light. If that light casts shadows, this will contain the ID of the shadow
/// map inside of the [`TextureAtlas`].
#[derive(Debug, Default, Copy, Clone, Component)]
pub struct LightShadowMapId(u64);
impl Deref for LightShadowMapId {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// An ecs resource storing the [`TextureAtlas`] of shadow maps.
#[derive(Clone)]
pub struct LightShadowMapAtlas(Arc<RwLock<TextureAtlas>>);
impl LightShadowMapAtlas {
pub fn get(&self) -> RwLockReadGuard<TextureAtlas> {
self.0.read().unwrap()
}
pub fn get_mut(&self) -> RwLockWriteGuard<TextureAtlas> {
self.0.write().unwrap()
}
}

View File

@ -22,6 +22,11 @@ struct VertexOutput {
@location(3) frag_pos_light_space: vec4<f32>, @location(3) frag_pos_light_space: vec4<f32>,
} }
struct TextureAtlasFrame {
offset: vec2<u32>,
size: vec2<u32>,
}
struct TransformData { struct TransformData {
transform: mat4x4<f32>, transform: mat4x4<f32>,
normal_matrix: mat4x4<f32>, normal_matrix: mat4x4<f32>,
@ -82,7 +87,7 @@ fn vs_main(
let normal_mat = mat3x3(normal_mat4[0].xyz, normal_mat4[1].xyz, normal_mat4[2].xyz); let normal_mat = mat3x3(normal_mat4[0].xyz, normal_mat4[1].xyz, normal_mat4[2].xyz);
out.world_normal = normalize(normal_mat * model.normal, ); out.world_normal = normalize(normal_mat * model.normal, );
out.frag_pos_light_space = u_light_space_matrix * world_position; out.frag_pos_light_space = u_light_shadow.light_space_matrix * world_position;
return out; return out;
} }
@ -104,13 +109,10 @@ var t_diffuse: texture_2d<f32>;
@group(0) @binding(2) @group(0) @binding(2)
var s_diffuse: sampler; var s_diffuse: sampler;
/*@group(4) @binding(0) struct LightShadowMapUniform {
var<uniform> u_material: Material; light_space_matrix: mat4x4<f32>,
atlas_frame: TextureAtlasFrame,
@group(5) @binding(0) }
var t_specular: texture_2d<f32>;
@group(5) @binding(1)
var s_specular: sampler;*/
@group(4) @binding(0) @group(4) @binding(0)
var<storage, read_write> u_light_indices: array<u32>; var<storage, read_write> u_light_indices: array<u32>;
@ -122,7 +124,9 @@ var t_shadow_maps_atlas: texture_depth_2d;
@group(5) @binding(1) @group(5) @binding(1)
var s_shadow_maps_atlas: sampler; var s_shadow_maps_atlas: sampler;
@group(5) @binding(2) @group(5) @binding(2)
var<uniform> u_light_space_matrix: mat4x4<f32>; var<uniform> u_shadow_maps_atlas_size: vec2<u32>;
@group(5) @binding(3)
var<uniform> u_light_shadow: LightShadowMapUniform;
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
@ -149,36 +153,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let light: Light = u_lights.data[light_index]; let light: Light = u_lights.data[light_index];
if (light.light_ty == LIGHT_TY_DIRECTIONAL) { if (light.light_ty == LIGHT_TY_DIRECTIONAL) {
/*var proj_coords = in.frag_pos_light_space.xyz / in.frag_pos_light_space.w;
// for some reason the y component is clipped after transforming
proj_coords.y = -proj_coords.y;
// Remap xy to [0.0, 1.0]
let xy_remapped = proj_coords.xy * 0.5 + 0.5;
proj_coords.x = mix(0.0, 1024.0 / 4096.0, xy_remapped.x);
proj_coords.y = mix(0.0, 1024.0 / 4096.0, xy_remapped.y);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2<i32>(0, 0));
let current_depth = proj_coords.z;
// use a bias to avoid shadow acne
let light_dir = normalize(-light.direction); let light_dir = normalize(-light.direction);
let bias = max(0.05 * (1.0 - dot(in.world_normal, light_dir)), 0.005); let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space, u_light_shadow.atlas_frame);
var shadow = 0.0;
if current_depth - bias > closest_depth {
shadow = 1.0;
}
// dont cast shadows outside the light's far plane
if (proj_coords.z > 1.0) {
shadow = 0.0;
}
return vec4<f32>(vec3<f32>(closest_depth), 1.0);*/
let light_dir = normalize(-light.direction);
let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space);
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); light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color);
@ -191,7 +167,7 @@ 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);
} }
fn calc_shadow(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>) -> f32 { fn calc_shadow(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: vec4<f32>, 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 clipped after transforming
proj_coords.y = -proj_coords.y; proj_coords.y = -proj_coords.y;
@ -203,20 +179,24 @@ fn calc_shadow(normal: vec3<f32>, light_dir: vec3<f32>, frag_pos_light_space: ve
// Remap xy to [0.0, 1.0] // Remap xy to [0.0, 1.0]
let xy_remapped = proj_coords.xy * 0.5 + 0.5; let xy_remapped = proj_coords.xy * 0.5 + 0.5;
// TODO: when more lights are added, change the index, and the atlas sizes
let shadow_map_index = 0; // no need to get the y since the maps are square
let shadow_map_region = vec2<f32>( (f32(shadow_map_index) * 1024.0) / 4096.0, (f32(shadow_map_index + 1) * 1024.0) / 4096.0); let atlas_start = f32(atlas_region.offset.x) / f32(u_shadow_maps_atlas_size.x);
let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(u_shadow_maps_atlas_size.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(shadow_map_region.x, shadow_map_region.y, xy_remapped.x); proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x);
proj_coords.y = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.y); proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y);
// simulate `ClampToBorder`, not creating shadows past the shadow map regions // simulate `ClampToBorder`, not creating shadows past the shadow map regions
if (proj_coords.x > shadow_map_region.y && proj_coords.y > shadow_map_region.y) if (proj_coords.x > atlas_end && proj_coords.y > atlas_end)
|| (proj_coords.x < shadow_map_region.x && proj_coords.y < shadow_map_region.x) { || (proj_coords.x < atlas_start && proj_coords.y < atlas_start) {
return 0.0; return 0.0;
} }
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2<i32>(0, 0)); // must manually apply offset to the texture coords since `textureSampleLevel` requires a
// const value.
let offset_coords = proj_coords.xy + (vec2<f32>(atlas_region.offset) / vec2<f32>(u_shadow_maps_atlas_size));
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;
// use a bias to avoid shadow acne // use a bias to avoid shadow acne

View File

@ -3,8 +3,18 @@ struct TransformData {
normal_matrix: mat4x4<f32>, normal_matrix: mat4x4<f32>,
} }
struct TextureAtlasFrame {
offset: vec2<u32>,
size: vec2<u32>,
}
struct LightShadowMapUniform {
light_space_matrix: mat4x4<f32>,
atlas_frame: TextureAtlasFrame,
}
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> u_light_space_matrix: mat4x4<f32>; var<uniform> u_light_shadow: LightShadowMapUniform;
@group(1) @binding(0) @group(1) @binding(0)
var<uniform> u_model_transform_data: TransformData; var<uniform> u_model_transform_data: TransformData;
@ -18,6 +28,6 @@ struct VertexOutput {
fn vs_main( fn vs_main(
@location(0) position: vec3<f32> @location(0) position: vec3<f32>
) -> VertexOutput { ) -> VertexOutput {
let pos = u_light_space_matrix * u_model_transform_data.transform * vec4<f32>(position, 1.0); let pos = u_light_shadow.light_space_matrix * u_model_transform_data.transform * vec4<f32>(position, 1.0);
return VertexOutput(pos); return VertexOutput(pos);
} }

View File

@ -1,31 +1,54 @@
use std::sync::Arc; use std::{
collections::BTreeMap,
sync::Arc,
};
use glam::UVec2; use glam::UVec2;
use rectangle_pack::{pack_rects, GroupedRectsToPlace, RectToInsert, RectanglePackOk, TargetBin};
#[derive(Debug, Clone, Copy)] #[derive(Debug, thiserror::Error)]
pub enum AtlasPackError {
/// The rectangles can't be placed into the atlas. The atlas must increase in size
#[error("There is not enough space in the atlas for the textures")]
NotEnoughSpace,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AtlasViewport { pub struct AtlasViewport {
pub offset: UVec2, pub offset: UVec2,
pub size: UVec2, pub size: UVec2,
} }
pub struct TextureAtlas { pub struct TextureAtlas {
/// The size of each texture in the atlas. atlas_size: UVec2,
texture_size: UVec2,
/// The amount of textures in the atlas.
texture_count: UVec2,
texture_format: wgpu::TextureFormat, texture_format: wgpu::TextureFormat,
texture: Arc<wgpu::Texture>, texture: Arc<wgpu::Texture>,
view: Arc<wgpu::TextureView>, view: Arc<wgpu::TextureView>,
/// The next id of the next texture that will be added to the atlas.
next_texture_id: u64,
rects: GroupedRectsToPlace<u64>,
bins: BTreeMap<u64, TargetBin>,
placement: Option<RectanglePackOk<u64, u64>>,
} }
impl TextureAtlas { impl TextureAtlas {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, usages: wgpu::TextureUsages, texture_size: UVec2, texture_count: UVec2) -> Self { pub fn new(
let total_size = texture_size * texture_count; device: &wgpu::Device,
format: wgpu::TextureFormat,
usages: wgpu::TextureUsages,
atlas_size: UVec2,
) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("texture_atlas"), label: Some("texture_atlas"),
size: wgpu::Extent3d { width: total_size.x, height: total_size.y, depth_or_array_layers: 1 }, size: wgpu::Extent3d {
width: atlas_size.x,
height: atlas_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
@ -35,21 +58,98 @@ impl TextureAtlas {
}); });
let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut bins = BTreeMap::new();
// max_depth=1 for 2d
bins.insert(0, TargetBin::new(atlas_size.x, atlas_size.y, 1));
Self { Self {
texture_size, atlas_size,
texture_count,
texture_format: format, texture_format: format,
texture: Arc::new(texture), texture: Arc::new(texture),
view: Arc::new(view), view: Arc::new(view),
next_texture_id: 0,
rects: GroupedRectsToPlace::new(),
bins,
placement: None,
} }
} }
/// Get the viewport of a texture index in the atlas. /// Add a texture of `size` and pack it into the atlas, returning the id of the texture in
pub fn texture_viewport(&self, atlas_index: u32) -> AtlasViewport { /// the atlas.
let x = (atlas_index % self.texture_count.x) * self.texture_size.x; ///
let y = (atlas_index / self.texture_count.y) * self.texture_size.y; /// If you are adding multiple textures at a time and want to wait to pack the atlas, use
/// [`TextureAtlas::add_texture_unpacked`] and then after you're done adding them, pack them
/// with [`TextureAtlas::pack_atlas`].
pub fn pack_new_texture(&mut self, width: u32, height: u32) -> Result<u64, AtlasPackError> {
let id = self.next_texture_id;
self.next_texture_id += 1;
AtlasViewport { offset: UVec2::new(x, y), size: self.texture_size } // for 2d rects, set depth to 1
let r = RectToInsert::new(width, height, 1);
self.rects.push_rect(id, None, r);
self.pack_atlas()?;
Ok(id)
}
/// Add a new texture and **DO NOT** pack it into the atlas.
///
/// <div class="warning">
///
/// The texture will not be packed into the atlas meaning
/// [`TextureAtlas::texture_viewport`] will return `None`. To pack the texture,
/// use [`TextureAtlas::pack_atlas`] or use [`TextureAtlas::pack_new_texture`]
/// when only adding a single texture.
///
/// </div>
pub fn add_texture_unpacked(&mut self, width: u32, height: u32) -> Result<u64, AtlasPackError> {
let id = self.next_texture_id;
self.next_texture_id += 1;
// for 2d rects, set depth to 1
let r = RectToInsert::new(width, height, 1);
self.rects.push_rect(id, None, r);
self.pack_atlas()?;
Ok(id)
}
/// Pack the textures into the atlas.
pub fn pack_atlas(&mut self) -> Result<(), AtlasPackError> {
let placement = pack_rects(
&self.rects,
&mut self.bins,
&rectangle_pack::volume_heuristic,
&rectangle_pack::contains_smallest_box,
)
.map_err(|e| match e {
rectangle_pack::RectanglePackError::NotEnoughBinSpace => AtlasPackError::NotEnoughSpace,
})?;
self.placement = Some(placement);
Ok(())
}
/// Get the viewport of a texture index in the atlas.
pub fn texture_viewport(&self, atlas_index: u64) -> AtlasViewport {
let locations = self.placement.as_ref().unwrap().packed_locations();
let (bin_id, loc) = locations
.get(&atlas_index)
.expect("atlas index is incorrect");
debug_assert_eq!(*bin_id, 0, "somehow the texture was put in some other bin");
AtlasViewport {
offset: UVec2 {
x: loc.x(),
y: loc.y(),
},
size: UVec2 {
x: loc.width(),
y: loc.height(),
},
}
} }
pub fn view(&self) -> &Arc<wgpu::TextureView> { pub fn view(&self) -> &Arc<wgpu::TextureView> {
@ -64,15 +164,12 @@ impl TextureAtlas {
&self.texture_format &self.texture_format
} }
pub fn texture_size(&self) -> UVec2 { pub fn total_texture_count(&self) -> u64 {
self.texture_size self.next_texture_id // starts at zero, so no need to increment
} }
pub fn texture_count(&self) -> UVec2 { /// Returns the size of the entire texture atlas.
self.texture_count pub fn atlas_size(&self) -> UVec2 {
self.atlas_size
} }
}
pub fn total_texture_count(&self) -> u32 {
self.texture_count.x * self.texture_count.y
}
}