diff --git a/crates/lyra-game/src/render/graph/passes/sprite.rs b/crates/lyra-game/src/render/graph/passes/sprite.rs index b853aac..184d8ff 100644 --- a/crates/lyra-game/src/render/graph/passes/sprite.rs +++ b/crates/lyra-game/src/render/graph/passes/sprite.rs @@ -17,20 +17,18 @@ use crate::{ render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState}, - transform_buffer_storage::{TransformBuffers, TransformIndex}, vertex::Vertex2D, }, sprite::{AtlasSprite, Sprite}, }; -use super::{BasePassSlots, RenderAssets}; +use super::{BasePassSlots, InterpTransform, 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, } @@ -64,11 +62,8 @@ 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>, + sprite_instances_buf: Option>, - transform_buffers: Option, texture_store: Option, buffer_store: Option, } @@ -175,7 +170,7 @@ impl SpritePass { }, ); - let frames = self.atlas_frames_buf.as_ref().unwrap(); + let sprite_instances = self.sprite_instances_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)), @@ -192,7 +187,7 @@ impl SpritePass { wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &frames, + buffer: &sprite_instances, offset: 0, size: None, }), @@ -211,16 +206,13 @@ 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, + let sprite_instances = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("sprite_instances"), + 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)); + self.sprite_instances_buf = Some(Arc::new(sprite_instances)); let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bgl_sprite_main"), @@ -243,7 +235,7 @@ impl Node for SpritePass { }, wgpu::BindGroupLayoutEntry { binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, @@ -254,7 +246,6 @@ impl Node for SpritePass { ], }); 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![]); @@ -282,16 +273,12 @@ impl Node for SpritePass { let diffuse_bgl = self.texture_bgl.clone().unwrap(); let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera).clone(); - let transforms = world - .get_resource::() - .expect("Missing transform buffers"); - let transform_bgl = transforms.bindgroup_layout.clone(); self.pipeline = Some(RenderPipeline::create( device, &RenderPipelineDescriptor { label: Some("sprite_pass".into()), - layouts: vec![diffuse_bgl, transform_bgl, camera_bgl], + layouts: vec![diffuse_bgl, camera_bgl], push_constant_ranges: vec![], vertex: VertexState { module: shader.clone(), @@ -317,7 +304,6 @@ impl Node for SpritePass { }, )); - drop(transforms); world.add_resource_default_if_absent::>(); let texture_store = world .get_resource_data::>().unwrap(); @@ -327,22 +313,17 @@ impl Node for SpritePass { let buffer_store = world .get_resource_data::>().unwrap(); self.buffer_store = Some(buffer_store.clone()); - - let transforms = world - .get_resource_data::() - .expect("Missing transform buffers"); - self.transform_buffers = Some(transforms.clone()); } - let mut v = Vec::with_capacity(500); + let mut sprite_instances = Vec::with_capacity(500); let world_tick = world.current_tick(); let queue = &graph.queue; - for (entity, (sprite, atlas_sprite), transform_idx, mut texture_store, mut buffer_store) in world + for (entity, (sprite, atlas_sprite), interp_transform, mut texture_store, mut buffer_store) in world .view::<( Entities, Or<&Sprite, (&AtlasSprite, TickOf)>, - &TransformIndex, + &InterpTransform, ResMut>, ResMut>, )>() @@ -374,13 +355,14 @@ impl Node for SpritePass { } } - if !buffer_store.contains_key(&entity) { - let dim = rect.unwrap_or_else(|| { + let dim = rect + .map(|r| r.dimensions()) + .unwrap_or_else(|| { let i = image.dimensions(); - URect::new(0, 0, i.0, i.1) + UVec2::new(i.0, i.1) }); - debug!("storing rect: {dim:?}"); - let dim = dim.dimensions(); + + if !buffer_store.contains_key(&entity) { 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 { @@ -393,29 +375,36 @@ impl Node for SpritePass { } } - 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 + let pivot = atlas_sprite.map(|ats| ats.0.pivot) + // unwrap is safe since its either AtlasSprite or Sprite. + .unwrap_or_else(|| sprite.unwrap().pivot) + .as_vec(); + + let pivot_pos = dim.as_vec2() * (pivot - Vec2::splat(0.5)); + let transform = interp_transform.last_transform + + lyra_math::Transform::from_translation(Vec3::new(pivot_pos.x, pivot_pos.y, 0.0)); + + let inst = SpriteInstance { + atlas_frame: rect.unwrap_or(URect::ZERO), + transform: transform.calculate_mat4(), + pivot, + _padding: [0; 2], }; + sprite_instances.push(inst); + let inst_id = sprite_instances.len() as u64 - 1; self.jobs.push_back(RenderJob { entity, shader_id: 0, asset_uuid: texture_uuid, - transform_id: *transform_idx, - atlas_frame_id: frame_id, + atlas_frame_id: inst_id, }); }; } - let buf = self.atlas_frames_buf.as_ref().unwrap(); + let buf = self.sprite_instances_buf.as_ref().unwrap(); // skip default rect - queue.write_buffer(buf, std::mem::size_of::() as _, bytemuck::cast_slice(&v)); + queue.write_buffer(buf, 0, bytemuck::cast_slice(&sprite_instances)); } fn execute( @@ -430,8 +419,6 @@ impl Node for SpritePass { 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(); let vt = graph.view_target(); let view = vt.render_view(); @@ -468,12 +455,7 @@ impl Node for SpritePass { .expect("failed to find SpriteTexture for job asset_uuid"); 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); - let offset = transforms.buffer_offset(job.transform_id); - pass.set_bind_group(1, bindgroup, &[offset]); - - pass.set_bind_group(2, camera_bg, &[]); + pass.set_bind_group(1, camera_bg, &[]); // set vertex and index buffers let bufs = buffer_store @@ -489,3 +471,12 @@ impl Node for SpritePass { } } } + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct SpriteInstance { + atlas_frame: URect, + transform: glam::Mat4, + pivot: glam::Vec2, + _padding: [u32; 2], +} \ No newline at end of file diff --git a/crates/lyra-game/src/render/graph/passes/transform.rs b/crates/lyra-game/src/render/graph/passes/transform.rs index 7ca1fc5..0797533 100644 --- a/crates/lyra-game/src/render/graph/passes/transform.rs +++ b/crates/lyra-game/src/render/graph/passes/transform.rs @@ -25,8 +25,8 @@ use crate::{ /// transform is updated less often than rendering. #[derive(Clone, Debug, Component)] pub struct InterpTransform { - last_transform: Transform, - alpha: f32, + pub last_transform: Transform, + pub alpha: f32, } #[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)] 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 ba17363..d74d836 100644 --- a/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl +++ b/crates/lyra-game/src/render/shaders/2d/sprite_main.wgsl @@ -23,6 +23,12 @@ struct URect { max: vec2, } +struct SpriteInstance { + atlas_frame: URect, + transform: mat4x4, + pivot: vec2, +} + struct CameraUniform { view: mat4x4, inverse_projection: mat4x4, @@ -33,21 +39,21 @@ struct CameraUniform { } @group(1) @binding(0) -var u_model_transform_data: TransformData; - -@group(2) @binding(0) var u_camera: CameraUniform; @vertex fn vs_main( in: VertexInput, ) -> VertexOutput { + let transform = u_sprite_instances[in.instance_index].transform; + var world_position: vec4 = transform * vec4(in.position, 1.0); + var out: VertexOutput; - var world_position: vec4 = u_model_transform_data.transform * vec4(in.position, 1.0); 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; } @@ -58,11 +64,12 @@ var t_diffuse: texture_2d; var s_diffuse: sampler; @group(0) @binding(2) -var u_sprite_atlas_frames: array; +var u_sprite_instances: array; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let frame = u_sprite_atlas_frames[in.instance_index]; + let sprite = u_sprite_instances[in.instance_index]; + let frame = sprite.atlas_frame; var region_coords = in.tex_coords; if (frame.min.x != 0 || frame.min.y != 0 || frame.max.x != 0 || frame.max.y != 0) { diff --git a/crates/lyra-game/src/scene/camera.rs b/crates/lyra-game/src/scene/camera.rs index 785e156..bf9cf5a 100755 --- a/crates/lyra-game/src/scene/camera.rs +++ b/crates/lyra-game/src/scene/camera.rs @@ -34,6 +34,7 @@ impl Default for ScaleMode { #[derive(Debug, Clone, Copy, PartialEq, Reflect)] pub struct OrthographicProjection { + pub viewport_origin: Vec2, pub scale_mode: ScaleMode, pub scale: f32, pub znear: f32, @@ -43,6 +44,7 @@ pub struct OrthographicProjection { impl Default for OrthographicProjection { fn default() -> Self { Self { + viewport_origin: Vec2::new(0.5, 0.5), scale_mode: Default::default(), scale: 1.0, znear: 0.0, @@ -53,52 +55,44 @@ impl Default for OrthographicProjection { impl OrthographicProjection { fn get_rect(&self, viewport_size: Vec2) -> Rect { - let origin; + //let origin; let size; match self.scale_mode { ScaleMode::Viewport => { - origin = viewport_size * 0.5; size = viewport_size; }, ScaleMode::Width(width) => { let aspect = viewport_size.x / viewport_size.y; - let origin_x = width * 0.5; let scaled_height = width / aspect; - let origin_y = scaled_height * 0.5; - origin = Vec2::new(origin_x, origin_y); size = Vec2::new(width, scaled_height); }, ScaleMode::Height(height) => { let aspect = viewport_size.x / viewport_size.y; - let origin_y = height * 0.5; let scaled_width = height * aspect; - let origin_x = scaled_width * 0.5; - origin = Vec2::new(origin_x, origin_y); size = Vec2::new(scaled_width, height); }, ScaleMode::Size(s) => { - origin = s * 0.5; size = s; }, ScaleMode::MaxSize(s) => { let clamped = s.min(viewport_size); - origin = clamped * 0.5; size = clamped; }, ScaleMode::MinSize(s) => { let clamped = s.max(viewport_size); - origin = clamped * 0.5; size = clamped; } } + let origin = size * self.viewport_origin; + Rect::new(self.scale * -origin.x, self.scale * -origin.y, - self.scale * size.x - origin.x, - self.scale * size.y - origin.y) + self.scale * (size.x - origin.x), + self.scale * (size.y - origin.y)) } pub fn to_mat(&self, viewport_size: Vec2) -> Mat4 { diff --git a/crates/lyra-game/src/sprite/animation_sheet.rs b/crates/lyra-game/src/sprite/animation_sheet.rs index 2eab8e3..e7dbb38 100644 --- a/crates/lyra-game/src/sprite/animation_sheet.rs +++ b/crates/lyra-game/src/sprite/animation_sheet.rs @@ -12,7 +12,7 @@ use tracing::error; use crate::DeltaTime; -use super::{AtlasSprite, TextureAtlas}; +use super::{AtlasSprite, Pivot, TextureAtlas}; /// A struct describing an animation of a Sprite. /// @@ -266,6 +266,7 @@ fn system_animation_entity_impl( let sprite = AtlasSprite { atlas: animations.atlas.clone(), sprite: rect, + pivot: Pivot::default(), }; commands.insert(en, sprite); @@ -286,6 +287,7 @@ fn system_animation_entity_impl( let new_sprite = AtlasSprite { atlas: animations.atlas.clone(), sprite: rect, + pivot: Pivot::default(), }; let sprite = sprite.as_mut().unwrap(); diff --git a/crates/lyra-game/src/sprite/mod.rs b/crates/lyra-game/src/sprite/mod.rs index 670413c..bc30f73 100644 --- a/crates/lyra-game/src/sprite/mod.rs +++ b/crates/lyra-game/src/sprite/mod.rs @@ -24,8 +24,9 @@ pub enum Pivot { BottomLeft, BottomRight, BottomCenter, - /// A custom anchor point relative to top left. - /// Top left is `(0.0, 0.0)`. + /// A custom anchor point. + /// + /// Top left is (-0.5, 0.5), center is (0.0, 0.0). Custom(Vec2) } @@ -35,15 +36,15 @@ impl Pivot { /// The point is offset from the top left `(0.0, 0.0)`. pub fn as_vec(&self) -> Vec2 { match self { - Pivot::Center => Vec2::new(0.5, 0.5), - Pivot::CenterLeft => Vec2::new(0.0, 0.5), - Pivot::CenterRight => Vec2::new(1.0, 0.5), - Pivot::TopLeft => Vec2::ZERO, - Pivot::TopRight => Vec2::new(1.0, 0.0), + Pivot::Center => Vec2::ZERO, + Pivot::CenterLeft => Vec2::new(-0.5, 0.0), + Pivot::CenterRight => Vec2::new(0.5, 0.0), + Pivot::TopLeft => Vec2::new(-0.5, 0.5), + Pivot::TopRight => Vec2::new(0.5, 0.5), Pivot::TopCenter => Vec2::new(0.0, 0.5), - Pivot::BottomLeft => Vec2::new(0.0, 1.0), - Pivot::BottomRight => Vec2::new(1.0, 1.0), - Pivot::BottomCenter => Vec2::new(0.5, 1.0), + Pivot::BottomLeft => Vec2::new(-0.5, -0.5), + Pivot::BottomRight => Vec2::new(0.5, -0.5), + Pivot::BottomCenter => Vec2::new(0.0, -0.5), Pivot::Custom(v) => *v, } } diff --git a/crates/lyra-game/src/sprite/texture_atlas.rs b/crates/lyra-game/src/sprite/texture_atlas.rs index 39015e9..4896aab 100644 --- a/crates/lyra-game/src/sprite/texture_atlas.rs +++ b/crates/lyra-game/src/sprite/texture_atlas.rs @@ -4,6 +4,8 @@ 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 { @@ -60,17 +62,19 @@ impl lyra_resource::ResourceData for TextureAtlas { pub struct AtlasSprite { pub atlas: ResHandle, pub sprite: URect, + pub pivot: Pivot, } impl AtlasSprite { #[inline(always)] pub fn from_atlas_index(atlas: ResHandle, i: u32) -> Self { let a = atlas.data_ref().unwrap(); - let rect = a.frames.get(i as usize).cloned().unwrap(); //index_rect(i); + let rect = a.frames.get(i as usize).cloned().unwrap(); Self { atlas: atlas.clone(), sprite: rect, + pivot: Pivot::default(), } } } diff --git a/crates/lyra-math/src/transform.rs b/crates/lyra-math/src/transform.rs index 83fd2b7..f1b8d0b 100755 --- a/crates/lyra-math/src/transform.rs +++ b/crates/lyra-math/src/transform.rs @@ -136,4 +136,26 @@ impl std::ops::Add for Transform { self.scale *= rhs.scale; self } +} + +impl std::ops::Sub for Transform { + type Output = Transform; + + fn sub(mut self, rhs: Self) -> Self::Output { + self.translation -= rhs.translation; + self.rotation *= rhs.rotation; + self.scale *= rhs.scale; + self + } +} + +impl std::ops::Mul for Transform { + type Output = Transform; + + fn mul(mut self, rhs: Self) -> Self::Output { + self.translation *= rhs.translation; + self.rotation *= rhs.rotation; + self.scale *= rhs.scale; + self + } } \ No newline at end of file