From 4018fdaa8082aa2b4f1bd32c350fa915476153f6 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 20 Nov 2024 17:29:52 -0500 Subject: [PATCH] implement texture atlases for sprites, allow storage of assets not from a loader --- Cargo.lock | 5 +- crates/lyra-game/src/render/graph/mod.rs | 11 +- .../src/render/graph/passes/shadows.rs | 10 +- .../src/render/graph/passes/sprite.rs | 204 +++++++++++++----- .../src/render/shaders/2d/sprite_main.wgsl | 27 ++- crates/lyra-game/src/render/texture_atlas.rs | 4 +- crates/lyra-game/src/sprite/mod.rs | 3 + crates/lyra-game/src/sprite/texture_atlas.rs | 105 +++++++++ crates/lyra-math/Cargo.toml | 3 +- crates/lyra-math/src/i32/mod.rs | 2 + crates/lyra-math/src/i32/rect.rs | 90 ++++++++ crates/lyra-math/src/lib.rs | 8 + crates/lyra-math/src/rect.rs | 9 +- crates/lyra-math/src/u32/mod.rs | 2 + crates/lyra-math/src/u32/rect.rs | 90 ++++++++ crates/lyra-reflect/src/impls/impl_math.rs | 10 + crates/lyra-resource/src/resource_manager.rs | 8 + examples/2d/src/main.rs | 77 +++++-- .../assets/tiny_rpg_characters/.gitignore | 3 + .../assets/tiny_rpg_characters/source.txt | 1 + 20 files changed, 582 insertions(+), 90 deletions(-) create mode 100644 crates/lyra-game/src/sprite/texture_atlas.rs create mode 100644 crates/lyra-math/src/i32/mod.rs create mode 100644 crates/lyra-math/src/i32/rect.rs create mode 100644 crates/lyra-math/src/u32/mod.rs create mode 100644 crates/lyra-math/src/u32/rect.rs create mode 100644 examples/assets/tiny_rpg_characters/.gitignore create mode 100644 examples/assets/tiny_rpg_characters/source.txt diff --git a/Cargo.lock b/Cargo.lock index 375cd4b..e456e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,9 +515,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] @@ -1893,6 +1893,7 @@ dependencies = [ name = "lyra-math" version = "0.1.0" dependencies = [ + "bytemuck", "glam", ] diff --git a/crates/lyra-game/src/render/graph/mod.rs b/crates/lyra-game/src/render/graph/mod.rs index b8d1f4a..239f4a1 100644 --- a/crates/lyra-game/src/render/graph/mod.rs +++ b/crates/lyra-game/src/render/graph/mod.rs @@ -21,7 +21,7 @@ pub use render_target::*; use rustc_hash::FxHashMap; use tracing::{debug_span, instrument, trace, warn}; -use wgpu::CommandEncoder; +use wgpu::{util::DeviceExt, BufferUsages, CommandEncoder}; use super::{resource::{ComputePipeline, Pass, Pipeline, RenderPipeline}, Shader}; @@ -543,6 +543,15 @@ impl RenderGraph { shader.wait_for_load()?; Ok(shader) } + + /// Create a buffer with a single item inside of it + pub fn create_buffer_with_data(&self, label: Option<&'static str>, usage: BufferUsages, data: &T) -> wgpu::Buffer { + self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label, + usage, + contents: bytemuck::bytes_of(data), + }) + } } pub struct SubGraphNode { diff --git a/crates/lyra-game/src/render/graph/passes/shadows.rs b/crates/lyra-game/src/render/graph/passes/shadows.rs index e2d5c60..8d031ab 100644 --- a/crates/lyra-game/src/render/graph/passes/shadows.rs +++ b/crates/lyra-game/src/render/graph/passes/shadows.rs @@ -20,7 +20,7 @@ use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, RenderGraph, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, Shader, TextureAtlas + graph::{Node, NodeDesc, NodeType, RenderGraph, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, Shader, PackedTextureAtlas }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; @@ -98,7 +98,7 @@ impl ShadowMapsPass { }), ); - let atlas = TextureAtlas::new( + let atlas = PackedTextureAtlas::new( device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, @@ -1062,14 +1062,14 @@ impl LightShadowMapId { /// An ecs resource storing the [`TextureAtlas`] of shadow maps. #[derive(Clone)] -pub struct LightShadowMapAtlas(Arc>); +pub struct LightShadowMapAtlas(Arc>); impl LightShadowMapAtlas { - pub fn get(&self) -> RwLockReadGuard { + pub fn get(&self) -> RwLockReadGuard { self.0.read().unwrap() } - pub fn get_mut(&self) -> RwLockWriteGuard { + pub fn get_mut(&self) -> RwLockWriteGuard { self.0.write().unwrap() } } diff --git a/crates/lyra-game/src/render/graph/passes/sprite.rs b/crates/lyra-game/src/render/graph/passes/sprite.rs index acaa9fe..b853aac 100644 --- a/crates/lyra-game/src/render/graph/passes/sprite.rs +++ b/crates/lyra-game/src/render/graph/passes/sprite.rs @@ -1,35 +1,39 @@ -use std::{ - collections::VecDeque, - sync::Arc, -}; +use std::{collections::VecDeque, sync::Arc}; -use glam::{Vec2, Vec3}; +use glam::{UVec2, Vec2, Vec3}; use image::GenericImageView; use lyra_ecs::{ - query::{Entities, ResMut}, AtomicRef, ResourceData + query::{filter::Or, Entities, ResMut, TickOf}, AtomicRef, Entity, ResourceData }; use lyra_game_derive::RenderGraphLabel; +use lyra_math::URect; use lyra_resource::Image; -use tracing::{info, instrument, warn}; +use rustc_hash::FxHashMap; +use tracing::{debug, instrument, warn}; use uuid::Uuid; use wgpu::util::DeviceExt; use crate::{ render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute}, - render_job::RenderJob, - resource::{ - FragmentState, RenderPipeline, RenderPipelineDescriptor, - VertexState, - }, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState}, transform_buffer_storage::{TransformBuffers, TransformIndex}, vertex::Vertex2D, }, - sprite::Sprite, + sprite::{AtlasSprite, Sprite}, }; use super::{BasePassSlots, RenderAssets}; +#[derive(Clone)] +pub struct RenderJob { + pub entity: Entity, + pub shader_id: u64, + pub asset_uuid: uuid::Uuid, + pub transform_id: TransformIndex, + pub atlas_frame_id: u64, +} + #[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)] pub struct SpritePassLabel; @@ -48,7 +52,9 @@ struct SpriteTexture { #[allow(dead_code)] sampler: wgpu::Sampler, texture_bg: Arc, +} +struct SpriteBuffers { vertex_buffers: wgpu::Buffer, index_buffers: wgpu::Buffer, } @@ -58,9 +64,13 @@ pub struct SpritePass { pipeline: Option, texture_bgl: Option>, jobs: VecDeque, - + /// Buffer that stores a `Rect` with `min` and `max` set to zero. + /// This can be used for sprites that are not from an atlas. + atlas_frames_buf: Option>, + transform_buffers: Option, - sprite_textures: Option, + texture_store: Option, + buffer_store: Option, } impl SpritePass { @@ -68,32 +78,22 @@ impl SpritePass { Self::default() } - #[instrument(skip(self, device, sprite))] + #[instrument(skip(self, device))] fn create_vertex_index_buffers( &mut self, device: &wgpu::Device, - sprite: &Sprite, + dimensions: UVec2, ) -> (wgpu::Buffer, wgpu::Buffer) { - let tex_dims = sprite - .texture - .data_ref() - .map(|t| t.dimensions()); - if tex_dims.is_none() { - info!("Sprite texture is not loaded, not rendering it until it is!"); - todo!("Wait until texture is loaded"); - } - let tex_dims = tex_dims.unwrap(); - let vertices = vec![ // top left Vertex2D::new(Vec3::new(0.0, 0.0, 0.0), Vec2::new(0.0, 1.0)), // bottom left - Vertex2D::new(Vec3::new(0.0, tex_dims.1 as f32, 0.0), Vec2::new(0.0, 0.0)), + Vertex2D::new(Vec3::new(0.0, dimensions.y as f32, 0.0), Vec2::new(0.0, 0.0)), // top right - Vertex2D::new(Vec3::new(tex_dims.0 as f32, 0.0, 0.0), Vec2::new(1.0, 1.0)), + Vertex2D::new(Vec3::new(dimensions.x as f32, 0.0, 0.0), Vec2::new(1.0, 1.0)), // bottom right Vertex2D::new( - Vec3::new(tex_dims.0 as f32, tex_dims.1 as f32, 0.0), + Vec3::new(dimensions.x as f32, dimensions.y as f32, 0.0), Vec2::new(1.0, 0.0), ), ]; @@ -109,7 +109,7 @@ impl SpritePass { //0, 2, 3 3, 1, 0, // second tri 0, 2, 3, // first tri - //0, 2, 3, // second tri + //0, 2, 3, // second tri ]; let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Index Buffer"), @@ -175,6 +175,7 @@ impl SpritePass { }, ); + let frames = self.atlas_frames_buf.as_ref().unwrap(); let bgl = self.texture_bgl.as_ref().unwrap(); let tex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some(&format!("sprite_texture_bg_{}", uuid_str)), @@ -188,6 +189,14 @@ impl SpritePass { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &frames, + offset: 0, + size: None, + }), + }, ], }); @@ -202,6 +211,17 @@ impl Node for SpritePass { ) -> crate::render::graph::NodeDesc { let device = &graph.device; + let atlas_frames = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("default_sprite_atlas_frame"), + size: std::mem::size_of::() as u64 * 1000, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + // write the rect for sprites that aren't part of the texture atlas. + graph + .queue + .write_buffer(&atlas_frames, 0, bytemuck::bytes_of(&URect::ZERO)); + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bgl_sprite_main"), entries: &[ @@ -221,9 +241,20 @@ impl Node for SpritePass { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); self.texture_bgl = Some(Arc::new(bgl)); + self.atlas_frames_buf = Some(Arc::new(atlas_frames)); let mut desc = NodeDesc::new(NodeType::Render, None, vec![]); @@ -242,7 +273,11 @@ impl Node for SpritePass { let vt = graph.view_target(); if self.pipeline.is_none() { - let shader = graph.load_shader_str("sprite_shader", include_str!("../../shaders/2d/sprite_main.wgsl")) + let shader = graph + .load_shader_str( + "sprite_shader", + include_str!("../../shaders/2d/sprite_main.wgsl"), + ) .expect("failed to load wgsl shader from manager"); let diffuse_bgl = self.texture_bgl.clone().unwrap(); @@ -284,10 +319,14 @@ impl Node for SpritePass { drop(transforms); world.add_resource_default_if_absent::>(); - let sprite_textures = world - .get_resource_data::>() - .expect("Missing sprite texture store"); - self.sprite_textures = Some(sprite_textures.clone()); + let texture_store = world + .get_resource_data::>().unwrap(); + self.texture_store = Some(texture_store.clone()); + + world.add_resource_default_if_absent::>(); + let buffer_store = world + .get_resource_data::>().unwrap(); + self.buffer_store = Some(buffer_store.clone()); let transforms = world .get_resource_data::() @@ -295,46 +334,88 @@ impl Node for SpritePass { self.transform_buffers = Some(transforms.clone()); } + let mut v = Vec::with_capacity(500); + + let world_tick = world.current_tick(); let queue = &graph.queue; - for (entity, sprite, transform_idx, mut sprite_store) in world + for (entity, (sprite, atlas_sprite), transform_idx, mut texture_store, mut buffer_store) in world .view::<( Entities, - &Sprite, + Or<&Sprite, (&AtlasSprite, TickOf)>, &TransformIndex, ResMut>, + ResMut>, )>() .iter() { - if let Some(image) = sprite.texture.data_ref() { - let texture_uuid = sprite.texture.uuid(); - if !sprite_store.contains_key(&texture_uuid) { + let tex = if let Some(sprite) = &sprite { + sprite.texture.clone()//.data_ref() + } else if let Some((a, _)) = &atlas_sprite { + a.atlas.data_ref() + .unwrap().texture.clone() + } else { continue; }; + let rect = atlas_sprite.as_ref().map(|(a, _)| a.sprite); + + if let Some(image) = tex.data_ref() { + let texture_uuid = tex.uuid(); + if !texture_store.contains_key(&texture_uuid) { // returns `None` if the Texture image is not loaded. if let Some((texture, sampler, tex_bg)) = self.load_sprite_texture(device, queue, &texture_uuid, &image) { - let (vertex, index) = self.create_vertex_index_buffers(device, &sprite); - - sprite_store.insert( + texture_store.insert( texture_uuid, SpriteTexture { texture, sampler, texture_bg: Arc::new(tex_bg), - vertex_buffers: vertex, - index_buffers: index, }, ); } } + if !buffer_store.contains_key(&entity) { + let dim = rect.unwrap_or_else(|| { + let i = image.dimensions(); + URect::new(0, 0, i.0, i.1) + }); + debug!("storing rect: {dim:?}"); + let dim = dim.dimensions(); + let (vertex, index) = self.create_vertex_index_buffers(device, dim); + buffer_store.insert(entity, SpriteBuffers { vertex_buffers: vertex, index_buffers: index }); + } else if let Some((ats, tick)) = &atlas_sprite { + // detect a change for the vertex and index buffers of the sprite + if tick.checked_sub(1).unwrap_or(0) >= *world_tick { + debug!("Updating buffer for entity after change detected in atlas sprite"); + let dim = ats.sprite.dimensions(); + let (vertex, index) = self.create_vertex_index_buffers(device, dim); + buffer_store.insert(entity, SpriteBuffers { vertex_buffers: vertex, index_buffers: index }); + } + } + + let frame_id = match rect { + Some(r) => { + v.push(r); + // No -1 here since the gpu buffer is already offset by 1 + // to store the default rect at the front + v.len() as u64 + } + None => 0 + }; + self.jobs.push_back(RenderJob { entity, shader_id: 0, asset_uuid: texture_uuid, transform_id: *transform_idx, + atlas_frame_id: frame_id, }); - } + }; } + + let buf = self.atlas_frames_buf.as_ref().unwrap(); + // skip default rect + queue.write_buffer(buf, std::mem::size_of::() as _, bytemuck::cast_slice(&v)); } fn execute( @@ -345,8 +426,10 @@ impl Node for SpritePass { ) { let pipeline = self.pipeline.as_ref().unwrap(); - let sprite_store = self.sprite_textures.clone().unwrap(); - let sprite_store: AtomicRef> = sprite_store.get(); + let texture_store = self.texture_store.clone().unwrap(); + let texture_store: AtomicRef> = texture_store.get(); + let buffer_store = self.buffer_store.clone().unwrap(); + let buffer_store: AtomicRef> = buffer_store.get(); let transforms = self.transform_buffers.clone().unwrap(); let transforms: AtomicRef = transforms.get(); @@ -379,10 +462,11 @@ impl Node for SpritePass { pass.set_pipeline(pipeline); while let Some(job) = self.jobs.pop_front() { - let sprite = sprite_store.get(&job.asset_uuid) + // bind texture + let tex = texture_store + .get(&job.asset_uuid) .expect("failed to find SpriteTexture for job asset_uuid"); - - pass.set_bind_group(0, &sprite.texture_bg, &[]); + pass.set_bind_group(0, &tex.texture_bg, &[]); // Get the bindgroup for job's transform and bind to it using an offset. let bindgroup = transforms.bind_group(job.transform_id); @@ -391,12 +475,16 @@ impl Node for SpritePass { pass.set_bind_group(2, camera_bg, &[]); - pass.set_vertex_buffer( - 0, - sprite.vertex_buffers.slice(..), - ); - pass.set_index_buffer(sprite.index_buffers.slice(..), wgpu::IndexFormat::Uint32); - pass.draw_indexed(0..6, 0, 0..1); + // set vertex and index buffers + let bufs = buffer_store + .get(&job.entity) + .expect("failed to find buffers for job entity"); + pass.set_vertex_buffer(0, bufs.vertex_buffers.slice(..)); + pass.set_index_buffer(bufs.index_buffers.slice(..), wgpu::IndexFormat::Uint32); + + // use the atlas frame id as the instance + let inst = job.atlas_frame_id as u32; + pass.draw_indexed(0..6, 0, inst..inst + 1); } } } diff --git a/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl b/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl index d091011..ba17363 100644 --- a/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl +++ b/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl @@ -3,12 +3,14 @@ const ALPHA_CUTOFF = 0.1; struct VertexInput { @location(0) position: vec3, @location(1) tex_coords: vec2, + @builtin(instance_index) instance_index: u32, } struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) tex_coords: vec2, @location(1) world_position: vec3, + @location(2) instance_index: u32 } struct TransformData { @@ -16,6 +18,11 @@ struct TransformData { normal_matrix: mat4x4, } +struct URect { + min: vec2, + max: vec2, +} + struct CameraUniform { view: mat4x4, inverse_projection: mat4x4, @@ -40,6 +47,7 @@ fn vs_main( out.world_position = world_position.xyz; out.tex_coords = in.tex_coords; out.clip_position = u_camera.view_projection * world_position; + out.instance_index = in.instance_index; return out; } @@ -49,9 +57,26 @@ var t_diffuse: texture_2d; @group(0) @binding(1) var s_diffuse: sampler; +@group(0) @binding(2) +var u_sprite_atlas_frames: array; + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let object_color: vec4 = textureSample(t_diffuse, s_diffuse, in.tex_coords); + let frame = u_sprite_atlas_frames[in.instance_index]; + var region_coords = in.tex_coords; + + if (frame.min.x != 0 || frame.min.y != 0 || frame.max.x != 0 || frame.max.y != 0) { + let dim = vec2(textureDimensions(t_diffuse)); + // convert tex coords to frame + region_coords = vec2( + mix(f32(frame.min.x), f32(frame.max.x), in.tex_coords.x), + mix(f32(frame.min.y), f32(frame.max.y), in.tex_coords.y) + ); + // convert frame coords to texture coords + region_coords /= dim; + } + + let object_color: vec4 = textureSample(t_diffuse, s_diffuse, region_coords); if (object_color.a < ALPHA_CUTOFF) { discard; } diff --git a/crates/lyra-game/src/render/texture_atlas.rs b/crates/lyra-game/src/render/texture_atlas.rs index 3dec36e..b8fbf1f 100644 --- a/crates/lyra-game/src/render/texture_atlas.rs +++ b/crates/lyra-game/src/render/texture_atlas.rs @@ -28,7 +28,7 @@ impl AtlasFrame { } } -pub struct TextureAtlas { +pub struct PackedTextureAtlas { atlas_size: UVec2, texture_format: wgpu::TextureFormat, @@ -38,7 +38,7 @@ pub struct TextureAtlas { packer: P, } -impl TextureAtlas

{ +impl PackedTextureAtlas

{ pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, diff --git a/crates/lyra-game/src/sprite/mod.rs b/crates/lyra-game/src/sprite/mod.rs index ba71765..c754af7 100644 --- a/crates/lyra-game/src/sprite/mod.rs +++ b/crates/lyra-game/src/sprite/mod.rs @@ -3,6 +3,9 @@ use lyra_reflect::Reflect; use lyra_resource::ResHandle; use lyra_math::{Vec3, Vec2}; +mod texture_atlas; +pub use texture_atlas::*; + /// How the sprite is positioned and rotated relative to its [`Transform`]. /// /// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites. diff --git a/crates/lyra-game/src/sprite/texture_atlas.rs b/crates/lyra-game/src/sprite/texture_atlas.rs new file mode 100644 index 0000000..6143ec3 --- /dev/null +++ b/crates/lyra-game/src/sprite/texture_atlas.rs @@ -0,0 +1,105 @@ +use glam::{UVec2, Vec3}; +use lyra_ecs::Component; +use lyra_math::URect; +use lyra_reflect::Reflect; +use lyra_resource::ResHandle; + +use super::Pivot; + +/// A texture atlas of multiple sprites. +#[derive(Clone, Component, Reflect)] +pub struct TextureAtlas { + pub texture: ResHandle, + /// The coordinates in the texture where the grid starts. + pub grid_offset: UVec2, + /// The size of the grid in cells. + pub grid_size: UVec2, + /// The size of each cell. + pub cell_size: UVec2, + + pub sprite_color: Vec3, + pub pivot: Pivot, +} + +impl TextureAtlas { + /// The cell x and y in the grid of a specific index. + pub fn index_cell(&self, i: u32) -> UVec2 { + let x = i % self.grid_size.x; + let y = i / self.grid_size.x; + UVec2 { x, y } + } + + /// The coords of the cell at x and y in the grid. + /// + /// The indices are different then the image coords, this is the position in the grid. + /// So if you have a 9x7 grid, and wanted to get the 1nd cell on the 2nd row, you'd + /// use the values `x = 0, y = 1` (indices start at zero like arrays). + #[inline(always)] + pub fn cell_coords(&self, x: u32, y: u32) -> UVec2 { + UVec2 { + x: x * self.cell_size.x, + y: y * self.cell_size.y, + } + } + + /// The coords of the cell at an index. + #[inline(always)] + pub fn index_coords(&self, i: u32) -> UVec2 { + let cell = self.index_cell(i); + + self.cell_coords(cell.x, cell.y) + } + + /// The rectangle of the cell at the x and y indices in the grid. + /// + /// The indices are different then the image coords, this is the position in the grid. + /// So if you have a 9x7 grid, and wanted to get the 1nd cell on the 2nd row, you'd + /// use the values `x = 0, y = 1` (indices start at zero like arrays). + #[inline(always)] + pub fn cell_rect(&self, x: u32, y: u32) -> URect { + let start = self.cell_coords(x, y); + let end = start + self.cell_size; + URect { min: start, max: end } + } + + /// The rectangle of the cell at an index. + #[inline(always)] + pub fn index_rect(&self, i: u32) -> URect { + let cell = self.index_cell(i); + self.cell_rect(cell.x, cell.y) + } +} + +impl lyra_resource::ResourceData for TextureAtlas { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn dependencies(&self) -> Vec { + vec![self.texture.untyped_clone()] + } +} + +/// A sprite from a texture atlas. +#[derive(Clone, Component, Reflect)] +pub struct AtlasSprite { + pub atlas: ResHandle, + pub sprite: URect, +} + +impl AtlasSprite { + #[inline(always)] + pub fn from_atlas_index(atlas: ResHandle, i: u32) -> Self { + let a = atlas.data_ref().unwrap(); + let rect = a.index_rect(i); + + Self { + atlas: atlas.clone(), + sprite: rect, + } + } +} \ No newline at end of file diff --git a/crates/lyra-math/Cargo.toml b/crates/lyra-math/Cargo.toml index fa5e35a..e7127bb 100644 --- a/crates/lyra-math/Cargo.toml +++ b/crates/lyra-math/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -glam = { version = "0.29.0" } \ No newline at end of file +bytemuck = "1.19.0" +glam = { version = "0.29.0", features = ["bytemuck"] } diff --git a/crates/lyra-math/src/i32/mod.rs b/crates/lyra-math/src/i32/mod.rs new file mode 100644 index 0000000..d698a64 --- /dev/null +++ b/crates/lyra-math/src/i32/mod.rs @@ -0,0 +1,2 @@ +mod rect; +pub use rect::*; \ No newline at end of file diff --git a/crates/lyra-math/src/i32/rect.rs b/crates/lyra-math/src/i32/rect.rs new file mode 100644 index 0000000..a08d57f --- /dev/null +++ b/crates/lyra-math/src/i32/rect.rs @@ -0,0 +1,90 @@ +use glam::IVec2; + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct IRect { + pub min: IVec2, + pub max: IVec2, +} + +impl IRect { + pub const ZERO: IRect = IRect { min: IVec2::ZERO, max: IVec2::ZERO }; + + pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self { + min: IVec2::new(x1, y1), + max: IVec2::new(x2, y2), + } + } + + pub fn from_vec(min: IVec2, max: IVec2) -> Self { + Self { + min, + max + } + } + + pub fn dimensions(&self) -> IVec2 { + self.max - self.min + } +} + +impl std::ops::Add for IRect { + type Output = IRect; + + fn add(self, rhs: Self) -> Self::Output { + IRect::from_vec(self.min + rhs.min, self.max + rhs.max) + } +} + +impl std::ops::AddAssign for IRect { + fn add_assign(&mut self, rhs: Self) { + self.min += rhs.min; + self.max += rhs.max; + } +} + +impl std::ops::Sub for IRect { + type Output = IRect; + + fn sub(self, rhs: Self) -> Self::Output { + IRect::from_vec(self.min - rhs.min, self.max - rhs.max) + } +} + +impl std::ops::SubAssign for IRect { + fn sub_assign(&mut self, rhs: Self) { + self.min -= rhs.min; + self.max -= rhs.max; + } +} + +impl std::ops::Mul for IRect { + type Output = IRect; + + fn mul(self, rhs: Self) -> Self::Output { + IRect::from_vec(self.min * rhs.min, self.max * rhs.max) + } +} + +impl std::ops::MulAssign for IRect { + fn mul_assign(&mut self, rhs: Self) { + self.min *= rhs.min; + self.max *= rhs.max; + } +} + +impl std::ops::Div for IRect { + type Output = IRect; + + fn div(self, rhs: Self) -> Self::Output { + IRect::from_vec(self.min / rhs.min, self.max / rhs.max) + } +} + +impl std::ops::DivAssign for IRect { + fn div_assign(&mut self, rhs: Self) { + self.min /= rhs.min; + self.max /= rhs.max; + } +} \ No newline at end of file diff --git a/crates/lyra-math/src/lib.rs b/crates/lyra-math/src/lib.rs index f79c9bf..f1fc8ec 100644 --- a/crates/lyra-math/src/lib.rs +++ b/crates/lyra-math/src/lib.rs @@ -10,6 +10,14 @@ pub use area::*; mod rect; pub use rect::*; +#[allow(hidden_glob_reexports)] +mod u32; +pub use u32::*; + +#[allow(hidden_glob_reexports)] +mod i32; +pub use i32::*; + pub mod transform; pub use transform::*; diff --git a/crates/lyra-math/src/rect.rs b/crates/lyra-math/src/rect.rs index e2256fb..ec96494 100644 --- a/crates/lyra-math/src/rect.rs +++ b/crates/lyra-math/src/rect.rs @@ -1,12 +1,15 @@ use glam::Vec2; -#[derive(Debug, Clone, Copy, Default, PartialEq)] +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] pub struct Rect { pub min: Vec2, pub max: Vec2, } impl Rect { + pub const ZERO: Rect = Rect { min: Vec2::ZERO, max: Vec2::ZERO }; + pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { Self { min: Vec2::new(x1, y1), @@ -20,6 +23,10 @@ impl Rect { max } } + + pub fn dimensions(&self) -> Vec2 { + (self.max - self.min).abs() + } } impl std::ops::Add for Rect { diff --git a/crates/lyra-math/src/u32/mod.rs b/crates/lyra-math/src/u32/mod.rs new file mode 100644 index 0000000..d698a64 --- /dev/null +++ b/crates/lyra-math/src/u32/mod.rs @@ -0,0 +1,2 @@ +mod rect; +pub use rect::*; \ No newline at end of file diff --git a/crates/lyra-math/src/u32/rect.rs b/crates/lyra-math/src/u32/rect.rs new file mode 100644 index 0000000..983c554 --- /dev/null +++ b/crates/lyra-math/src/u32/rect.rs @@ -0,0 +1,90 @@ +use glam::UVec2; + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct URect { + pub min: UVec2, + pub max: UVec2, +} + +impl URect { + pub const ZERO: URect = URect { min: UVec2::ZERO, max: UVec2::ZERO }; + + pub fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self { + Self { + min: UVec2::new(x1, y1), + max: UVec2::new(x2, y2), + } + } + + pub fn from_vec(min: UVec2, max: UVec2) -> Self { + Self { + min, + max + } + } + + pub fn dimensions(&self) -> UVec2 { + self.max - self.min + } +} + +impl std::ops::Add for URect { + type Output = URect; + + fn add(self, rhs: Self) -> Self::Output { + URect::from_vec(self.min + rhs.min, self.max + rhs.max) + } +} + +impl std::ops::AddAssign for URect { + fn add_assign(&mut self, rhs: Self) { + self.min += rhs.min; + self.max += rhs.max; + } +} + +impl std::ops::Sub for URect { + type Output = URect; + + fn sub(self, rhs: Self) -> Self::Output { + URect::from_vec(self.min - rhs.min, self.max - rhs.max) + } +} + +impl std::ops::SubAssign for URect { + fn sub_assign(&mut self, rhs: Self) { + self.min -= rhs.min; + self.max -= rhs.max; + } +} + +impl std::ops::Mul for URect { + type Output = URect; + + fn mul(self, rhs: Self) -> Self::Output { + URect::from_vec(self.min * rhs.min, self.max * rhs.max) + } +} + +impl std::ops::MulAssign for URect { + fn mul_assign(&mut self, rhs: Self) { + self.min *= rhs.min; + self.max *= rhs.max; + } +} + +impl std::ops::Div for URect { + type Output = URect; + + fn div(self, rhs: Self) -> Self::Output { + URect::from_vec(self.min / rhs.min, self.max / rhs.max) + } +} + +impl std::ops::DivAssign for URect { + fn div_assign(&mut self, rhs: Self) { + self.min /= rhs.min; + self.max /= rhs.max; + } +} \ No newline at end of file diff --git a/crates/lyra-reflect/src/impls/impl_math.rs b/crates/lyra-reflect/src/impls/impl_math.rs index b317246..352314b 100644 --- a/crates/lyra-reflect/src/impls/impl_math.rs +++ b/crates/lyra-reflect/src/impls/impl_math.rs @@ -6,7 +6,17 @@ use crate::{lyra_engine, Enum, Method, Reflect, ReflectMut, ReflectRef}; impl_reflect_simple_struct!(lyra_math::Vec2, fields(x = f32, y = f32)); impl_reflect_simple_struct!(lyra_math::Vec3, fields(x = f32, y = f32, z = f32)); impl_reflect_simple_struct!(lyra_math::Vec4, fields(x = f32, y = f32, z = f32, w = f32)); +impl_reflect_simple_struct!(lyra_math::UVec2, fields(x = u32, y = u32)); +impl_reflect_simple_struct!(lyra_math::UVec3, fields(x = u32, y = u32, z = u32)); +impl_reflect_simple_struct!(lyra_math::UVec4, fields(x = u32, y = u32, z = u32, w = u32)); +impl_reflect_simple_struct!(lyra_math::IVec2, fields(x = i32, y = i32)); +impl_reflect_simple_struct!(lyra_math::IVec3, fields(x = i32, y = i32, z = i32)); +impl_reflect_simple_struct!(lyra_math::IVec4, fields(x = i32, y = i32, z = i32, w = i32)); + impl_reflect_simple_struct!(lyra_math::Quat, fields(x = f32, y = f32, z = f32, w = f32)); +impl_reflect_simple_struct!(lyra_math::Rect, fields(min = lyra_math::Vec2, max = lyra_math::Vec2)); +impl_reflect_simple_struct!(lyra_math::URect, fields(min = lyra_math::UVec2, max = lyra_math::UVec2)); +impl_reflect_simple_struct!(lyra_math::IRect, fields(min = lyra_math::IVec2, max = lyra_math::IVec2)); impl_reflect_simple_struct!( lyra_math::Transform, diff --git a/crates/lyra-resource/src/resource_manager.rs b/crates/lyra-resource/src/resource_manager.rs index 2e34e08..c0de90c 100644 --- a/crates/lyra-resource/src/resource_manager.rs +++ b/crates/lyra-resource/src/resource_manager.rs @@ -184,6 +184,14 @@ impl ResourceManager { } } + /// Store a new resource, returning its handle. + pub fn store_new(&self, data: T) -> ResHandle { + let handle = ResHandle::new_ready(None, data); + let mut state = self.state_mut(); + state.resources.insert(handle.uuid().to_string(), Arc::new(handle.clone())); + handle + } + /// Store a resource using its uuid. /// /// The resource cannot be requested with [`ResourceManager::request`], it can only be diff --git a/examples/2d/src/main.rs b/examples/2d/src/main.rs index f610013..c259a32 100644 --- a/examples/2d/src/main.rs +++ b/examples/2d/src/main.rs @@ -1,18 +1,12 @@ use lyra_engine::{ - assets::{Image, ResourceManager}, - game::App, - gltf::Gltf, - input::{ + assets::{Image, ResourceManager}, ecs::query::{Res, ResMut, View}, game::App, gltf::Gltf, input::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, - }, - math::{self, Transform, Vec3}, - render::light::directional::DirectionalLight, - scene::{ + }, math::{self, Rect, Transform, URect, UVec2, Vec2, Vec3}, render::light::directional::DirectionalLight, scene::{ system_update_world_transforms, Camera2dBundle, CameraProjection, OrthographicProjection, ScaleMode, TopDown2dCamera, TopDown2dCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN - }, - sprite::{self, Sprite}, + }, sprite::{self, AtlasSprite, Pivot, Sprite, TextureAtlas}, DeltaTime }; +use tracing::debug; #[async_std::main] async fn main() { @@ -100,6 +94,9 @@ async fn main() { } fn setup_scene_plugin(app: &mut App) { + app.add_resource(Timer(0.0)); + app.with_system("sprite_change", sprite_change, &[]); + let world = &mut app.world; let resman = world.get_resource_mut::().unwrap(); @@ -127,19 +124,28 @@ fn setup_scene_plugin(app: &mut App) { let image = resman.request::("../assets/Egg_item.png").unwrap(); image.wait_recurse_dependencies_load().unwrap(); + let soldier = resman.request::("../assets/tiny_rpg_characters/Characters(100x100)/Soldier/Soldier/Soldier.png").unwrap(); + soldier.wait_recurse_dependencies_load().unwrap(); + + let atlas = resman.store_new(TextureAtlas { + texture: soldier, + grid_offset: UVec2::ZERO, + grid_size: UVec2::new(9, 7), + cell_size: UVec2::new(100, 100), + sprite_color: Vec3::ONE, + pivot: Pivot::default(), + }); + let sprite = AtlasSprite::from_atlas_index(atlas, 9); + drop(resman); - world.spawn(( - cube_mesh.clone(), - WorldTransform::default(), - Transform::from_xyz(0.0, 0.0, -2.0), - )); world.spawn(( - Sprite { - texture: image, + /* Sprite { + texture: sprite, color: Vec3::ONE, pivot: sprite::Pivot::Center, - }, + }, */ + sprite, WorldTransform::default(), Transform::from_xyz(0.0, 0.0, -10.0), )); @@ -169,8 +175,41 @@ fn setup_scene_plugin(app: &mut App) { }, Transform::from_xyz(0.0, 0.0, 0.0), TopDown2dCamera { - zoom_speed: Some(0.1), + zoom_speed: Some(0.2), + speed: 14.0, ..Default::default() } )); } + +#[derive(Clone, Copy, Debug)] +struct Timer(f32); + +fn sprite_change(mut timer: ResMut, dt: Res, view: View<&mut AtlasSprite>) -> anyhow::Result<()> { + timer.0 += **dt; + + const TIME: f32 = 0.1; + if timer.0 >= TIME { + //println!("{t} seconds timer triggered, moving sprite"); + timer.0 = 0.0; + + for mut a in view.iter() { + //println!("a.sprite: {:?}", a.sprite); + + if a.sprite.max.x >= 800 { + a.sprite = URect { + min: UVec2::new(0, 100), + max: UVec2::new(100, 200), + }; + //println!("restart!"); + } else { + a.sprite += URect { + min: UVec2::new(100, 0), + max: UVec2::new(100, 0), + }; + } + } + } + + Ok(()) +} \ No newline at end of file diff --git a/examples/assets/tiny_rpg_characters/.gitignore b/examples/assets/tiny_rpg_characters/.gitignore new file mode 100644 index 0000000..3de37e8 --- /dev/null +++ b/examples/assets/tiny_rpg_characters/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!source.txt \ No newline at end of file diff --git a/examples/assets/tiny_rpg_characters/source.txt b/examples/assets/tiny_rpg_characters/source.txt new file mode 100644 index 0000000..507d5bc --- /dev/null +++ b/examples/assets/tiny_rpg_characters/source.txt @@ -0,0 +1 @@ +https://zerie.itch.io/tiny-rpg-character-asset-pack \ No newline at end of file