use crate::error::{MesherError, Result};
use crate::resource_pack::TextureData;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct AtlasRegion {
pub u_min: f32,
pub v_min: f32,
pub u_max: f32,
pub v_max: f32,
}
impl AtlasRegion {
pub fn width(&self) -> f32 {
self.u_max - self.u_min
}
pub fn height(&self) -> f32 {
self.v_max - self.v_min
}
pub fn transform_uv(&self, u: f32, v: f32) -> [f32; 2] {
[
self.u_min + u * self.width(),
self.v_min + v * self.height(),
]
}
}
#[derive(Debug)]
pub struct TextureAtlas {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
pub regions: HashMap<String, AtlasRegion>,
}
impl TextureAtlas {
pub fn get_region(&self, texture_path: &str) -> Option<&AtlasRegion> {
self.regions.get(texture_path)
}
pub fn contains(&self, texture_path: &str) -> bool {
self.regions.contains_key(texture_path)
}
pub fn empty() -> Self {
Self {
width: 16,
height: 16,
pixels: vec![255; 16 * 16 * 4], regions: HashMap::new(),
}
}
pub fn to_png(&self) -> Result<Vec<u8>> {
use image::{ImageBuffer, Rgba};
let img: ImageBuffer<Rgba<u8>, _> =
ImageBuffer::from_raw(self.width, self.height, self.pixels.clone())
.ok_or_else(|| MesherError::AtlasBuild("Failed to create image buffer".to_string()))?;
let mut bytes = Vec::new();
let mut cursor = std::io::Cursor::new(&mut bytes);
img.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|e| MesherError::AtlasBuild(format!("Failed to encode PNG: {}", e)))?;
Ok(bytes)
}
}
pub struct AtlasBuilder {
max_size: u32,
padding: u32,
textures: HashMap<String, TextureData>,
}
impl AtlasBuilder {
pub fn new(max_size: u32, padding: u32) -> Self {
Self {
max_size,
padding,
textures: HashMap::new(),
}
}
pub fn add_texture(&mut self, path: String, texture: TextureData) {
self.textures.insert(path, texture);
}
pub fn build(self) -> Result<TextureAtlas> {
if self.textures.is_empty() {
return Ok(TextureAtlas::empty());
}
let padding = self.padding;
let max_size = self.max_size;
let mut textures: Vec<_> = self.textures.into_iter().collect();
textures.sort_by(|a, b| b.1.height.cmp(&a.1.height));
let total_area: u32 = textures
.iter()
.map(|(_, t)| (t.width + padding * 2) * (t.height + padding * 2))
.sum();
let min_size = (total_area as f64).sqrt().ceil() as u32;
let mut atlas_size = 64u32;
while atlas_size < min_size && atlas_size < max_size {
atlas_size *= 2;
}
loop {
if atlas_size > max_size {
return Err(MesherError::AtlasBuild(format!(
"Failed to pack {} textures into {}x{} atlas",
textures.len(),
max_size,
max_size
)));
}
if let Some((pixels, regions)) = try_pack(&textures, atlas_size, padding) {
return Ok(TextureAtlas {
width: atlas_size,
height: atlas_size,
pixels,
regions,
});
}
atlas_size *= 2;
}
}
}
fn try_pack(
textures: &[(String, TextureData)],
atlas_size: u32,
padding: u32,
) -> Option<(Vec<u8>, HashMap<String, AtlasRegion>)> {
let mut pixels = vec![0u8; (atlas_size * atlas_size * 4) as usize];
let mut regions = HashMap::new();
let mut current_x = 0u32;
let mut current_y = 0u32;
let mut row_height = 0u32;
for (path, texture) in textures {
let tex_width = texture.width + padding * 2;
let tex_height = texture.height + padding * 2;
if current_x + tex_width > atlas_size {
current_x = 0;
current_y += row_height;
row_height = 0;
}
if current_y + tex_height > atlas_size {
return None;
}
let x = current_x + padding;
let y = current_y + padding;
for ty in 0..texture.height {
for tx in 0..texture.width {
let src_idx = ((ty * texture.width + tx) * 4) as usize;
let dst_x = x + tx;
let dst_y = y + ty;
let dst_idx = ((dst_y * atlas_size + dst_x) * 4) as usize;
if src_idx + 4 <= texture.pixels.len() && dst_idx + 4 <= pixels.len() {
pixels[dst_idx..dst_idx + 4]
.copy_from_slice(&texture.pixels[src_idx..src_idx + 4]);
}
}
}
let region = AtlasRegion {
u_min: x as f32 / atlas_size as f32,
v_min: y as f32 / atlas_size as f32,
u_max: (x + texture.width) as f32 / atlas_size as f32,
v_max: (y + texture.height) as f32 / atlas_size as f32,
};
regions.insert(path.clone(), region);
current_x += tex_width;
row_height = row_height.max(tex_height);
}
Some((pixels, regions))
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_texture(width: u32, height: u32, color: [u8; 4]) -> TextureData {
let pixels: Vec<u8> = (0..width * height)
.flat_map(|_| color.iter().copied())
.collect();
TextureData::new(width, height, pixels)
}
#[test]
fn test_empty_atlas() {
let builder = AtlasBuilder::new(256, 0);
let atlas = builder.build().unwrap();
assert_eq!(atlas.width, 16);
assert_eq!(atlas.height, 16);
assert!(atlas.regions.is_empty());
}
#[test]
fn test_single_texture_atlas() {
let mut builder = AtlasBuilder::new(256, 0);
builder.add_texture(
"test".to_string(),
create_test_texture(16, 16, [255, 0, 0, 255]),
);
let atlas = builder.build().unwrap();
assert!(atlas.regions.contains_key("test"));
let region = atlas.get_region("test").unwrap();
assert!(region.u_min >= 0.0);
assert!(region.u_max <= 1.0);
assert!(region.v_min >= 0.0);
assert!(region.v_max <= 1.0);
}
#[test]
fn test_multiple_textures() {
let mut builder = AtlasBuilder::new(256, 1);
builder.add_texture(
"red".to_string(),
create_test_texture(16, 16, [255, 0, 0, 255]),
);
builder.add_texture(
"green".to_string(),
create_test_texture(16, 16, [0, 255, 0, 255]),
);
builder.add_texture(
"blue".to_string(),
create_test_texture(16, 16, [0, 0, 255, 255]),
);
let atlas = builder.build().unwrap();
assert_eq!(atlas.regions.len(), 3);
assert!(atlas.contains("red"));
assert!(atlas.contains("green"));
assert!(atlas.contains("blue"));
}
#[test]
fn test_atlas_region_transform() {
let region = AtlasRegion {
u_min: 0.25,
v_min: 0.5,
u_max: 0.5,
v_max: 0.75,
};
let [u, v] = region.transform_uv(0.0, 0.0);
assert!((u - 0.25).abs() < 0.001);
assert!((v - 0.5).abs() < 0.001);
let [u, v] = region.transform_uv(1.0, 1.0);
assert!((u - 0.5).abs() < 0.001);
assert!((v - 0.75).abs() < 0.001);
}
}