twgpu 0.4.1

Render Teeworlds and DDNet maps
Documentation
use crate::buffer::{GpuBuffer, GpuDeviceExt};
use vek::{Aabr, Vec2};
use wgpu::{BufferUsages, Device};

const LABEL: Option<&str> = Some("Camera");

/// Camera that defines a view into a map
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(C)]
pub struct Camera {
    /// Position in map, in tiles
    pub position: Vec2<f32>,
    /// Amount of tiles horizontally and vertically at zoom 1
    pub base_dimensions: Vec2<f32>,
    /// Zoom, e.g. (2.0, 0.5) -> twice as many tiles horizontally, half as many vertically
    pub zoom: Vec2<f32>,
    /// Required for webgl compatibility, total struct size must be a multiple of 16
    _padding: [f32; 2],
    /// For uniform buffer alignment with offsets
    _more_padding: [f32; 24 + 32],
}

pub type GpuCamera = GpuBuffer<Camera>;

unsafe impl bytemuck::Zeroable for Camera {}
unsafe impl bytemuck::Pod for Camera {}

/// Desired amount of sub-tiles (tiles / 32) in default camera view
pub const AMOUNT: u32 = 1150 * 1000;
/// Maximum amount of sub-tiles horizontally
pub const MAX_WIDTH: u32 = 1500;
/// Maximum amount of sub-tiles vertically
pub const MAX_HEIGHT: u32 = 1150;

/// The aspect ratio is width / height
/// Returns the dimension in pixels that Teeworlds/DDNet would for that aspect ratio
fn aspect_ratio_to_dimensions(aspect_ratio: f32) -> Vec2<f32> {
    /*
    width (x), height (y) calculation from the aspect ratio
        x * y = x * y
    <=>     x = (x * y) / y
    <=>   x^2 = (x * y) * (x / y)
    <=>   x^2 = AMOUNT * aspect_ratio
    <=>     x = sqrt(AMOUNT * aspect_ratio)
    */
    let mut width = (AMOUNT as f32 * aspect_ratio).sqrt();
    // x * y = x * y <=> y = x * (y / x) <=> y = x / aspect_ratio
    let mut height = width / aspect_ratio;
    // If a calculated length exceeds the maximum, cap it at the respective maximum
    if width > MAX_WIDTH as f32 {
        width = MAX_WIDTH as f32;
        height = width / aspect_ratio;
    }
    if height > MAX_HEIGHT as f32 {
        height = MAX_HEIGHT as f32;
        width = height * aspect_ratio;
    }
    Vec2::new(width / 32., height / 32.)
}

impl GpuCamera {
    pub fn upload(camera: &Camera, device: &Device) -> Self {
        device.buffer(camera, LABEL, BufferUsages::UNIFORM | BufferUsages::VERTEX)
    }
}

impl Camera {
    pub fn new_with_dimensions(base_dimensions: Vec2<f32>) -> Self {
        Self {
            position: Vec2::new(0., 0.),
            base_dimensions,
            zoom: Vec2::new(1., 1.),
            _padding: [21.; 2],
            _more_padding: [21.; 24 + 32],
        }
    }

    /// The aspect ratio is width / height
    /// Position defaults to (0, 0)
    /// Zoom defaults to (1, 1)
    pub fn new(aspect_ratio: f32) -> Self {
        Self::new_with_dimensions(aspect_ratio_to_dimensions(aspect_ratio))
    }

    /// Supposed to be run whenever the resolution changes
    pub fn switch_aspect_ratio(&mut self, aspect_ratio: f32) {
        self.base_dimensions = aspect_ratio_to_dimensions(aspect_ratio);
    }

    /// Transforms logical coordinates from the camera view into map coordinates **relative** to the camera position
    /// Logical coordinates are (0, 0) at the top-left corner of the camera, (1, 1) at the bottom-right corner
    pub fn relative_map_position(&self, position: Vec2<f32>) -> Vec2<f32> {
        // Move (0, 0) to the center of the camera, where the position is defined
        let adjusted_position = position - Vec2::new(0.5, 0.5);
        // Relative map position from the center of the camera
        adjusted_position * self.base_dimensions * self.zoom
    }

    /// Transforms logical coordinates from the camera view into map coordinates
    /// Logical coordinates are (0, 0) at the top-left corner of the camera, (1, 1) at the bottom-right corner
    pub fn map_position(&self, position: Vec2<f32>) -> Vec2<f32> {
        let relative_position = self.relative_map_position(position);
        // Combine position of camera with the relative position of the point
        self.position + relative_position
    }

    /// Move the camera position so that the specified map_coordinates are at the specified logical coordinates of the camera
    pub fn move_to(&mut self, map_position: Vec2<f32>, logical_position: Vec2<f32>) {
        let relative_position = self.relative_map_position(logical_position);
        self.position = map_position - relative_position;
    }

    /// How many tiles the camera views horizontally and vertically (world coordinates).
    /// Takes into account parallax and zoom.
    pub fn world_perspective_size(&self, parallax: Vec2<f32>) -> Vec2<f32> {
        let parallax_zoom = parallax.x.max(parallax.y);
        let parallax_zoom_factor = Vec2::lerp(Vec2::one(), self.zoom, parallax_zoom);
        self.base_dimensions * parallax_zoom_factor
    }

    /// Projects the camera view onto the world coordinate system.
    /// Returns the rectangle, in world coordinates, which is viewed by the camera.
    /// Takes into account parallax and zoom.
    pub fn world_perspective(&self, parallax: Vec2<f32>) -> Aabr<f32> {
        let tile_counts = self.world_perspective_size(parallax);

        // Position of the camera from the clip's coordinate system (parallax is calculated into position of camera)
        let camera_position = self.position * parallax;
        Aabr {
            min: camera_position - tile_counts / 2.,
            max: camera_position + tile_counts / 2.,
        }
    }
}