diff options
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/square.rs | 2 | ||||
| -rw-r--r-- | shaders/sprite.wgsl | 16 | ||||
| -rw-r--r-- | src/camera.rs | 95 | ||||
| -rw-r--r-- | src/config.rs | 2 | ||||
| -rw-r--r-- | src/instance.rs | 4 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/renderer.rs | 82 |
8 files changed, 187 insertions, 17 deletions
@@ -12,6 +12,7 @@ wgpu = "0.13" thiserror = "1" pollster = "0.2" bytemuck = { version = "1.4", features = ["derive"] } +cgmath = "0.18" [[example]] name = "black" diff --git a/examples/square.rs b/examples/square.rs index 078d90a..90e2fdb 100644 --- a/examples/square.rs +++ b/examples/square.rs @@ -10,7 +10,7 @@ fn main() { let config = RenderWindowConfig { title: "Pokemon: Black and White (New Edition)", instance_capacity: 1, - default_width: NonZeroU32::new(480).unwrap(), + default_width: NonZeroU32::new(640).unwrap(), default_height: NonZeroU32::new(480).unwrap(), ..Default::default() }; diff --git a/shaders/sprite.wgsl b/shaders/sprite.wgsl index b8ce42b..2358f43 100644 --- a/shaders/sprite.wgsl +++ b/shaders/sprite.wgsl @@ -1,4 +1,7 @@ +@group(0) @binding(0) +var<uniform> camera: mat4x4<f32>; + struct VertexInput { @location(0) position: vec2<f32> } @@ -7,7 +10,7 @@ struct InstanceInput { @location(1) position: vec2<f32>, @location(2) size: vec2<f32>, @location(3) rotation: f32, - @location(4) z_layer: i32 + @location(4) z_layer: u32 } struct VertexOutput { @@ -25,13 +28,16 @@ fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { let rotated = rotation * model.position; // scale the sprite - let x = rotated[0] * instance.size[0]; - let y = rotated[1] * instance.size[1]; + let scaled = rotated * instance.size; // move the sprite - let position = vec2<f32>(x, y) + instance.position; + let position2d = scaled + instance.position; + + // camera stuff + let position4d = vec4<f32>(position2d, f32(instance.z_layer), 1.0); + out.clip_position = camera * position4d; + //out.clip_position = position4d; - out.clip_position = vec4<f32>(position, f32(instance.z_layer), 1.0); return out; } diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..2eb1730 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,95 @@ +use cgmath::{Matrix4, Vector3}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Camera { + position: (f32, f32), + zoom: f32, + rotation: f32, + inverse_aspect_ratio: f32, +} + +pub(crate) type CameraUniform = [[f32; 4]; 4]; + +#[allow(clippy::cast_precision_loss)] +fn inverse_aspect_ratio(width: u32, height: u32) -> f32 { + (height as f32) / (width as f32) +} + +impl Camera { + /// Create a new camera, with a position of (0, 0), and a zoom of 1.0 + pub(crate) fn from_size(width: u32, height: u32) -> Self { + Self { + position: (0.0, 0.0), + zoom: 1.0, + rotation: 0.0, + inverse_aspect_ratio: inverse_aspect_ratio(width, height), + } + } + + /// Get the camera's current x position + #[must_use] + pub const fn x(&self) -> f32 { + self.position.0 + } + + /// Get the camera's current y position + #[must_use] + pub const fn y(&self) -> f32 { + self.position.1 + } + + /// Get the camera's current zoom + #[must_use] + pub const fn zoom(&self) -> f32 { + self.zoom + } + + /// Set the position of the camera + pub fn set_position(&mut self, x: f32, y: f32) { + self.position = (x, y); + } + + /// Set the aspect ratio of the camera + pub fn set_zoom(&mut self, zoom: f32) { + self.zoom = zoom; + } + + /// Set the aspect ratio of the camera + pub(crate) fn set_size(&mut self, width: u32, height: u32) { + self.inverse_aspect_ratio = inverse_aspect_ratio(width, height); + } + + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_matrix(&mut self) -> [[f32; 4]; 4] { + let cos_theta = self.rotation.cos(); + let sin_theta = self.rotation.sin(); + + let x_axis = Vector3::new(cos_theta, -sin_theta, 0.0); + let y_axis = Vector3::new(sin_theta, cos_theta, 0.0); + let z_axis = Vector3::new(0.0, 0.0, 1.0); + + let eye = Vector3::new(self.position.0, self.position.1, 0.0); + let x_dot = -cgmath::dot(x_axis, eye); + let y_dot = -cgmath::dot(y_axis, eye); + let z_dot = -cgmath::dot(z_axis, eye); + + #[rustfmt::skip] + let view_matrix = Matrix4::new( + x_axis.x, y_axis.x, z_axis.x, 0.0, + x_axis.y, y_axis.y, z_axis.y, 0.0, + x_axis.x, y_axis.y, z_axis.z, 0.0, + x_dot, y_dot, z_dot, 1.0 + ); + + #[rustfmt::skip] + // TODO implement more scaling coordinate systems + let projection_matrix = Matrix4::new( + self.inverse_aspect_ratio, 0.0, 0.0, 0.0, + 0.0, self.zoom, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + + (projection_matrix * view_matrix).into() + } +} diff --git a/src/config.rs b/src/config.rs index 312cf91..7eedb21 100644 --- a/src/config.rs +++ b/src/config.rs @@ -80,7 +80,7 @@ impl<'a> Default for RenderWindowConfig<'a> { title: "Alligator Game", low_power: true, vsync: true, - instance_capacity: 0, + instance_capacity: 0, // TODO this should probably be bigger } } } diff --git a/src/instance.rs b/src/instance.rs index 554d02d..6ea5321 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -15,7 +15,7 @@ pub struct Instance { /// Rotation, in radians pub rotation: f32, /// z-index - pub z_index: i32, + pub z_index: u32, } impl Default for Instance { @@ -32,7 +32,7 @@ impl Default for Instance { impl Instance { // whenever this is updated, please also update `sprite.wgsl` const ATTRIBUTES: [wgpu::VertexAttribute; 4] = - wgpu::vertex_attr_array![1 => Float32x2, 2 => Float32x2, 3 => Float32, 4 => Sint32]; + wgpu::vertex_attr_array![1 => Float32x2, 2 => Float32x2, 3 => Float32, 4 => Uint32]; pub(crate) fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { // make sure these two don't conflict @@ -4,11 +4,13 @@ #![warn(clippy::nursery)] #![allow(clippy::module_name_repetitions)] +pub mod camera; pub mod config; pub mod instance; pub mod renderer; mod vertex; +pub(crate) use camera::Camera; pub use config::RenderWindowConfig; pub use instance::Instance; pub use renderer::Renderer; diff --git a/src/renderer.rs b/src/renderer.rs index a86b05b..5820f1c 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,9 @@ use std::{convert::TryInto, mem::size_of, num::NonZeroU32}; -use crate::{instance::InstanceId, vertex::SQUARE, Instance, RenderWindowConfig, Vertex}; +use crate::{ + camera::CameraUniform, instance::InstanceId, vertex::SQUARE, Camera, Instance, + RenderWindowConfig, Vertex, +}; use pollster::FutureExt; use thiserror::Error; use wgpu::{include_wgsl, util::DeviceExt}; @@ -48,6 +51,9 @@ pub struct Renderer { instance_buffer: wgpu::Buffer, instance_buffer_size: usize, instances: Vec<Instance>, + camera: Camera, + camera_buffer: wgpu::Buffer, + camera_bind_group: wgpu::BindGroup, window: Window, } @@ -78,17 +84,13 @@ impl Renderer { fn sprite_render_pipeline( device: &wgpu::Device, texture_format: wgpu::TextureFormat, + render_pipeline_layout: &wgpu::PipelineLayout, ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl")); - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Sprite Render Pipeline Layout"), - ..Default::default() - }); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Sprite Render Pipeline"), - layout: Some(&render_pipeline_layout), + layout: Some(render_pipeline_layout), // information about the vertex shader vertex: wgpu::VertexState { module: &shader, @@ -183,8 +185,51 @@ impl Renderer { ); surface.configure(&device, &surface_config); + // create the camera + let width = window.inner_size().width; + let height = window.inner_size().height; + let camera = Camera::from_size(width, height); + let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Camera Uniform"), + size: size_of::<CameraUniform>() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let camera_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Camera Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Camera Bind Group"), + layout: &camera_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Sprite Render Pipeline Layout"), + bind_group_layouts: &[&camera_bind_group_layout], + push_constant_ranges: &[], + }); + // set up a pipeline for sprite rendering - let render_pipeline = Self::sprite_render_pipeline(&device, surface_config.format); + let render_pipeline = + Self::sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout); // the vertex buffer used for rendering squares let square_vertices = SQUARE @@ -214,6 +259,9 @@ impl Renderer { instance_buffer, instance_buffer_size, instances, + camera, + camera_buffer, + camera_bind_group, window, }) } @@ -231,6 +279,7 @@ impl Renderer { self.surface_config.height = size.height; self.surface_config.width = size.width; + self.camera.set_size(size.width, size.height); self.reconfigure(); } @@ -278,6 +327,11 @@ impl Renderer { InstanceId(index) } + /// Get an immutable reference to an instance + pub fn instance(&self, id: InstanceId) -> Option<&Instance> { + self.instances.get(id.0) + } + /// Get a mutable reference to an instance pub fn instance_mut(&mut self, id: InstanceId) -> Option<&mut Instance> { self.instances.get_mut(id.0) @@ -288,6 +342,14 @@ impl Renderer { self.instances.clear(); } + fn refresh_camera_buffer(&mut self) { + self.queue.write_buffer( + &self.camera_buffer, + 0 as wgpu::BufferAddress, + bytemuck::cast_slice(&self.camera.to_matrix()), + ); + } + /// Renders a new frame to the window /// /// # Errors @@ -295,6 +357,7 @@ impl Renderer { /// A number of problems could occur here. A timeout could occur while /// trying to acquire the next frame. There may also be no more memory left /// that can be used for the new frame. + // TODO this is too big fn render(&mut self) -> Result<(), wgpu::SurfaceError> { // the new texture we can render to let output = self.surface.get_current_texture()?; @@ -316,6 +379,8 @@ impl Renderer { .try_into() .expect("expected less than 3 billion instances"); + self.refresh_camera_buffer(); + { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), @@ -331,6 +396,7 @@ impl Renderer { }); render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.camera_bind_group, &[]); render_pass.set_vertex_buffer(0, self.square_vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); render_pass.draw(0..self.square_vertices, 0..num_instances); |
