summaryrefslogtreecommitdiff
path: root/src/camera.rs
blob: ecece9073820f39d8be6250175b89900b90f2a92 (plain)
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()),
		);
	}
}