diff --git a/Cargo.lock b/Cargo.lock index c5c7786..b453320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,6 +1881,7 @@ dependencies = [ "lyra-scene", "petgraph", "quote", + "rectangle-pack", "rustc-hash", "syn 2.0.51", "thiserror", @@ -2767,6 +2768,12 @@ dependencies = [ "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]] name = "redox_syscall" version = "0.3.5" diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 26cc050..5420015 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -38,6 +38,7 @@ unique = "0.9.1" rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" +rectangle-pack = "0.4.2" [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 61b5c05..b00df97 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -116,9 +116,14 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .as_sampler() .unwrap(); - let dir_light_projection_buf = graph - .slot_value(ShadowMapsPassSlots::DirLightProjectionBuffer) - .expect("missing ShadowMapsPassSlots::DirLightProjectionBuffer") + let atlas_size_buf = graph + .slot_value(ShadowMapsPassSlots::ShadowAtlasSizeBuffer) + .expect("missing ShadowMapsPassSlots::ShadowAtlasSizeBuffer") + .as_buffer() + .unwrap(); + let light_uniform_buf = graph + .slot_value(ShadowMapsPassSlots::ShadowLightUniformsBuffer) + .expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer") .as_buffer() .unwrap(); @@ -151,6 +156,16 @@ impl Node for MeshPass { }, 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 { binding: 2, 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, size: None, }), diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 8dab5cf..b4de5fb 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -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::{ - query::{filter::Has, Entities}, - AtomicRef, Entity, ResourceData, + query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData }; use lyra_game_derive::RenderGraphLabel; use lyra_math::Transform; @@ -12,24 +10,20 @@ use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::directional::DirectionalLight, - resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, - transform_buffer_storage::TransformBuffers, - vertex::Vertex, - TextureAtlas, + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasViewport, TextureAtlas }; 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)] pub enum ShadowMapsPassSlots { ShadowAtlasTexture, ShadowAtlasTextureView, ShadowAtlasSampler, - DirLightProjectionBuffer, + ShadowAtlasSizeBuffer, + ShadowLightUniformsBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -38,10 +32,12 @@ pub struct ShadowMapsPassLabel; struct LightDepthMap { light_projection_buffer: Arc, bindgroup: wgpu::BindGroup, + atlas_index: u64, } pub struct ShadowMapsPass { bgl: Arc, + atlas_size_buffer: Arc, /// depth maps for a light owned by an entity. depth_maps: FxHashMap, @@ -52,7 +48,7 @@ pub struct ShadowMapsPass { mesh_buffers: Option, pipeline: Option, - atlas: Arc, + atlas: LightShadowMapAtlas, /// The depth map atlas sampler atlas_sampler: Rc, } @@ -61,50 +57,38 @@ impl ShadowMapsPass { pub fn new(device: &wgpu::Device) -> Self { let bgl = Arc::new( device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("bgl_shadows_light_projection"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some( - NonZeroU64::new(mem::size_of::() as _).unwrap(), - ), - }, - count: None, - }], + label: Some("bgl_shadow_maps_lights"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + NonZeroU64::new(mem::size_of::() as _).unwrap(), + ), + }, + 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( device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE, - UVec2::new(4, 4), + SHADOW_SIZE * 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 { label: Some("sampler_shadow_map_atlas"), address_mode_u: wgpu::AddressMode::ClampToBorder, @@ -119,6 +103,7 @@ impl ShadowMapsPass { Self { bgl, + atlas_size_buffer: Arc::new(atlas_size_buffer), depth_maps: Default::default(), transform_buffers: None, render_meshes: None, @@ -126,14 +111,20 @@ impl ShadowMapsPass { pipeline: None, 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 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 = 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()); let light_proj = ortho_proj * look_view; + let uniform = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + }; let light_projection_buffer = 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, - contents: bytemuck::bytes_of(&light_proj), + contents: bytemuck::bytes_of(&uniform), }); let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("shadows_bind_group"), + label: Some("shadow_maps_bind_group"), layout: &self.bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &light_projection_buffer, - offset: 0, - size: None, - }), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &light_projection_buffer, + offset: 0, + size: None, + }), + } + ], }); self.depth_maps.insert( @@ -167,8 +164,11 @@ impl ShadowMapsPass { LightDepthMap { light_projection_buffer: Arc::new(light_projection_buffer), bindgroup: bg, + atlas_index }, ); + + atlas_index } fn transform_buffers(&self) -> AtomicRef { @@ -191,16 +191,18 @@ impl Node for ShadowMapsPass { ) -> crate::render::graph::NodeDesc { let mut node = NodeDesc::new(NodeType::Render, None, vec![]); + let atlas = self.atlas.get(); + node.add_texture_slot( ShadowMapsPassSlots::ShadowAtlasTexture, SlotAttribute::Output, - Some(SlotValue::Texture(self.atlas.texture().clone())), + Some(SlotValue::Texture(atlas.texture().clone())), ); node.add_texture_view_slot( ShadowMapsPassSlots::ShadowAtlasTextureView, SlotAttribute::Output, - Some(SlotValue::TextureView(self.atlas.view().clone())), + Some(SlotValue::TextureView(atlas.view().clone())), ); node.add_sampler_slot( @@ -210,11 +212,17 @@ impl Node for ShadowMapsPass { ); node.add_sampler_slot( - ShadowMapsPassSlots::DirLightProjectionBuffer, + ShadowMapsPassSlots::ShadowLightUniformsBuffer, SlotAttribute::Output, Some(SlotValue::Lazy), ); + node.add_buffer_slot( + ShadowMapsPassSlots::ShadowAtlasSizeBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), + ); + node } @@ -230,17 +238,29 @@ impl Node for ShadowMapsPass { 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)>() { 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); } } + + // 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 let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); let val = graph - .slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) + .slot_value_mut(ShadowMapsPassSlots::ShadowLightUniformsBuffer) .unwrap(); *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); @@ -313,11 +333,12 @@ impl Node for ShadowMapsPass { .expect("missing directional light in scene"); { + let atlas = self.atlas.get(); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("pass_shadow_map"), color_attachments: &[], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: self.atlas.view(), + view: atlas.view(), depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: true, @@ -326,7 +347,7 @@ impl Node for ShadowMapsPass { }), }); 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 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 @@ -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>); + +impl LightShadowMapAtlas { + pub fn get(&self) -> RwLockReadGuard { + self.0.read().unwrap() + } + + pub fn get_mut(&self) -> RwLockWriteGuard { + self.0.write().unwrap() + } +} \ No newline at end of file diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index eb3c321..8d3bb19 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -22,6 +22,11 @@ struct VertexOutput { @location(3) frag_pos_light_space: vec4, } +struct TextureAtlasFrame { + offset: vec2, + size: vec2, +} + struct TransformData { transform: mat4x4, normal_matrix: mat4x4, @@ -82,7 +87,7 @@ fn vs_main( 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.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; } @@ -104,13 +109,10 @@ var t_diffuse: texture_2d; @group(0) @binding(2) var s_diffuse: sampler; -/*@group(4) @binding(0) -var u_material: Material; - -@group(5) @binding(0) -var t_specular: texture_2d; -@group(5) @binding(1) -var s_specular: sampler;*/ +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, +} @group(4) @binding(0) var u_light_indices: array; @@ -122,7 +124,9 @@ var t_shadow_maps_atlas: texture_depth_2d; @group(5) @binding(1) var s_shadow_maps_atlas: sampler; @group(5) @binding(2) -var u_light_space_matrix: mat4x4; +var u_shadow_maps_atlas_size: vec2; +@group(5) @binding(3) +var u_light_shadow: LightShadowMapUniform; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -149,36 +153,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light: Light = u_lights.data[light_index]; 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(0, 0)); - let current_depth = proj_coords.z; - - // use a bias to avoid shadow acne let light_dir = normalize(-light.direction); - let bias = max(0.05 * (1.0 - dot(in.world_normal, light_dir)), 0.005); - 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(vec3(closest_depth), 1.0);*/ - - - let light_dir = normalize(-light.direction); - let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space); + let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space, u_light_shadow.atlas_frame); 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) { 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 { return vec4(light_object_res, object_color.a); } -fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4) -> f32 { +fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_region: TextureAtlasFrame) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is clipped after transforming proj_coords.y = -proj_coords.y; @@ -203,20 +179,24 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve // Remap xy to [0.0, 1.0] 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; - let shadow_map_region = vec2( (f32(shadow_map_index) * 1024.0) / 4096.0, (f32(shadow_map_index + 1) * 1024.0) / 4096.0); + + // no need to get the y since the maps are square + 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. - proj_coords.x = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.x); - proj_coords.y = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.y); + proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x); + proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y); // 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) - || (proj_coords.x < shadow_map_region.x && proj_coords.y < shadow_map_region.x) { + if (proj_coords.x > atlas_end && proj_coords.y > atlas_end) + || (proj_coords.x < atlas_start && proj_coords.y < atlas_start) { return 0.0; } - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2(0, 0)); + // must manually apply offset to the texture coords since `textureSampleLevel` requires a + // const value. + let offset_coords = proj_coords.xy + (vec2(atlas_region.offset) / vec2(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; // use a bias to avoid shadow acne diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index fa2291c..5f1a0c5 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -3,8 +3,18 @@ struct TransformData { normal_matrix: mat4x4, } +struct TextureAtlasFrame { + offset: vec2, + size: vec2, +} + +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, +} + @group(0) @binding(0) -var u_light_space_matrix: mat4x4; +var u_light_shadow: LightShadowMapUniform; @group(1) @binding(0) var u_model_transform_data: TransformData; @@ -18,6 +28,6 @@ struct VertexOutput { fn vs_main( @location(0) position: vec3 ) -> VertexOutput { - let pos = u_light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); + let pos = u_light_shadow.light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); return VertexOutput(pos); } \ No newline at end of file diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index 6808d74..65b0989 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -1,31 +1,54 @@ -use std::sync::Arc; +use std::{ + collections::BTreeMap, + sync::Arc, +}; 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 offset: UVec2, pub size: UVec2, } pub struct TextureAtlas { - /// The size of each texture in the atlas. - texture_size: UVec2, - /// The amount of textures in the atlas. - texture_count: UVec2, + atlas_size: UVec2, texture_format: wgpu::TextureFormat, texture: Arc, view: Arc, + + /// The next id of the next texture that will be added to the atlas. + next_texture_id: u64, + + rects: GroupedRectsToPlace, + bins: BTreeMap, + placement: Option>, } impl TextureAtlas { - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, usages: wgpu::TextureUsages, texture_size: UVec2, texture_count: UVec2) -> Self { - let total_size = texture_size * texture_count; - + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + usages: wgpu::TextureUsages, + atlas_size: UVec2, + ) -> Self { let texture = device.create_texture(&wgpu::TextureDescriptor { 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, sample_count: 1, dimension: wgpu::TextureDimension::D2, @@ -35,21 +58,98 @@ impl TextureAtlas { }); 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 { - texture_size, - texture_count, + atlas_size, texture_format: format, texture: Arc::new(texture), view: Arc::new(view), + next_texture_id: 0, + rects: GroupedRectsToPlace::new(), + bins, + placement: None, } } - /// Get the viewport of a texture index in the atlas. - pub fn texture_viewport(&self, atlas_index: u32) -> AtlasViewport { - let x = (atlas_index % self.texture_count.x) * self.texture_size.x; - let y = (atlas_index / self.texture_count.y) * self.texture_size.y; + /// Add a texture of `size` and pack it into the atlas, returning the id of the texture in + /// the atlas. + /// + /// 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 { + 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. + /// + ///
+ /// + /// 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. + /// + ///
+ pub fn add_texture_unpacked(&mut self, width: u32, height: u32) -> Result { + 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 { @@ -64,15 +164,12 @@ impl TextureAtlas { &self.texture_format } - pub fn texture_size(&self) -> UVec2 { - self.texture_size + pub fn total_texture_count(&self) -> u64 { + self.next_texture_id // starts at zero, so no need to increment } - pub fn texture_count(&self) -> UVec2 { - self.texture_count + /// Returns the size of the entire texture atlas. + pub fn atlas_size(&self) -> UVec2 { + self.atlas_size } - - pub fn total_texture_count(&self) -> u32 { - self.texture_count.x * self.texture_count.y - } -} \ No newline at end of file +}