render: implement sprite pivot, fix sprite centering in ortho projection

This commit is contained in:
SeanOMik 2024-11-27 23:04:20 -05:00
parent d1aee610cc
commit a06c065337
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
8 changed files with 111 additions and 90 deletions

View File

@ -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<RenderPipeline>,
texture_bgl: Option<Arc<wgpu::BindGroupLayout>>,
jobs: VecDeque<RenderJob>,
/// 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<Arc<wgpu::Buffer>>,
sprite_instances_buf: Option<Arc<wgpu::Buffer>>,
transform_buffers: Option<ResourceData>,
texture_store: Option<ResourceData>,
buffer_store: Option<ResourceData>,
}
@ -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::<URect>() as u64 * 1000,
let sprite_instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_instances"),
size: std::mem::size_of::<SpriteInstance>() 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::<TransformBuffers>()
.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::<RenderAssets<SpriteTexture>>();
let texture_store = world
.get_resource_data::<RenderAssets<SpriteTexture>>().unwrap();
@ -327,22 +313,17 @@ impl Node for SpritePass {
let buffer_store = world
.get_resource_data::<FxHashMap<Entity, SpriteBuffers>>().unwrap();
self.buffer_store = Some(buffer_store.clone());
let transforms = world
.get_resource_data::<TransformBuffers>()
.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<AtlasSprite>)>,
&TransformIndex,
&InterpTransform,
ResMut<RenderAssets<SpriteTexture>>,
ResMut<FxHashMap<Entity, SpriteBuffers>>,
)>()
@ -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::<URect>() 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<RenderAssets<SpriteTexture>> = texture_store.get();
let buffer_store = self.buffer_store.clone().unwrap();
let buffer_store: AtomicRef<FxHashMap<Entity, SpriteBuffers>> = buffer_store.get();
let transforms = self.transform_buffers.clone().unwrap();
let transforms: AtomicRef<TransformBuffers> = 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],
}

View File

@ -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)]

View File

@ -23,6 +23,12 @@ struct URect {
max: vec2<u32>,
}
struct SpriteInstance {
atlas_frame: URect,
transform: mat4x4<f32>,
pivot: vec2<f32>,
}
struct CameraUniform {
view: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
@ -33,21 +39,21 @@ struct CameraUniform {
}
@group(1) @binding(0)
var<uniform> u_model_transform_data: TransformData;
@group(2) @binding(0)
var<uniform> u_camera: CameraUniform;
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
let transform = u_sprite_instances[in.instance_index].transform;
var world_position: vec4<f32> = transform * vec4<f32>(in.position, 1.0);
var out: VertexOutput;
var world_position: vec4<f32> = u_model_transform_data.transform * vec4<f32>(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<f32>;
var s_diffuse: sampler;
@group(0) @binding(2)
var<storage, read> u_sprite_atlas_frames: array<URect>;
var<storage, read> u_sprite_instances: array<SpriteInstance>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
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) {

View File

@ -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 {

View File

@ -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();

View File

@ -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,
}
}

View File

@ -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<TextureAtlas>,
pub sprite: URect,
pub pivot: Pivot,
}
impl AtlasSprite {
#[inline(always)]
pub fn from_atlas_index(atlas: ResHandle<TextureAtlas>, 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(),
}
}
}

View File

@ -137,3 +137,25 @@ impl std::ops::Add for Transform {
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
}
}