use std::mem::size_of; use cgmath::{Matrix4, Vector2}; #[derive(Debug)] pub struct Camera { position: (f32, f32), zoom: f32, rotation: f32, inverse_aspect_ratio: f32, buffer: wgpu::Buffer, bind_group: wgpu::BindGroup, } 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 new( device: &wgpu::Device, width: u32, height: u32, ) -> (Self, wgpu::BindGroupLayout) { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Camera Uniform"), size: size_of::() as wgpu::BufferAddress, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let 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 bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Camera Bind Group"), layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding(), }], }); ( Self { position: (0.0, 0.0), zoom: 1.0, rotation: 0.0, inverse_aspect_ratio: inverse_aspect_ratio(width, height), buffer, bind_group, }, bind_group_layout, ) } /// 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 } /// Get the camera's current rotation, in radians #[must_use] pub const fn rotation(&self) -> f32 { self.rotation } /// Set the position of the camera pub fn set_position(&mut self, x: f32, y: f32) { #[cfg(debug_assertions)] if !(-1000.0..1000.0).contains(&x) || !(-1000.0..1000.0).contains(&y) { log::warn!( "The position of the camera is (x: {}, y: {}). \ Please keep both the x and y positions above -1000 and below 1000 units. \ Otherwise, everything will look crazy. \ For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", x, y ); } self.position = (x, y); } /// Set the zoom of the camera pub fn set_zoom(&mut self, zoom: f32) { #[cfg(debug_assertions)] if !(-1000.0..1000.0).contains(&zoom) { log::warn!( "The zoom of the camera is {}. \ Please keep above -1000, and below 1000, or else smooth zoom may be difficult. \ For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", zoom ); } self.zoom = zoom; } /// Set the camera rotation, in radians pub fn set_rotation(&mut self, rotation: f32) { self.rotation = rotation % std::f32::consts::TAU; } /// 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); } /// Create a matrix that can be multiplied by any vector to transform it /// according to the current camera #[allow(clippy::wrong_self_convention)] fn to_matrix(&self) -> CameraUniform { let cos = self.rotation.cos(); let sin = self.rotation.sin(); let x_axis = Vector2::new(cos, -sin); let y_axis = Vector2::new(sin, cos); let eye = Vector2::new(self.position.0, self.position.1); let x_dot = -cgmath::dot(x_axis, eye); let y_dot = -cgmath::dot(y_axis, eye); #[rustfmt::skip] let view_matrix = Matrix4::new( x_axis.x, y_axis.x, 0.0, 0.0, x_axis.y, y_axis.y, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x_dot, y_dot, 0.0, 1.0 ); #[rustfmt::skip] // TODO implement more scaling coordinate systems let projection_matrix = Matrix4::new( self.inverse_aspect_ratio * self.zoom, 0.0, 0.0, 0.0, 0.0, self.zoom, 0.0, 0.0, 0.0, 0.0, 1.0 / 256.0, 0.0, 0.0, 0.0, 0.0, 1.0 ); let transform = projection_matrix * view_matrix; transform.into() } /// Get the bind group for the camera pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { &self.bind_group } /// Refresh the camera buffer for the next frame #[profiling::function] pub(crate) fn refresh(&self, queue: &wgpu::Queue) { queue.write_buffer( &self.buffer, 0 as wgpu::BufferAddress, bytemuck::cast_slice(&self.to_matrix()), ); } }