summaryrefslogtreecommitdiff
path: root/render/src/camera.rs
diff options
context:
space:
mode:
Diffstat (limited to 'render/src/camera.rs')
-rw-r--r--render/src/camera.rs187
1 files changed, 187 insertions, 0 deletions
diff --git a/render/src/camera.rs b/render/src/camera.rs
new file mode 100644
index 0000000..ecece90
--- /dev/null
+++ b/render/src/camera.rs
@@ -0,0 +1,187 @@
+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::<CameraUniform>() 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()),
+ );
+ }
+}