use anyhow::{bail, Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use image::{imageops, GenericImageView, ImageBuffer, Rgba, RgbaImage}; use std::{ fs::File, io::{Read, Write}, path::{Path, PathBuf}, }; // TODO: article // TODO: rework texturearray // TODO: reasonable sprite sizes // TODO: consistent naming // TODO: parallelize // spriteatlas: the big images // texture: the same, what we load to wgpu // image: a single file // sprite: a possibly animated texture pub struct AtlasSet { /// The width of each atlas texture_width: u32, /// The height of each atlas texture_height: u32, /// Make at most this many atlases /// Probably determined by the GPU texture_limit: usize, /// Keeps track of image files index: SpriteAtlas, /// Array of textures, grows as needed texture_list: Vec, Vec>>, /// The size of the smallest image that didn't fit in each texture image_max_sizes: Vec<[u32; 2]>, /// (y-value, image size) image_y_start: Vec<(u32, [u32; 2])>, /// A list of used regions in each texture /// Format: ([xpos, ypos], [width, height]) /// in pixels, with (0, 0) at top left used_regions: Vec>, /// Used to calculate packing efficiency used_area: f64, /// The root directory that contains all image files. /// Files outside this directory will not be packed. asset_root: PathBuf, /// Leave an empty border this many pixels wide around each image image_margin: u32, } impl AtlasSet { pub fn new( texture_width: u32, texture_height: u32, texture_limit: usize, asset_root: &Path, image_margin: u32, ) -> Self { Self { asset_root: asset_root.to_path_buf(), texture_width, texture_height, texture_limit, texture_list: Vec::new(), image_max_sizes: Vec::new(), used_regions: Vec::new(), index: SpriteAtlas::new(), used_area: 0f64, image_y_start: Vec::new(), image_margin, } } /// Add a sprite to this atlas set pub fn write_image(&mut self, path: &Path) -> Result { let mut f = File::open(&path)?; let mut bytes = Vec::new(); f.read_to_end(&mut bytes) .with_context(|| format!("While reading file `{}`", path.display()))?; let img = image::load_from_memory(&bytes) .with_context(|| format!("While loading file `{}`", path.display()))?; let image_dim = img.dimensions(); /* TODO: seperate CLI argument to check these. don't check this every run,because sometimes it's necessary. (for animated sprites!) let mut transparent_border = true; for (x, y, c) in img.pixels() { if (x == 0 || y == 0 || x == img.width() - 1 || y == img.height() - 1) && c[3] != 0 { transparent_border = false; break; } } if transparent_border { println!("[WARNING] {} wastes space with a transmparent border",); } */ let dim = [ image_dim.0 + 2 * self.image_margin, image_dim.1 + 2 * self.image_margin, ]; let mut x = 0; let mut y = 0; let mut final_atlas_idx = None; // Loop over atlas textures 'outer: for atlas_idx in 0..self.texture_limit { if atlas_idx >= self.texture_list.len() { // Start a new atlas self.texture_list .push(RgbaImage::new(self.texture_width, self.texture_height)); self.used_regions.push(Vec::new()); self.image_max_sizes.push([u32::MAX, u32::MAX]); self.image_y_start.push((0, [u32::MAX, u32::MAX])); } // Optimization: save the smallest sprite that didn't fit in each atlas, // and don't try to add similarly-sized sprites. if dim[0] >= self.image_max_sizes[atlas_idx][0] && dim[1] >= self.image_max_sizes[atlas_idx][1] { continue 'outer; } x = 0; y = 0; if self.image_y_start.len() != 0 { let (sy, sd) = self.image_y_start[atlas_idx]; if dim[0] >= sd[0] || dim[1] >= sd[1] { y = sy; } } let mut free = false; let mut new; 'inner: while y < self.texture_height && !free { new = ([x, y], dim); free = true; for r in &self.used_regions[atlas_idx] { // If boxes overlap... if r.0[0] < new.0[0] + new.1[0] && r.0[0] + r.1[0] > new.0[0] && r.0[1] < new.0[1] + new.1[1] && r.0[1] + r.1[1] > new.0[1] { // Skip the whole occupied area x = r.1[0] + r.0[0]; if x + dim[0] >= self.texture_width { y += 1; x = 0; } free = false; continue 'inner; } } } if y + dim[1] >= self.texture_height { // This sprite didn't fit, move on to the next atlas self.image_max_sizes[atlas_idx] = [dim[0] / 2, dim[1] / 2]; } else if free { final_atlas_idx = Some(atlas_idx); break; } } let atlas_idx = match final_atlas_idx { None => bail!("textures didn't fit!"), Some(s) => s, }; // We found a spot for this image, write it. imageops::overlay( &mut self.texture_list[atlas_idx], &img, (x + self.image_margin).into(), (y + self.image_margin).into(), ); self.used_regions[atlas_idx].push(([x, y], dim)); self.used_area += dim[0] as f64 * dim[1] as f64; let (sy, sd) = self.image_y_start[atlas_idx]; if dim[0] <= sd[0] && dim[1] <= sd[1] { // Reset start y if both dimensions of this texture are smaller than the previous smallest texture // We check for both, because that ensures that the smaller texture can tile the previous largest one. self.image_y_start[atlas_idx] = (0, [dim[0] / 2, dim[1] / 2]); } else { self.image_y_start[atlas_idx] = (y.max(sy), sd); } let p = path.strip_prefix(&self.asset_root).with_context(|| { format!( "path `{}` is not relative to asset root `{}`", path.display(), self.asset_root.display() ) })?; self.index.index.insert( p.to_path_buf(), SpriteAtlasImage { atlas: atlas_idx as u32, x: (x + self.image_margin) as f32 / self.texture_width as f32, y: (y + self.image_margin) as f32 / self.texture_height as f32, w: image_dim.0 as f32 / self.texture_width as f32, h: image_dim.1 as f32 / self.texture_height as f32, }, ); return Ok(atlas_idx); } pub fn save_files(mut self, atlas_path: F, index_path: &Path) -> Result<()> where F: Fn(usize) -> PathBuf, { // Save atlases for i in 0..self.texture_list.len() { let path = atlas_path(i); self.texture_list[i].save(&path)?; self.index .atlas_list .push(path.file_name().unwrap().to_str().unwrap().to_owned()); } // Save index let toml = toml::to_string(&self.index)?; let mut f = File::create(index_path)?; f.write_all(toml.as_bytes())?; return Ok(()); } /* pub fn get_width(&self) -> u32 { self.texture_width } pub fn get_height(&self) -> u32 { self.texture_height } pub fn get_limit(&self) -> usize { self.texture_limit } pub fn get_used_area(&self) -> f64 { self.used_area } pub fn num_textures(&self) -> usize { self.texture_list.len() } */ pub fn get_efficiency(&self) -> f64 { self.used_area / (self.texture_height as f64 * self.texture_width as f64 * self.texture_list.len() as f64) } }