rezcraft 0.2.0

Minecraft like game written in rust using wgpu, supporting both native and wasm
Documentation
use std::{collections::HashMap, path::Path};

use image::{DynamicImage, ImageBuffer, Rgb, RgbImage};

use crate::{engine::resource::Texture, game::world::TextureID, misc::loader::load_resource_binary};

pub struct TextureAtlas {
    texture_buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
    offset: HashMap<TextureID, (u32, u32)>,
    atlas_size: (u32, u32),
}

impl TextureAtlas {
    pub async fn new(texture_names: &[String], texture_folder: &impl AsRef<Path>) -> Self {
        let mut images: HashMap<&str, ImageBuffer<Rgb<u8>, Vec<u8>>> = HashMap::default();

        let (mut last_width, mut last_height) = (0, 0);
        for texture_name in texture_names {
            let img = load_image(texture_name.clone(), texture_folder).await;

            if last_width != 0 && last_height != 0 {
                assert!(
                    (last_width == img.width()) && (last_height == img.height()),
                    "All textures must have same size"
                );
            }

            last_width = img.width();
            last_height = img.height();

            images.insert(texture_name, img);
        }

        let texture_width = (images.len() as f32).sqrt().ceil() as u32;
        let texture_height = texture_width;

        let mut offset = HashMap::default();
        let mut images_iter = images.into_iter();
        let mut texture_buffer = RgbImage::new(texture_width * last_width, texture_height * last_height);

        for x in 0..texture_width {
            for y in 0..texture_height {
                if let Some((texture_name, image)) = images_iter.next() {
                    offset.insert(texture_name.into(), (x, y));
                    for image_x in 0..image.width() {
                        for image_y in 0..image.height() {
                            texture_buffer.put_pixel(
                                image_x + x * image.width(),
                                image_y + y * image.height(),
                                *image.get_pixel(image_x, image_y),
                            )
                        }
                    }
                }
            }
        }

        Self {
            texture_buffer,
            offset,
            atlas_size: (texture_width, texture_height),
        }
    }

    pub fn load_texture(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Texture {
        Texture::from_image(
            device,
            queue,
            &DynamicImage::ImageRgb8(self.texture_buffer.clone()),
            Some("TextureAtlas"),
        )
        .expect("Failed creating TextureAtlas")
    }

    pub fn texture_coordinates(&self, texture: &TextureID) -> (f32, f32) {
        let coords = self.offset[texture];
        (
            coords.0 as f32 / self.atlas_size.0 as f32,
            coords.1 as f32 / self.atlas_size.1 as f32,
        )
    }

    pub fn atlas_size(&self) -> (f32, f32) {
        (self.atlas_size.0 as f32, self.atlas_size.1 as f32)
    }

    pub fn tile_size(&self) -> (f32, f32) {
        let atlas_size = self.atlas_size();
        (1.0 / atlas_size.0, 1.0 / atlas_size.1)
    }

    pub fn clone_without_image(&self) -> Self {
        Self {
            texture_buffer: ImageBuffer::new(1, 1),
            offset: self.offset.clone(),
            atlas_size: self.atlas_size,
        }
    }
}

async fn load_image(texture_name: String, texture_folder: &impl AsRef<Path>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    let path = texture_folder.as_ref().join(texture_name.clone()).with_extension("png");

    let bytes =
        load_resource_binary(&path).unwrap_or_else(|_| panic!("Failed to load texture: {texture_name:?} - {path:?}"));

    image::load_from_memory(&bytes)
        .unwrap_or_else(|_| panic!("Failed to parse {texture_name:?} - {path:?} as image"))
        .to_rgb8()
}