From 0af265040c80cded8dc880411d2bdc075560a54a Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 26 Dec 2023 22:33:00 -0800 Subject: [PATCH] Added subsprites --- src/game/game.rs | 11 +- src/game/ship.rs | 76 ++++++++------ src/game/system.rs | 2 +- src/game/systemobject.rs | 8 +- src/render/consts.rs | 9 ++ src/render/gpustate.rs | 173 ++++++++++++++++++++++++------- src/render/mod.rs | 13 --- src/render/shaders/sprite.wgsl | 41 ++------ src/render/sprite.rs | 13 ++- src/render/vertexbuffer/types.rs | 49 +++------ 10 files changed, 239 insertions(+), 156 deletions(-) diff --git a/src/game/game.rs b/src/game/game.rs index 6320a98..23e1369 100644 --- a/src/game/game.rs +++ b/src/game/game.rs @@ -2,13 +2,14 @@ use cgmath::Deg; use std::time::Instant; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; -use super::{ship::ShipKind, Camera, InputStatus, Ship, System}; +use super::{Camera, InputStatus, Ship, System}; use crate::{consts, content::Content, render::Sprite, render::Spriteable}; pub struct Game { pub input: InputStatus, pub last_update: Instant, pub player: Ship, + pub test: Ship, pub system: System, pub camera: Camera, paused: bool, @@ -20,7 +21,7 @@ impl Game { Game { last_update: Instant::now(), input: InputStatus::new(), - player: Ship::new(ShipKind::Gypsum, (0.0, 0.0).into()), + player: Ship::new(&ct.ships[0], (0.0, 0.0).into()), camera: Camera { pos: (0.0, 0.0).into(), zoom: 500.0, @@ -28,6 +29,7 @@ impl Game { system: System::new(&ct.systems[0]), paused: false, time_scale: 1.0, + test: Ship::new(&ct.ships[0], (100.0, 100.0).into()), } } @@ -55,6 +57,7 @@ impl Game { pub fn update(&mut self) { let t: f32 = self.last_update.elapsed().as_secs_f32() * self.time_scale; + self.player.engines_on = self.input.key_thrust; if self.input.key_thrust { self.player.physicsbody.thrust(50.0 * t); } @@ -84,11 +87,13 @@ impl Game { sprites.append(&mut self.system.get_sprites()); sprites.push(self.player.get_sprite()); + sprites.push(self.test.get_sprite()); // Make sure sprites are drawn in the correct order // (note the reversed a, b in the comparator) // - // TODO: use a gpu depth buffer instead. + // TODO: maybe use a gpu depth buffer instead? + // I've tried this, but it doesn't seem to work with transparent textures. sprites.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z)); return sprites; diff --git a/src/game/ship.rs b/src/game/ship.rs index 382e2f8..f76adba 100644 --- a/src/game/ship.rs +++ b/src/game/ship.rs @@ -1,49 +1,63 @@ -use cgmath::Point2; +use cgmath::{Deg, Point2, Point3}; -use crate::{physics::PhysicsBody, render::Sprite, render::SpriteTexture, render::Spriteable}; - -pub enum ShipKind { - Gypsum, -} - -impl ShipKind { - fn sprite(&self) -> SpriteTexture { - let name = match self { - Self::Gypsum => "ship::gypsum", - }; - - return SpriteTexture(name.to_owned()); - } - - fn size(&self) -> f32 { - match self { - Self::Gypsum => 100.0, - } - } -} +use crate::{ + content::{self, ship::Engine}, + physics::PhysicsBody, + render::Sprite, + render::SpriteTexture, + render::Spriteable, +}; pub struct Ship { pub physicsbody: PhysicsBody, - kind: ShipKind, + pub engines_on: bool, + + sprite: SpriteTexture, + size: f32, + engines: Vec, } impl Ship { - pub fn new(kind: ShipKind, pos: Point2) -> Self { + pub fn new(ct: &content::ship::Ship, pos: Point2) -> Self { Ship { physicsbody: PhysicsBody::new(pos), - kind, + sprite: SpriteTexture(ct.sprite.clone()), + size: ct.size, + engines: ct.engines.clone(), + engines_on: false, } } } impl Spriteable for Ship { fn get_sprite(&self) -> Sprite { - return Sprite { - pos: (self.physicsbody.pos.x, self.physicsbody.pos.y, 1.0).into(), - texture: self.kind.sprite(), - angle: self.physicsbody.angle, - scale: 1.0, - size: self.kind.size(), + let engines = if self.engines_on { + Some( + self.engines + .iter() + .map(|e| Sprite { + pos: Point3 { + x: e.pos.x, + y: e.pos.y, + z: 1.0, + }, + texture: SpriteTexture("flare::ion".to_owned()), + angle: Deg(0.0), + size: e.size, + children: None, + }) + .collect(), + ) + } else { + None }; + + Sprite { + pos: (self.physicsbody.pos.x, self.physicsbody.pos.y, 1.0).into(), + texture: self.sprite.clone(), // TODO: sprite texture should be easy to clone + angle: self.physicsbody.angle, + size: self.size, + children: engines, + } } } diff --git a/src/game/system.rs b/src/game/system.rs index 2e4f852..270cf7a 100644 --- a/src/game/system.rs +++ b/src/game/system.rs @@ -2,7 +2,7 @@ use cgmath::{Point3, Vector2}; use rand::{self, Rng}; use super::SystemObject; -use crate::{consts, content, render::Sprite, render::SpriteTexture, render::Spriteable}; +use crate::{consts, content, render::Sprite, render::SpriteTexture}; pub struct StarfieldStar { /// Star coordinates, in world space. diff --git a/src/game/systemobject.rs b/src/game/systemobject.rs index f43b2ff..5b73365 100644 --- a/src/game/systemobject.rs +++ b/src/game/systemobject.rs @@ -1,6 +1,6 @@ use cgmath::{Deg, Point3}; -use crate::{render::Sprite, render::SpriteTexture, render::Spriteable}; +use crate::{render::Sprite, render::SpriteTexture}; pub struct SystemObject { pub sprite: SpriteTexture, @@ -9,14 +9,14 @@ pub struct SystemObject { pub angle: Deg, } -impl Spriteable for SystemObject { - fn get_sprite(&self) -> Sprite { +impl SystemObject { + pub(super) fn get_sprite(&self) -> Sprite { return Sprite { texture: self.sprite.clone(), - scale: 1.0, pos: self.pos, angle: self.angle, size: self.size, + children: None, }; } } diff --git a/src/render/consts.rs b/src/render/consts.rs index 9a3affb..9fd4d2f 100644 --- a/src/render/consts.rs +++ b/src/render/consts.rs @@ -1,4 +1,5 @@ use crate::consts; +use cgmath::Matrix4; // We can draw at most this many sprites on the screen. // TODO: compile-time option @@ -16,3 +17,11 @@ pub const TEXTURE_INDEX_PATH: &'static str = "./assets"; /// Shader entry points pub const SHADER_MAIN_VERTEX: &'static str = "vertex_main"; pub const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main"; + +#[rustfmt::skip] +pub const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.5, + 0.0, 0.0, 0.0, 1.0, +); diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index 0758365..d298b8c 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -1,12 +1,12 @@ use anyhow::Result; use bytemuck; -use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2, Vector3}; +use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector2, Vector3}; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::PhysicalSize, window::Window}; use super::{ - consts::{SPRITE_INSTANCE_LIMIT, STARFIELD_INSTANCE_LIMIT}, + consts::{OPENGL_TO_WGPU_MATRIX, SPRITE_INSTANCE_LIMIT, STARFIELD_INSTANCE_LIMIT}, globaldata::{GlobalData, GlobalDataContent}, pipeline::PipelineBuilder, texturearray::TextureArray, @@ -15,6 +15,7 @@ use super::{ types::{SpriteInstance, StarfieldInstance, TexturedVertex}, VertexBuffer, }, + Sprite, }; use crate::{consts, game::Game}; @@ -194,6 +195,138 @@ impl GPUState { self.update_starfield_buffer(game) } + /// Create a SpriteInstance for s and add it to instances. + /// Also handles child sprites. + fn push_sprite( + &self, + game: &Game, + instances: &mut Vec, + clip_ne: Point2, + clip_sw: Point2, + s: Sprite, + ) { + // Position adjusted for parallax + // TODO: adjust parallax for zoom? + let pos: Point2 = { + (Point2 { + x: s.pos.x, + y: s.pos.y, + } - game.camera.pos.to_vec()) + / s.pos.z + }; + let texture = self.texture_array.get_sprite_texture(s.texture); + + // Game dimensions of this sprite post-scale. + // Don't divide by 2, we use this later. + let height = s.size / s.pos.z; + let width = height * texture.aspect; + + // Don't draw (or compute matrices for) + // sprites that are off the screen + if pos.x < clip_ne.x - width + || pos.y > clip_ne.y + height + || pos.x > clip_sw.x + width + || pos.y < clip_sw.y - height + { + return; + } + + // TODO: clean up + let scale = height / game.camera.zoom; + + // Note that our mesh starts centered at (0, 0). + // This is essential---we do not want scale and rotation + // changing our sprite's position! + + // Apply sprite aspect ratio, preserving height. + // This must be done *before* rotation. + // + // We apply the provided scale here as well as a minor optimization + let sprite_aspect_and_scale = + Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0); + + // Apply rotation + let rotate = Matrix4::from_angle_z(s.angle); + + // Apply screen aspect ratio, again preserving height. + // This must be done AFTER rotation... think about it! + let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0); + + // After finishing all ops, translate. + // This must be done last, all other operations + // require us to be at (0, 0). + let translate = Matrix4::from_translation(Vector3 { + x: pos.x / game.camera.zoom / self.window_aspect, + y: pos.y / game.camera.zoom, + z: 0.0, + }); + + // Order matters! + // The rightmost matrix is applied first. + let t = + OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * sprite_aspect_and_scale; + + instances.push(SpriteInstance { + transform: t.into(), + texture_index: texture.index, + }); + + // Add children + if let Some(children) = s.children { + for c in children { + self.push_subsprite(game, instances, c, pos, s.angle); + } + } + } + + /// Add a sprite's subsprite to instance. + /// Only called by push_sprite. + fn push_subsprite( + &self, + game: &Game, + instances: &mut Vec, + s: Sprite, + parent_pos: Point2, + parent_angle: Deg, + ) { + // TODO: clean up + if s.children.is_some() { + panic!("Child sprites must not have child sprites!") + } + + let texture = self.texture_array.get_sprite_texture(s.texture); + let scale = s.size / (s.pos.z * game.camera.zoom); + let sprite_aspect_and_scale = + Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0); + let rotate = Matrix4::from_angle_z(s.angle); + let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0); + + let ptranslate = Matrix4::from_translation(Vector3 { + x: parent_pos.x / game.camera.zoom / self.window_aspect, + y: parent_pos.y / game.camera.zoom, + z: 0.0, + }); + let protate = Matrix4::from_angle_z(parent_angle); + + let translate = Matrix4::from_translation(Vector3 { + x: s.pos.x / game.camera.zoom / self.window_aspect, + y: s.pos.y / game.camera.zoom, + z: 0.0, + }); + + // Order matters! + // The rightmost matrix is applied first. + let t = OPENGL_TO_WGPU_MATRIX + * ptranslate * screen_aspect + * protate * translate + * rotate * sprite_aspect_and_scale; + + instances.push(SpriteInstance { + transform: t.into(), + texture_index: texture.index, + }); + } + /// Make a SpriteInstance for each of the game's visible sprites. /// Will panic if SPRITE_INSTANCE_LIMIT is exceeded. /// @@ -207,45 +340,13 @@ impl GPUState { let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom; for s in game.get_sprites() { - // Compute post-parallax position and distance-adjusted scale. - // We do this here so we can check if a sprite is on the screen. - let pos: Point2 = { - (Point2 { - x: s.pos.x, - y: s.pos.y, - } - game.camera.pos.to_vec()) - / (s.pos.z + game.camera.zoom / consts::ZOOM_MIN) - }; - let texture = self.texture_array.get_sprite_texture(s.texture); - - // Game dimensions of this sprite post-scale. - // Don't divide by 2, we use this later. - let height = s.size * s.scale / s.pos.z; - let width = height * texture.aspect; - - // Don't draw (or compute matrices for) - // sprites that are off the screen - if pos.x < clip_ne.x - width - || pos.y > clip_ne.y + height - || pos.x > clip_sw.x + width - || pos.y < clip_sw.y - height - { - continue; - } - - instances.push(SpriteInstance { - position: pos.into(), - aspect: texture.aspect, - rotation: Matrix2::from_angle(s.angle).into(), - size: height, - texture_index: texture.index, - }) + self.push_sprite(game, &mut instances, clip_ne, clip_sw, s); } // Enforce sprite limit if instances.len() as u64 > SPRITE_INSTANCE_LIMIT { // TODO: no panic, handle this better. - unreachable!("Sprite limit exceeded!") + panic!("Sprite limit exceeded!") } return instances; diff --git a/src/render/mod.rs b/src/render/mod.rs index 30317c5..445ba36 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -12,16 +12,3 @@ pub use sprite::{Sprite, Spriteable}; /// A handle to a sprite texture #[derive(Debug, Clone)] pub struct SpriteTexture(pub String); - -/* -// API correction matrix. -// cgmath uses OpenGL's matrix format, which -// needs to be converted to wgpu's matrix format. -#[rustfmt::skip] -const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 0.5, 0.5, - 0.0, 0.0, 0.0, 1.0, -); -*/ diff --git a/src/render/shaders/sprite.wgsl b/src/render/shaders/sprite.wgsl index 9a5b78a..ea6de70 100644 --- a/src/render/shaders/sprite.wgsl +++ b/src/render/shaders/sprite.wgsl @@ -1,10 +1,9 @@ struct InstanceInput { - @location(2) rotation_matrix_0: vec2, - @location(3) rotation_matrix_1: vec2, - @location(4) position: vec2, - @location(5) size: f32, - @location(6) aspect: f32, - @location(7) texture_idx: u32, + @location(2) transform_matrix_0: vec4, + @location(3) transform_matrix_1: vec4, + @location(4) transform_matrix_2: vec4, + @location(5) transform_matrix_3: vec4, + @location(6) texture_idx: u32, }; struct VertexInput { @@ -47,33 +46,15 @@ fn vertex_main( instance: InstanceInput, ) -> VertexOutput { - // Apply sprite aspect ratio & scale factor - // This must be done *before* rotation. - let scale = instance.size / global.camera_zoom.x; - var pos: vec2 = vec2( - vertex.position.x * instance.aspect * scale, - vertex.position.y * scale - ); - - // Rotate - pos = mat2x2( - instance.rotation_matrix_0, - instance.rotation_matrix_1, - ) * pos; - - // Apply screen aspect ratio, again preserving height. - // This must be done AFTER rotation... think about it! - pos = pos / vec2(global.window_aspect.x, 1.0); - - // Translate - pos = pos + ( - // Don't forget to correct distance for screen aspect ratio too! - (instance.position / global.camera_zoom.x) - / vec2(global.window_aspect.x, 1.0) + let transform = mat4x4( + instance.transform_matrix_0, + instance.transform_matrix_1, + instance.transform_matrix_2, + instance.transform_matrix_3, ); var out: VertexOutput; - out.position = vec4(pos, 0.0, 1.0); + out.position = transform * vec4(vertex.position, 1.0); out.texture_coords = vertex.texture_coords; out.index = instance.texture_idx; return out; diff --git a/src/render/sprite.rs b/src/render/sprite.rs index f597a22..3a85248 100644 --- a/src/render/sprite.rs +++ b/src/render/sprite.rs @@ -13,13 +13,18 @@ pub struct Sprite { /// given as height in world units. pub size: f32, - /// Scale factor. - /// if this is 1, sprite height is exactly self.size. - pub scale: f32, - /// This sprite's rotation /// (relative to north, measured ccw) pub angle: Deg, + + /// Sprites that should be drawn relative to this sprite. + /// Coordinates of sprites in this array will be interpreted + /// as world units, relative to the center of this sprite, + /// before any rotation or scaling. + /// Children rotate with their parent sprite. + /// + /// Note that child sprites may NOT have children. + pub children: Option>, } pub trait Spriteable { diff --git a/src/render/vertexbuffer/types.rs b/src/render/vertexbuffer/types.rs index 3ba17ab..de03ca4 100644 --- a/src/render/vertexbuffer/types.rs +++ b/src/render/vertexbuffer/types.rs @@ -86,20 +86,9 @@ impl BufferObject for StarfieldInstance { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct SpriteInstance { - /// Rotation matrix for this sprite - pub rotation: [[f32; 2]; 2], - - /// World position, relative to camera - /// Note that this does NOT contain z-distance, - /// since sprite parallax and distance scaling - /// is applied beforehand. - pub position: [f32; 2], - - /// Height of (unrotated) sprite in world units - pub size: f32, - - // Sprite aspect ratio (width / height) - pub aspect: f32, + /// Extra transformations this sprite + /// (rotation, etc) + pub transform: [[f32; 4]; 4], // What texture to use for this sprite pub texture_index: u32, @@ -114,39 +103,31 @@ impl BufferObject for SpriteInstance { // instance when the shader starts processing a new instance step_mode: wgpu::VertexStepMode::Instance, attributes: &[ - // 2 arrays = 1 2x2 matrix + // 4 arrays = 1 4x4 matrix wgpu::VertexAttribute { offset: 0, shader_location: 2, - format: wgpu::VertexFormat::Float32x2, + format: wgpu::VertexFormat::Float32x4, }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, - shader_location: 3, - format: wgpu::VertexFormat::Float32x2, - }, - // Position wgpu::VertexAttribute { offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, + shader_location: 3, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, shader_location: 4, - format: wgpu::VertexFormat::Float32x2, + format: wgpu::VertexFormat::Float32x4, }, - // Size wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, shader_location: 5, - format: wgpu::VertexFormat::Float32, - }, - // Aspect - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 7]>() as wgpu::BufferAddress, - shader_location: 6, - format: wgpu::VertexFormat::Float32, + format: wgpu::VertexFormat::Float32x4, }, // Texture wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 7, + offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, + shader_location: 6, format: wgpu::VertexFormat::Uint32, }, ],