diff --git a/src/doodad.rs b/src/doodad.rs index 82cb204..f123aa2 100644 --- a/src/doodad.rs +++ b/src/doodad.rs @@ -1,6 +1,6 @@ use cgmath::{Deg, Point2}; -use crate::{physics::Pfloat, Camera, Sprite, Spriteable}; +use crate::{physics::Pfloat, Sprite, Spriteable}; pub struct Doodad { pub sprite: String, @@ -8,10 +8,9 @@ pub struct Doodad { } impl Spriteable for Doodad { - fn sprite(&self, camera: Camera) -> Sprite { + fn sprite(&self) -> Sprite { return Sprite { - position: self.pos, - camera: camera, + pos: self.pos, name: self.sprite.clone(), angle: Deg { 0: 0.0 }, scale: 1.0, diff --git a/src/main.rs b/src/main.rs index f53a77f..82cdc2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ struct Camera { } trait Spriteable { - fn sprite(&self, camera: Camera) -> Sprite; + fn sprite(&self) -> Sprite; } struct Sprite { @@ -44,16 +44,13 @@ struct Sprite { name: String, // This object's position, in world coordinates. - position: Point2, + pos: Point2, scale: Pfloat, // This sprite's rotation // (relative to north, measured ccw) angle: Deg, - - // The camera we want to draw this sprite from - camera: Camera, } struct Game { @@ -119,8 +116,8 @@ impl Game { fn sprites(&self) -> Vec { let mut sprites: Vec = Vec::new(); - sprites.append(&mut self.system.sprites(self.camera)); - sprites.push(self.player.sprite(self.camera)); + sprites.append(&mut self.system.sprites()); + sprites.push(self.player.sprite()); return sprites; } @@ -141,7 +138,7 @@ pub async fn run() -> Result<()> { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { gpu.update(); game.update(); - match gpu.render(&game.sprites()) { + match gpu.render(&game.sprites(), game.camera) { Ok(_) => {} // Reconfigure the surface if lost Err(wgpu::SurfaceError::Lost) => gpu.resize(gpu.size), diff --git a/src/render/bufferdata.rs b/src/render/bufferdata.rs new file mode 100644 index 0000000..31748fc --- /dev/null +++ b/src/render/bufferdata.rs @@ -0,0 +1,89 @@ +use std::mem; + +// Represents a textured vertex in WGSL +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex { + pub position: [f32; 3], + pub texture_coords: [f32; 2], +} + +impl Vertex { + pub fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + ], + } + } +} + +// Represents a sprite instance in WGSL +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct SpriteInstance { + // All transformations we need to put this sprite in its place + pub transform: [[f32; 4]; 4], + + // Which texture we should use for this sprite + // (see TextureArray) + pub texture_index: u32, +} + +impl SpriteInstance { + // Number of bytes used to store this data. + // Should match desc() below. + pub const SIZE: u64 = 20; + + pub fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + // We need to switch from using a step mode of Vertex to Instance + // This means that our shaders will only change to use the next + // instance when the shader starts processing a new instance + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + // A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot + // for each vec4. We'll have to reassemble the mat4 in the shader. + wgpu::VertexAttribute { + offset: 0, + // While our vertex shader only uses locations 0, and 1 now, in later tutorials, we'll + // be using 2, 3, and 4, for Vertex. We'll start at slot 5, not conflict with them later + shader_location: 5, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, + shader_location: 7, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, + shader_location: 8, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, + shader_location: 9, + format: wgpu::VertexFormat::Uint32, + }, + ], + } + } +} diff --git a/src/render/gpu.rs b/src/render/gpustate.rs similarity index 52% rename from src/render/gpu.rs rename to src/render/gpustate.rs index 09a1bd9..ad02067 100644 --- a/src/render/gpu.rs +++ b/src/render/gpustate.rs @@ -1,12 +1,18 @@ use anyhow::Result; use bytemuck; -use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3}; -use std::{iter, mem}; +use cgmath::{EuclideanSpace, Point2}; +use std::iter; use wgpu::{self, util::DeviceExt}; use winit::{self, window::Window}; -use super::texturearray::TextureArray; -use crate::Sprite; +use crate::{Camera, Sprite}; + +use super::{ + bufferdata::{SpriteInstance, Vertex}, + texturearray::TextureArray, + util::Transform, + SPRITE_MESH_INDICES, SPRITE_MESH_VERTICES, +}; pub struct GPUState { device: wgpu::Device, @@ -25,186 +31,6 @@ pub struct GPUState { instance_buffer: wgpu::Buffer, } -struct Instance { - transform: Transform, - texture_index: u32, -} -impl Instance { - fn to_raw(&self) -> InstanceRaw { - InstanceRaw { - model: (self.transform.to_matrix()).into(), - texture_index: self.texture_index, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct InstanceRaw { - model: [[f32; 4]; 4], - texture_index: u32, -} - -impl InstanceRaw { - fn get_size() -> u64 { - 20 - } - - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - // We need to switch from using a step mode of Vertex to Instance - // This means that our shaders will only change to use the next - // instance when the shader starts processing a new instance - step_mode: wgpu::VertexStepMode::Instance, - attributes: &[ - // A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot - // for each vec4. We'll have to reassemble the mat4 in the shader. - wgpu::VertexAttribute { - offset: 0, - // While our vertex shader only uses locations 0, and 1 now, in later tutorials, we'll - // be using 2, 3, and 4, for Vertex. We'll start at slot 5, not conflict with them later - shader_location: 5, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 6, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 7, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, - shader_location: 8, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, - shader_location: 9, - format: wgpu::VertexFormat::Uint32, - }, - ], - } - } -} - -/// 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, -); - -struct Transform { - pos: Point2, // position on screen - screen_aspect: f32, // width / height. Screen aspect ratio. - aspect: f32, // width / height. Sprite aspect ratio. - scale: f32, // if scale = 1, this sprite will be as tall as the screen. - rotate: Deg, // Around this object's center, in degrees measured ccw from vertical -} - -impl Transform { - /// Build a matrix that corresponds to this transformation. - fn to_matrix(&self) -> Matrix4 { - // 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. - let sprite_aspect = Matrix4::from_nonuniform_scale(self.aspect, 1.0, 1.0); - - // Apply provided scale - let scale = Matrix4::from_scale(self.scale); - - // Apply rotation - let rotate = Matrix4::from_angle_z(self.rotate); - - // 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.screen_aspect, 1.0, 1.0); - - // After finishing all op, translate. - // This must be done last, all other operations - // require us to be at (0, 0). - let translate = Matrix4::from_translation(Vector3 { - x: self.pos.x, - y: self.pos.y, - z: 0.0, - }); - - // Order matters! - // The rightmost matrix is applied first. - return OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale * sprite_aspect; - } -} - -// Datatype for vertex buffer -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 3], - tex_coords: [f32; 2], -} - -impl Vertex { - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - ], - } - } -} - -// These vertices form a rectangle that covers the whole screen. -// Two facts are important to note: -// - This is centered at (0, 0), so scaling doesn't change a sprite's position -// - At scale = 1, this covers the whole screen. Makes scale calculation easier. -// -// Screen coordinates range from -1 to 1, with the origin at the center. -// Texture coordinates range from 0 to 1, with the origin at the top-left -// and (1,1) at the bottom-right. -const VERTICES: &[Vertex] = &[ - Vertex { - position: [-1.0, 1.0, 0.0], - tex_coords: [0.0, 0.0], - }, - Vertex { - position: [1.0, 1.0, 0.0], - tex_coords: [1.0, 0.0], - }, - Vertex { - position: [1.0, -1.0, 0.0], - tex_coords: [1.0, 1.0], - }, - Vertex { - position: [-1.0, -1.0, 0.0], - tex_coords: [0.0, 1.0], - }, -]; - -const INDICES: &[u16] = &[0, 3, 2, 0, 2, 1]; - impl GPUState { // We can draw at most this many sprites on the screen. // TODO: compile-time option @@ -305,7 +131,7 @@ impl GPUState { vertex: wgpu::VertexState { module: &shader, entry_point: "vertex_shader_main", - buffers: &[Vertex::desc(), InstanceRaw::desc()], + buffers: &[Vertex::desc(), SpriteInstance::desc()], }, fragment: Some(wgpu::FragmentState { module: &shader, @@ -339,20 +165,20 @@ impl GPUState { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("vertex buffer"), - contents: bytemuck::cast_slice(VERTICES), + contents: bytemuck::cast_slice(SPRITE_MESH_VERTICES), usage: wgpu::BufferUsages::VERTEX, }); let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("vertex index buffer"), - contents: bytemuck::cast_slice(INDICES), + contents: bytemuck::cast_slice(SPRITE_MESH_INDICES), usage: wgpu::BufferUsages::INDEX, }); let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("instance buffer"), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - size: InstanceRaw::get_size() * Self::SPRITE_LIMIT, + size: SpriteInstance::SIZE * Self::SPRITE_LIMIT, mapped_at_creation: false, }); @@ -386,7 +212,11 @@ impl GPUState { pub fn update(&mut self) {} - pub fn render(&mut self, sprites: &Vec) -> Result<(), wgpu::SurfaceError> { + pub fn render( + &mut self, + sprites: &Vec, + camera: Camera, + ) -> Result<(), wgpu::SurfaceError> { let output = self.surface.get_current_texture()?; let view = output .texture @@ -423,22 +253,49 @@ impl GPUState { // (it may not be square!) let screen_aspect = self.size.width as f32 / self.size.height as f32; - // TODO: warning when too many sprites are drawn. - let mut instances: Vec = Vec::new(); + // Game coordinates (relative to camera) of ne and sw corners of screen. + // Used to skip off-screen sprites. + let clip_ne = Point2::from((-1.0, 1.0)) * camera.zoom; + let clip_sw = Point2::from((1.0, -1.0)) * camera.zoom; + + let mut instances: Vec = Vec::new(); for s in sprites { - // Compute position on screen, - // using logical pixels - let screen_pos: Point2 = (s.position - s.camera.pos.to_vec()) / s.camera.zoom; + let pos = s.pos - camera.pos.to_vec(); let texture = self.texture_array.get_texture(&s.name[..]); - instances.push(Instance { + // Game dimensions of this sprite post-scale + // We really need height / 2 to check if we're on the screen, + // but we omit the division so we get a small "margin" + // and so we can re-use this value. + let height = texture.height * s.scale; + let width = height * texture.aspect; + + // Sprite scale is relative to the sprite's defined height, + // so we apply both factors here + + // Don't 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 + { + println!("skip {}", s.name); + continue; + } + + let scale = height / camera.zoom; + let screen_pos: Point2 = pos / camera.zoom; + + instances.push(SpriteInstance { transform: Transform { pos: screen_pos, aspect: texture.aspect, screen_aspect, - scale: s.scale * (texture.height / s.camera.zoom), rotate: s.angle, - }, + scale, + } + .to_matrix() + .into(), texture_index: texture.index, }) } @@ -450,19 +307,19 @@ impl GPUState { } // Write new sprite data to buffer - let instance_data: Vec<_> = instances.iter().map(Instance::to_raw).collect(); - self.queue.write_buffer( - &self.instance_buffer, - 0, - bytemuck::cast_slice(&instance_data), - ); + self.queue + .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances)); render_pass.set_pipeline(&self.render_pipeline); render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); - render_pass.draw_indexed(0..INDICES.len() as u32, 0, 0..instances.len() as _); + render_pass.draw_indexed( + 0..SPRITE_MESH_INDICES.len() as u32, + 0, + 0..instances.len() as _, + ); // begin_render_pass borrows encoder mutably, so we can't call finish() // without dropping this variable. diff --git a/src/render/mod.rs b/src/render/mod.rs index 79ae029..33afad8 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,6 +1,54 @@ -mod gpu; +mod bufferdata; +mod gpustate; mod rawtexture; mod texturearray; +mod util; -pub use gpu::GPUState; +pub use gpustate::GPUState; pub use texturearray::Texture; + +use self::bufferdata::Vertex; +use cgmath::Matrix4; + +/// 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, +); + +// The surface we draw sprites on. +// Every sprite is an instance of this. +// +// These vertices form a rectangle that covers the whole screen. +// Two facts are important to note: +// - This is centered at (0, 0), so scaling doesn't change a sprite's position +// - At scale = 1, this covers the whole screen. Makes scale calculation easier. +// +// Screen coordinates range from -1 to 1, with the origin at the center. +// Texture coordinates range from 0 to 1, with the origin at the top-left +// and (1,1) at the bottom-right. +const SPRITE_MESH_VERTICES: &[Vertex] = &[ + Vertex { + position: [-1.0, 1.0, 0.0], + texture_coords: [0.0, 0.0], + }, + Vertex { + position: [1.0, 1.0, 0.0], + texture_coords: [1.0, 0.0], + }, + Vertex { + position: [1.0, -1.0, 0.0], + texture_coords: [1.0, 1.0], + }, + Vertex { + position: [-1.0, -1.0, 0.0], + texture_coords: [0.0, 1.0], + }, +]; + +const SPRITE_MESH_INDICES: &[u16] = &[0, 3, 2, 0, 2, 1]; diff --git a/src/render/util.rs b/src/render/util.rs new file mode 100644 index 0000000..19748bd --- /dev/null +++ b/src/render/util.rs @@ -0,0 +1,53 @@ +use cgmath::{Deg, Matrix4, Point2, Vector3}; + +use super::OPENGL_TO_WGPU_MATRIX; + +// Represents a sprite tranformation. +// +// This produces a single matrix we can apply to a brand-new sprite instance +// to put it in the right place, at the right angle, with the right scale. +pub struct Transform { + pub pos: Point2, // position on screen + pub screen_aspect: f32, // width / height. Screen aspect ratio. + pub aspect: f32, // width / height. Sprite aspect ratio. + pub scale: f32, // if scale = 1, this sprite will be as tall as the screen. + pub rotate: Deg, // Around this object's center, in degrees measured ccw from vertical +} + +impl Transform { + /// Build the matrix that corresponds to this transformation. + pub fn to_matrix(&self) -> Matrix4 { + // 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(self.aspect * self.scale, self.scale, 1.0); + + // Apply rotation + let rotate = Matrix4::from_angle_z(self.rotate); + + // 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.screen_aspect, 1.0, 1.0); + + // After finishing all op, translate. + // This must be done last, all other operations + // require us to be at (0, 0). + let translate = Matrix4::from_translation(Vector3 { + x: self.pos.x, + y: self.pos.y, + z: 0.0, + }); + + // Order matters! + // The rightmost matrix is applied first. + return OPENGL_TO_WGPU_MATRIX + * translate * screen_aspect + * rotate * sprite_aspect_and_scale; + } +} diff --git a/src/ship.rs b/src/ship.rs index 8561e0a..f811eac 100644 --- a/src/ship.rs +++ b/src/ship.rs @@ -1,10 +1,6 @@ use cgmath::Point2; -use crate::physics::Pfloat; -use crate::physics::PhysBody; -use crate::Camera; -use crate::Sprite; -use crate::Spriteable; +use crate::{physics::Pfloat, physics::PhysBody, Sprite, Spriteable}; pub enum ShipKind { Gypsum, @@ -33,10 +29,9 @@ impl Ship { } impl Spriteable for Ship { - fn sprite(&self, camera: Camera) -> Sprite { + fn sprite(&self) -> Sprite { return Sprite { - position: self.body.pos, - camera: camera, + pos: self.body.pos, name: self.kind.sprite().to_owned(), angle: self.body.angle, scale: 1.0, diff --git a/src/system.rs b/src/system.rs index 3b30d90..477c519 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,4 +1,4 @@ -use crate::{physics::Polar, Camera, Doodad, Sprite, Spriteable}; +use crate::{physics::Polar, Doodad, Sprite, Spriteable}; use cgmath::Deg; pub struct System { @@ -32,7 +32,7 @@ impl System { return s; } - pub fn sprites(&self, camera: Camera) -> Vec { - return self.bodies.iter().map(|x| x.sprite(camera)).collect(); + pub fn sprites(&self) -> Vec { + return self.bodies.iter().map(|x| x.sprite()).collect(); } }