diff --git a/Cargo.toml b/Cargo.toml index 4317883..35b5313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ thiserror = "1.0.61" hashbrown = { version = "0.14.5", features = ["rayon"] } stretto = "0.8.4" rayon = "1.10.0" +sys-info = "0.7.0" clap = {version = "4.5.9", features = ["derive"] } [dev-dependencies] diff --git a/examples/output/0.jpg b/examples/output/0.jpg new file mode 100644 index 0000000..0f3a490 Binary files /dev/null and b/examples/output/0.jpg differ diff --git a/examples/output/0.png b/examples/output/0.png deleted file mode 100644 index a7cde49..0000000 Binary files a/examples/output/0.png and /dev/null differ diff --git a/examples/test_pack.rs b/examples/test_pack.rs index c2a25d1..a36b4c8 100644 --- a/examples/test_pack.rs +++ b/examples/test_pack.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::Instant; +use atlas_packer::texture::{CroppedTexture, TextureSizeCache}; use rayon::prelude::*; use atlas_packer::{ @@ -38,7 +39,7 @@ fn main() { (0.3, 0.8), (0.2, 0.7), ]; - let path_string: String = format!("examples/assets/{}.png", j); + let path_string: String = format!("./examples/assets/{}.png", j); let image_path = PathBuf::from(path_string.as_str()); polygons.push(Polygon { id: format!("texture_{}_{}", i, j), @@ -59,39 +60,44 @@ fn main() { let exporter = JpegAtlasExporter::default(); let packer = Mutex::new(TexturePacker::new(placer, exporter)); - // Texture cache - let texture_cache = TextureCache::new(100_000_000); - - let start = Instant::now(); + let packing_start = Instant::now(); - // Add textures to the atlas + // cache image size + let texture_size_cache = TextureSizeCache::new(); + // place textures on the atlas polygons.par_iter().for_each(|polygon| { - let texture = texture_cache.get_or_insert( - &polygon.uv_coords, + let place_start = Instant::now(); + let texture_size = texture_size_cache.get_or_insert(&polygon.texture_uri); + let cropped_texture = CroppedTexture::new( &polygon.texture_uri, - &polygon.downsample_factor.value(), + texture_size, + &polygon.uv_coords, + polygon.downsample_factor.clone(), ); + let _ = packer .lock() .unwrap() - .add_texture(polygon.id.clone(), texture); - // println!("{:?}", info); + .add_texture(polygon.id.clone(), cropped_texture); + let place_duration = place_start.elapsed(); + println!("{}, texture place process {:?}", polygon.id, place_duration); }); - let duration = start.elapsed(); - println!("atlas process {:?}", duration); - let mut packer = packer.into_inner().unwrap(); packer.finalize(); + let duration = packing_start.elapsed(); + println!("all packing process {:?}", duration); + let start = Instant::now(); - let output_dir = Path::new("examples/output/"); + // Caches the original textures for exporting to an atlas. + let texture_cache = TextureCache::new(100_000_000); + let output_dir = Path::new("./examples/output/"); packer.export(output_dir, &texture_cache, config.width(), config.height()); - let duration = start.elapsed(); - println!("atlas export process {:?}", duration); + println!("all atlas export process {:?}", duration); let duration = all_process_start.elapsed(); println!("all process {:?}", duration); diff --git a/examples/test_pack_dice.rs b/examples/test_pack_dice.rs deleted file mode 100644 index 77f286d..0000000 --- a/examples/test_pack_dice.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -use atlas_packer::{ - export::PngAtlasExporter, - pack::TexturePacker, - place::{GuillotineTexturePlacer, TexturePlacerConfig}, - texture::{DownsampleFactor, TextureCache}, -}; - -#[derive(Debug, Clone)] -struct Polygon { - id: String, - uv_coords: Vec<(f64, f64)>, - texture_uri: PathBuf, - downsample_factor: DownsampleFactor, -} - -fn main() { - let faces = [ - ( - "yellow_dice", - vec![ - (0.316406, 0.816406), - (-0.000000, 0.628906), - (0.316406, 0.628906), - ], - ), - ( - "yellow_dice", - vec![ - (0.500000, 1.000000), - (0.816406, 0.816406), - (0.816406, 1.000000), - ], - ), - ( - "yellow_dice", - vec![ - (0.500000, 0.816406), - (0.816406, 0.628906), - (0.816406, 0.816406), - ], - ), - ( - "yellow_dice", - vec![ - (0.500000, 0.628906), - (0.816406, 0.445312), - (0.816406, 0.628906), - ], - ), - ( - "yellow_dice", - vec![ - (0.816406, 0.816406), - (1.000000, 0.628906), - (0.816406, 0.628906), - ], - ), - ( - "yellow_dice", - vec![ - (0.500000, 0.816406), - (0.316406, 0.628906), - (0.500000, 0.628906), - ], - ), - ( - "blue_dice", - vec![ - (0.500000, 0.329309), - (0.327473, 0.656008), - (0.327473, 0.329309), - ], - ), - ( - "blue_dice", - vec![ - (0.500000, 0.656008), - (0.327473, 0.984543), - (0.327473, 0.656008), - ], - ), - ( - "blue_dice", - vec![ - (0.826699, 0.656008), - (0.999226, 0.329309), - (0.999226, 0.656008), - ], - ), - ( - "blue_dice", - vec![ - (0.500000, 0.000774), - (0.327473, 0.329309), - (0.327473, 0.000774), - ], - ), - ( - "blue_dice", - vec![ - (0.327473, 0.656008), - (0.327473, 0.329309), - (0.000774, 0.656008), - ], - ), - ( - "blue_dice", - vec![ - (0.500000, 0.656008), - (0.826699, 0.329309), - (0.826699, 0.656008), - ], - ), - ( - "red_dice", - vec![ - (0.371094, 0.871094), - (-0.000000, 0.742188), - (0.371094, 0.742188), - ], - ), - ( - "red_dice", - vec![ - (0.500000, 1.000000), - (0.871094, 0.871094), - (0.871094, 1.000000), - ], - ), - ( - "red_dice", - vec![ - (0.500000, 0.871094), - (0.871094, 0.742188), - (0.871094, 0.871094), - ], - ), - ( - "red_dice", - vec![ - (0.500000, 0.742188), - (0.871094, 0.613281), - (0.871094, 0.742188), - ], - ), - ( - "red_dice", - vec![ - (0.871094, 0.871094), - (1.000000, 0.742188), - (0.871094, 0.742188), - ], - ), - ( - "red_dice", - vec![ - (0.500000, 0.871094), - (0.371094, 0.742188), - (0.500000, 0.742188), - ], - ), - ]; - - let material_to_texture = HashMap::from([ - ("blue_dice", "blue_dice.png"), - ("red_dice", "red_dice.png"), - ("yellow_dice", "yellow_dice.png"), - ]); - - // 3D Tiles Sink passes the texture path and UV coordinates for each polygon - let mut polygons: Vec = Vec::new(); - let downsample_factor = 1.0; - - for (idx, (material, uv_coords)) in faces.iter().enumerate() { - let texture_file = material_to_texture.get(material).unwrap(); - let path_string = format!("examples/assets/dice/{}", texture_file); - let image_path = PathBuf::from(path_string); - polygons.push(Polygon { - id: format!("texture_{}_{}", material, idx), - uv_coords: uv_coords.iter().map(|&(u, v)| (u, v)).collect(), - texture_uri: image_path, - downsample_factor: DownsampleFactor::new(&downsample_factor), - }); - } - - // initialize texture packer - let config = TexturePlacerConfig::new(500, 500, 1); - let placer = GuillotineTexturePlacer::new(config.clone()); - let exporter = PngAtlasExporter::default(); - let mut packer = TexturePacker::new(placer, exporter); - - // Texture cache - let texture_cache = TextureCache::new(100_000_000); - - // Add textures to the atlas, - polygons.iter().for_each(|polygon| { - let texture = texture_cache.get_or_insert( - &polygon.uv_coords, - &polygon.texture_uri, - &polygon.downsample_factor.value(), - ); - let info = packer.add_texture(polygon.id.clone(), texture); - println!("{:?}", info); - }); - - packer.finalize(); - - let output_dir = Path::new("examples/output/"); - packer.export(output_dir, &texture_cache, config.width(), config.height()); -} diff --git a/src/place.rs b/src/place.rs index 60936e2..d594405 100644 --- a/src/place.rs +++ b/src/place.rs @@ -1,4 +1,3 @@ -use std::cmp::max; use std::collections::HashMap; use crate::texture::CroppedTexture; @@ -78,104 +77,6 @@ pub trait TexturePlacer: Send + Sync { } } -pub struct SimpleTexturePlacer { - pub config: TexturePlacerConfig, - pub current_x: u32, - pub current_y: u32, - pub max_height_in_row: u32, -} - -impl SimpleTexturePlacer { - pub fn new(config: TexturePlacerConfig) -> Self { - SimpleTexturePlacer { - config, - current_x: 0, - current_y: 0, - max_height_in_row: 0, - } - } -} - -impl TexturePlacer for SimpleTexturePlacer { - fn config(&self) -> &TexturePlacerConfig { - &self.config - } - - fn place_texture( - &mut self, - id: &str, - texture: &CroppedTexture, - parent_atlas_id: &str, - ) -> PlacedTextureInfo { - let (scaled_width, scaled_height) = self.scale_dimensions( - texture.width, - texture.height, - texture.downsample_factor.value(), - ); - - if self.current_x + texture.width > self.config().width { - self.current_x = 0; - self.current_y += self.max_height_in_row + self.config().padding; - self.max_height_in_row = 0; - } - - let placed_uv_coords = texture - .cropped_uv_coords - .iter() - .map(|(u, v)| { - ( - self.current_x as f64 / self.config().width as f64 + u * scaled_width as f64, - self.current_y as f64 / self.config().height as f64 + v * scaled_height as f64, - ) - }) - .collect::>(); - - let texture_info = PlacedTextureInfo { - id: id.to_string(), - atlas_id: parent_atlas_id.to_string(), - origin: (self.current_x, self.current_y), - width: scaled_width, - height: scaled_height, - placed_uv_coords, - }; - - self.current_x += texture.width + self.config().padding; - self.max_height_in_row = self.max_height_in_row.max(texture.height); - - texture_info - } - - fn can_place(&self, texture: &CroppedTexture) -> bool { - let (scaled_width, scaled_height) = self.scale_dimensions( - texture.width, - texture.height, - texture.downsample_factor.value(), - ); - - let padding = self.config().padding; - let max_width = self.config().width; - let max_height = self.config().height; - - let next_x = self.current_x + scaled_width + padding; - let next_y = max( - self.current_y + scaled_height + padding, - self.current_y + self.max_height_in_row + padding, - ); - - if next_x <= max_width && next_y <= max_height { - true - } else { - next_y + scaled_height + padding <= max_height - } - } - - fn reset_param(&mut self) { - self.current_x = 0; - self.current_y = 0; - self.max_height_in_row = 0; - } -} - pub struct GuillotineTexturePlacer { config: TexturePlacerConfig, free_rects: Vec, @@ -206,105 +107,97 @@ impl GuillotineTexturePlacer { } fn find_best_rect(&self, width: u32, height: u32) -> Option { - let mut best_rect = None; - let mut best_area = u32::MAX; - - for rect in &self.free_rects { - if rect.width >= width && rect.height >= height { - let area = rect.width * rect.height; - if area < best_area { - best_rect = Some(*rect); - best_area = area; - } - } - } - - best_rect + self.free_rects + .iter() + .filter(|&rect| rect.width >= width && rect.height >= height) + .min_by_key(|&rect| rect.width * rect.height) + .cloned() } fn split_rect(&mut self, rect: Rect, placed: &PlacedTextureInfo) { - let shorter_axis_split = rect.width <= rect.height; - - if shorter_axis_split { - let right_rect = Rect { - x: rect.x + placed.width + self.config.padding, - y: rect.y, - width: rect.width - placed.width - self.config.padding, - height: placed.height, - }; - - let bottom_rect = Rect { - x: rect.x, - y: rect.y + placed.height + self.config.padding, - width: rect.width, - height: rect.height - placed.height - self.config.padding, - }; - - if right_rect.width > 0 && right_rect.height > 0 { - self.free_rects.push(right_rect); - } - if bottom_rect.width > 0 && bottom_rect.height > 0 { - self.free_rects.push(bottom_rect); - } + let padding = self.config.padding; + let (right_rect, bottom_rect) = if rect.width <= rect.height { + ( + Rect { + x: rect.x + placed.width + padding, + y: rect.y, + width: rect.width - placed.width - padding, + height: placed.height, + }, + Rect { + x: rect.x, + y: rect.y + placed.height + padding, + width: rect.width, + height: rect.height - placed.height - padding, + }, + ) } else { - let right_rect = Rect { - x: rect.x + placed.width + self.config.padding, - y: rect.y, - width: rect.width - placed.width - self.config.padding, - height: rect.height, - }; - - let bottom_rect = Rect { - x: rect.x, - y: rect.y + placed.height + self.config.padding, - width: placed.width, - height: rect.height - placed.height - self.config.padding, - }; + ( + Rect { + x: rect.x + placed.width + padding, + y: rect.y, + width: rect.width - placed.width - padding, + height: rect.height, + }, + Rect { + x: rect.x, + y: rect.y + placed.height + padding, + width: placed.width, + height: rect.height - placed.height - padding, + }, + ) + }; - if right_rect.width > 0 && right_rect.height > 0 { - self.free_rects.push(right_rect); - } - if bottom_rect.width > 0 && bottom_rect.height > 0 { - self.free_rects.push(bottom_rect); - } + if right_rect.width > 0 && right_rect.height > 0 { + self.free_rects.push(right_rect); + } + if bottom_rect.width > 0 && bottom_rect.height > 0 { + self.free_rects.push(bottom_rect); } } fn merge_free_rects(&mut self) { let mut i = 0; while i < self.free_rects.len() { + let mut merged = false; + let rect1 = self.free_rects[i]; let mut j = i + 1; while j < self.free_rects.len() { - let rect1 = self.free_rects[i]; let rect2 = self.free_rects[j]; - - if rect1.x == rect2.x - && rect1.width == rect2.width - && rect1.y + rect1.height == rect2.y - { - self.free_rects[i] = Rect { - x: rect1.x, - y: rect1.y, - width: rect1.width, - height: rect1.height + rect2.height, - }; - self.free_rects.remove(j); - } else if rect1.y == rect2.y - && rect1.height == rect2.height - && rect1.x + rect1.width == rect2.x - { - self.free_rects[i] = Rect { - x: rect1.x, - y: rect1.y, - width: rect1.width + rect2.width, - height: rect1.height, - }; - self.free_rects.remove(j); - } else { - j += 1; + if let Some(merged_rect) = Self::try_merge_rects(rect1, rect2) { + self.free_rects[i] = merged_rect; + self.free_rects.swap_remove(j); + merged = true; + break; } + j += 1; } - i += 1; + if !merged { + i += 1; + } + } + } + + fn try_merge_rects(rect1: Rect, rect2: Rect) -> Option { + if rect1.x == rect2.x && rect1.width == rect2.width && rect1.y + rect1.height == rect2.y { + Some(Rect { + x: rect1.x, + y: rect1.y, + width: rect1.width, + height: rect1.height + rect2.height, + }) + } else if rect1.y == rect2.y + && rect1.height == rect2.height + && rect1.x + rect1.width == rect2.x + { + Some(Rect { + x: rect1.x, + y: rect1.y, + width: rect1.width + rect2.width, + height: rect1.height, + }) + } else { + None } } @@ -366,6 +259,7 @@ impl TexturePlacer for GuillotineTexturePlacer { placed } else { + // todo: Consideration of processing when the texture is larger than the atlas size panic!("Texture could not be placed: {}", id); } } diff --git a/src/texture.rs b/src/texture.rs index e980901..c7a9336 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -3,9 +3,10 @@ use std::{ sync::mpsc, }; -use image::{DynamicImage, GenericImageView, ImageBuffer}; +use image::{DynamicImage, GenericImageView, ImageBuffer, ImageReader}; use rayon::prelude::*; use stretto::Cache; +use sys_info::mem_info; #[derive(Debug, Clone)] pub struct DownsampleFactor(f32); @@ -24,38 +25,61 @@ impl DownsampleFactor { } } -pub struct TextureCache { - cache: Cache, +// Cache for storing the only size of the image +pub struct TextureSizeCache { + cache: Cache, } -impl TextureCache { - pub fn new(capacity: usize) -> Self { - TextureCache { - cache: Cache::new(capacity, 1_000_000_000).unwrap(), +impl TextureSizeCache { + pub fn new() -> Self { + TextureSizeCache { + cache: Cache::new(1_000_000, 1_000_000).unwrap(), } } - pub fn get_or_insert( - &self, - uv_coords: &[(f64, f64)], - image_path: &PathBuf, - downsample_factor: &f32, - ) -> CroppedTexture { + pub fn get_or_insert(&self, image_path: &PathBuf) -> (u32, u32) { match self.cache.get(image_path) { - Some(image) => { - let image = image.value(); - CroppedTexture::new(uv_coords, image_path, image, downsample_factor) - } + Some(size) => *size.value(), None => { - let image = image::open(image_path).unwrap_or_else(|_| { - panic!("Failed to open image file {}", image_path.display()) - }); - let cost = image.width() * image.height() * image.color().bytes_per_pixel() as u32; - self.cache - .insert(image_path.to_path_buf(), image.clone(), cost as i64); + let size = get_image_size(image_path).unwrap(); + // Since it only retains the size of the texture, set the cost to 1 for everything. + let cost = 1; + self.cache.insert(image_path.to_path_buf(), size, cost); self.cache.wait().unwrap(); - CroppedTexture::new(uv_coords, image_path, &image, downsample_factor) + size + } + } + } +} + +impl Default for TextureSizeCache { + fn default() -> Self { + TextureSizeCache::new() + } +} + +impl Drop for TextureSizeCache { + fn drop(&mut self) { + self.cache.close().unwrap(); + } +} + +// Cache for storing the image +pub struct TextureCache { + cache: Cache, +} + +impl TextureCache { + pub fn new(capacity: usize) -> Self { + let default_capacity = get_cache_size().unwrap(); + if capacity == 0 { + TextureCache { + cache: Cache::new(default_capacity, 2_000_000_000).unwrap(), + } + } else { + TextureCache { + cache: Cache::new(capacity, 2_000_000_000).unwrap(), } } } @@ -76,12 +100,29 @@ impl TextureCache { } } +fn get_cache_size() -> Result { + const MIN_CACHE_SIZE: usize = 100 * 1024 * 1024; // 100MB + const MAX_CACHE_SIZE: usize = 2 * 1024 * 1024 * 1024; // 2GB + + match mem_info() { + Ok(mem) => { + let total_memory = mem.total as usize * 1024; + // 15% of total memory + let cache_size = (total_memory as f64 * 0.15) as usize; + Ok(cache_size.clamp(MIN_CACHE_SIZE, MAX_CACHE_SIZE)) + } + + Err(e) => Err(format!("Failed to retrieve memory information.: {}", e)), + } +} + impl Drop for TextureCache { fn drop(&mut self) { self.cache.close().unwrap(); } } +// A structure that retains an image cut out from the original image. pub struct CroppedTexture { pub image_path: PathBuf, // The origin of the cropped image in the original image (top-left corner). @@ -95,38 +136,17 @@ pub struct CroppedTexture { impl CroppedTexture { pub fn new( - uv_coords: &[(f64, f64)], image_path: &Path, - image: &DynamicImage, - downsample_factor: &f32, + size: (u32, u32), + uv_coords: &[(f64, f64)], + downsample_factor: DownsampleFactor, ) -> Self { - let downsample_factor = DownsampleFactor::new(downsample_factor); - - let (width, height) = image.dimensions(); - - // UV to pixel coordinates with clamping - let pixel_coords: Vec<(u32, u32)> = uv_coords - .iter() - .map(|(u, v)| { - ( - (u.clamp(0.0, 1.0) * width as f64).min(width as f64 - 1.0) as u32, - ((1.0 - v.clamp(0.0, 1.0)) * height as f64).min(height as f64 - 1.0) as u32, - ) - }) - .collect(); - - // calc bbox - let (min_x, min_y, max_x, max_y) = pixel_coords.iter().fold( - (u32::MAX, u32::MAX, 0, 0), - |(min_x, min_y, max_x, max_y), (x, y)| { - (min_x.min(*x), min_y.min(*y), max_x.max(*x), max_y.max(*y)) - }, - ); + let pixel_coords = uv_to_pixel_coords(uv_coords, size.0, size.1); + let (min_x, min_y, max_x, max_y) = calc_bbox(&pixel_coords); let cropped_width = max_x - min_x; let cropped_height = max_y - min_y; - // UV coordinates for the cropped image let dest_uv_coords = pixel_coords .iter() .map(|(x, y)| { @@ -244,3 +264,32 @@ fn is_point_inside_polygon(test_point: (f64, f64), polygon: &[(f64, f64)]) -> bo is_inside } + +// utils + +fn get_image_size>(file_path: P) -> Result<(u32, u32), image::ImageError> { + let reader = ImageReader::open(file_path)?; + let dimensions = reader.into_dimensions()?; + Ok(dimensions) +} + +fn uv_to_pixel_coords(uv_coords: &[(f64, f64)], width: u32, height: u32) -> Vec<(u32, u32)> { + uv_coords + .iter() + .map(|(u, v)| { + ( + (u.clamp(0.0, 1.0) * width as f64).min(width as f64 - 1.0) as u32, + ((1.0 - v.clamp(0.0, 1.0)) * height as f64).min(height as f64 - 1.0) as u32, + ) + }) + .collect() +} + +fn calc_bbox(pixel_coords: &[(u32, u32)]) -> (u32, u32, u32, u32) { + pixel_coords.iter().fold( + (u32::MAX, u32::MAX, 0, 0), + |(min_x, min_y, max_x, max_y), (x, y)| { + (min_x.min(*x), min_y.min(*y), max_x.max(*x), max_y.max(*y)) + }, + ) +}