twgpu 0.4.1

Render Teeworlds and DDNet maps
Documentation
use crate::buffer::GpuBuffer;
use crate::map::EnvelopeSampling;
use crate::shared::Corner;
use std::mem;
use twmap::{Tile, TileFlags, TilesLayer};
use vek::az::UnwrappedAs;
use vek::{Aabr, Rgba, Vec2};
use wgpu::util::{DeviceExt, TextureDataOrder};
use wgpu::{
    vertex_attr_array, Device, Extent3d, Queue, Texture, TextureDescriptor, TextureDimension,
    TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, VertexAttribute,
    VertexBufferLayout, VertexStepMode,
};

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

#[derive(Debug, Default, Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)]
#[repr(C)]
pub struct TilemapCorner {
    pub clip_pos: Vec2<f32>,
    pub world_pos: Vec2<f32>,
    pub color: Rgba<f32>,
    pub tilemap_bounds: Vec2<f32>,
    pub mipmap_level: f32,
}

impl TilemapCorner {
    pub const ATTRIBUTES: [VertexAttribute; 3] =
        vertex_attr_array![0 => Float32x4, 1 => Float32x4, 2 => Float32x3];

    pub fn vertex_buffer_layout() -> VertexBufferLayout<'static> {
        VertexBufferLayout {
            array_stride: mem::size_of::<Self>().unwrapped_as(),
            step_mode: VertexStepMode::Vertex,
            attributes: &Self::ATTRIBUTES,
        }
    }
}

pub struct GpuTilemapData {
    pub tilemap: Texture,
    pub color: Rgba<f32>,
    pub color_env: Option<u16>,
    pub color_env_offset: i32,
    pub tile_texture_size: f32,
    pub tilemap_bounds: Vec2<f32>,
    pub bounding_box: Aabr<f32>,
}

fn tile_to_gpu(tile: &Tile) -> [u8; 2] {
    let id = tile.id;
    let mut uv_transform_index = tile.flags;
    uv_transform_index.set(TileFlags::OPAQUE, tile.flags.contains(TileFlags::ROTATE));
    [id, uv_transform_index.bits() & 0b111]
}

pub fn bounding_box(layer: &TilesLayer) -> Aabr<f32> {
    let tiles = layer.tiles.unwrap_ref();
    let rows: Vec<_> = tiles.rows().into_iter().collect();
    let top = match rows
        .iter()
        .position(|row| row.iter().any(|tile| tile.id != 0))
    {
        Some(0) => f32::NEG_INFINITY,
        Some(pos) => pos as f32,
        None => return Aabr::new_empty(Vec2::zero()), // No non-air tiles in layer
    };
    let bottom = match rows
        .iter()
        .rev()
        .position(|row| row.iter().any(|tile| tile.id != 0))
        .unwrap()
    {
        0 => f32::INFINITY,
        pos => (rows.len() - pos) as f32,
    };

    let columns: Vec<_> = tiles.columns().into_iter().collect();
    let left = match columns
        .iter()
        .position(|column| column.iter().any(|tile| tile.id != 0))
        .unwrap()
    {
        0 => f32::NEG_INFINITY,
        pos => pos as f32,
    };
    let right = match columns
        .iter()
        .rev()
        .position(|column| column.iter().any(|tile| tile.id != 0))
        .unwrap()
    {
        0 => f32::INFINITY,
        pos => (columns.len() - pos) as f32,
    };
    Aabr {
        min: Vec2::new(left, top),
        max: Vec2::new(right, bottom),
    }
}

impl GpuTilemapData {
    pub fn view(&self) -> TextureView {
        self.tilemap.create_view(&TextureViewDescriptor::default())
    }

    pub fn upload(
        layer: &TilesLayer,
        images: &[twmap::Image],
        device: &Device,
        queue: &Queue,
    ) -> Self {
        let tiles = layer.tiles.unwrap_ref();
        let (height, width) = tiles.dim();
        let data: Vec<[u8; 2]> = tiles.iter().map(tile_to_gpu).collect();
        let tilemap = device.create_texture_with_data(
            queue,
            &TextureDescriptor {
                label: LABEL,
                size: Extent3d {
                    width: width.unwrapped_as(),
                    height: height.unwrapped_as(),
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: TextureDimension::D2,
                format: TextureFormat::Rg8Uint,
                usage: TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            },
            TextureDataOrder::LayerMajor,
            bytemuck::cast_slice(&data),
        );
        let tile_texture_size = match layer.image {
            None => 1.,
            Some(index) => images[index as usize].size().w as f32 / 16.,
        };
        let tilemap_bounds = Vec2::new(width, height).az::<f32>() - Vec2::one();
        let color = layer.color.az::<f32>() / u8::MAX as f32;

        Self {
            tilemap,
            color,
            color_env: layer.color_env,
            color_env_offset: layer.color_env_offset,
            tile_texture_size,
            tilemap_bounds,
            bounding_box: bounding_box(layer),
        }
    }

    pub fn update(
        &self,
        vertices: &GpuBuffer<[TilemapCorner; 4]>,
        group_projection: &Aabr<f32>,
        tiles_on_screen: Vec2<f32>,
        render_target_size: Vec2<u32>,
        envelopes: &[twmap::Envelope],
        client_time: i64,
        server_time: i64,
        queue: &Queue,
    ) {
        let clipped_map_position = group_projection.intersection(self.bounding_box);
        let viewport_map_range = group_projection.max - group_projection.min;
        let map_to_clip_pos = |map_pos: Vec2<f32>| {
            let rel_map_pos = map_pos - group_projection.min;
            let normalized_map_pos = rel_map_pos / viewport_map_range;
            (normalized_map_pos - 0.5) * 2. * Vec2::new(1., -1.)
        };
        let clipped_viewport_position = Aabr {
            min: map_to_clip_pos(clipped_map_position.min),
            max: map_to_clip_pos(clipped_map_position.max),
        };
        let color = match self.color_env {
            None => self.color,
            Some(index) => {
                self.color
                    * envelopes[index as usize].sample(
                        client_time,
                        server_time,
                        self.color_env_offset,
                    )
            }
        };
        let tile_pixels = tiles_on_screen * self.tile_texture_size;
        let pixel_ratio = tile_pixels / render_target_size.az::<f32>();
        let uncapped_mipmap_level = pixel_ratio.x.min(pixel_ratio.y).log2();
        let mipmap_level = uncapped_mipmap_level.clamp(0., self.tile_texture_size.log2());
        let new_vertices = [
            Corner::TopLeft,
            Corner::TopRight,
            Corner::BottomLeft,
            Corner::BottomRight,
        ]
        .map(|corner| TilemapCorner {
            clip_pos: corner.select_corner(&clipped_viewport_position),
            world_pos: corner.select_corner(&clipped_map_position),
            color,
            tilemap_bounds: self.tilemap_bounds,
            mipmap_level,
        });
        vertices.update(&new_vertices, queue);
    }
}