#[derive(Debug, Clone)]
pub struct Texture {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
impl Texture {
pub fn from_rgba(data: Vec<u8>, width: u32, height: u32) -> Self {
assert_eq!(data.len(), (width * height * 4) as usize);
Self { data, width, height }
}
pub fn solid(r: u8, g: u8, b: u8) -> Self {
Self {
data: vec![r, g, b, 255],
width: 1,
height: 1,
}
}
pub fn checkerboard(size: u32, tile_size: u32) -> Self {
let mut data = Vec::with_capacity((size * size * 4) as usize);
for y in 0..size {
for x in 0..size {
let is_white = ((x / tile_size) + (y / tile_size)) % 2 == 0;
let v = if is_white { 220u8 } else { 40u8 };
data.extend_from_slice(&[v, v, v, 255]);
}
}
Self { data, width: size, height: size }
}
}
#[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 remap(&self, u: f32, v: f32) -> (f32, f32) {
(
self.u_min + u * (self.u_max - self.u_min),
self.v_min + v * (self.v_max - self.v_min),
)
}
}
#[derive(Debug, Clone)]
pub struct TextureAtlas {
pub max_size: u32,
pub texture: Option<Texture>,
entries: Vec<(String, Texture)>,
regions: Vec<(String, AtlasRegion)>,
}
impl TextureAtlas {
pub fn new(max_size: u32) -> Self {
Self {
max_size,
texture: None,
entries: Vec::new(),
regions: Vec::new(),
}
}
pub fn add(&mut self, name: &str, texture: &Texture) {
self.entries.push((name.to_string(), texture.clone()));
self.texture = None;
self.regions.clear();
}
pub fn pack(&mut self) -> bool {
if self.entries.is_empty() {
let data = vec![0u8; (self.max_size * self.max_size * 4) as usize];
self.texture = Some(Texture::from_rgba(data, self.max_size, self.max_size));
return true;
}
let mut order: Vec<usize> = (0..self.entries.len()).collect();
order.sort_by(|&a, &b| self.entries[b].1.height.cmp(&self.entries[a].1.height));
let atlas_w = self.max_size;
let atlas_h = self.max_size;
let mut atlas_data = vec![0u8; (atlas_w * atlas_h * 4) as usize];
let mut shelf_x: u32 = 0;
let mut shelf_y: u32 = 0;
let mut shelf_height: u32 = 0;
let padding = 1u32;
let mut region_map: Vec<(usize, AtlasRegion)> = Vec::new();
for &idx in &order {
let tex = &self.entries[idx].1;
let tw = tex.width;
let th = tex.height;
if shelf_x + tw + padding > atlas_w {
shelf_y += shelf_height + padding;
shelf_x = 0;
shelf_height = 0;
}
if shelf_y + th > atlas_h {
return false; }
for row in 0..th {
let src_start = (row * tw * 4) as usize;
let src_end = src_start + (tw * 4) as usize;
let dst_y = shelf_y + row;
let dst_start = ((dst_y * atlas_w + shelf_x) * 4) as usize;
if src_end <= tex.data.len() && dst_start + (tw * 4) as usize <= atlas_data.len() {
atlas_data[dst_start..dst_start + (tw * 4) as usize]
.copy_from_slice(&tex.data[src_start..src_end]);
}
}
let region = AtlasRegion {
u_min: shelf_x as f32 / atlas_w as f32,
v_min: shelf_y as f32 / atlas_h as f32,
u_max: (shelf_x + tw) as f32 / atlas_w as f32,
v_max: (shelf_y + th) as f32 / atlas_h as f32,
};
region_map.push((idx, region));
shelf_x += tw + padding;
shelf_height = shelf_height.max(th);
}
self.regions.clear();
let mut ordered_regions = vec![None; self.entries.len()];
for (idx, region) in region_map {
ordered_regions[idx] = Some(region);
}
for (i, entry) in self.entries.iter().enumerate() {
if let Some(region) = ordered_regions[i] {
self.regions.push((entry.0.clone(), region));
}
}
self.texture = Some(Texture::from_rgba(atlas_data, atlas_w, atlas_h));
true
}
pub fn region(&self, name: &str) -> Option<AtlasRegion> {
self.regions.iter().find(|(n, _)| n == name).map(|(_, r)| *r)
}
pub fn region_by_index(&self, idx: usize) -> Option<AtlasRegion> {
self.regions.get(idx).map(|(_, r)| *r)
}
pub fn num_textures(&self) -> usize {
self.entries.len()
}
pub fn atlas_texture(&self) -> Option<&Texture> {
self.texture.as_ref()
}
pub fn remap_uv(&self, name: &str, u: f32, v: f32) -> Option<(f32, f32)> {
self.region(name).map(|r| r.remap(u, v))
}
pub fn texture_and_region(&self, name: &str) -> Option<(Texture, AtlasRegion)> {
let tex = self.texture.as_ref()?;
let region = self.region(name)?;
Some((tex.clone(), region))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solid_texture() {
let t = Texture::solid(255, 0, 0);
assert_eq!(t.width, 1);
assert_eq!(t.height, 1);
assert_eq!(t.data, vec![255, 0, 0, 255]);
}
#[test]
fn checkerboard_texture() {
let t = Texture::checkerboard(8, 4);
assert_eq!(t.width, 8);
assert_eq!(t.height, 8);
assert_eq!(t.data.len(), 8 * 8 * 4);
}
#[test]
fn from_rgba() {
let data = vec![0u8; 4 * 2 * 2];
let t = Texture::from_rgba(data, 2, 2);
assert_eq!(t.width, 2);
}
#[test]
fn atlas_pack_two_textures() {
let a = Texture::checkerboard(32, 8);
let b = Texture::checkerboard(16, 4);
let mut atlas = TextureAtlas::new(128);
atlas.add("big", &a);
atlas.add("small", &b);
assert!(atlas.pack(), "should fit in 128x128");
assert!(atlas.atlas_texture().is_some());
assert_eq!(atlas.num_textures(), 2);
let r1 = atlas.region("big").unwrap();
let r2 = atlas.region("small").unwrap();
assert!(r1.u_max <= r2.u_min || r2.u_max <= r1.u_min
|| r1.v_max <= r2.v_min || r2.v_max <= r1.v_min,
"regions should not overlap");
}
#[test]
fn atlas_remap_uv() {
let a = Texture::solid(255, 0, 0);
let mut atlas = TextureAtlas::new(64);
atlas.add("red", &a);
atlas.pack();
let (u, v) = atlas.remap_uv("red", 0.5, 0.5).unwrap();
let region = atlas.region("red").unwrap();
assert!(u >= region.u_min && u <= region.u_max);
assert!(v >= region.v_min && v <= region.v_max);
}
#[test]
fn atlas_region_remap() {
let region = AtlasRegion { u_min: 0.25, v_min: 0.0, u_max: 0.75, v_max: 0.5 };
let (u, v) = region.remap(0.0, 0.0);
assert!((u - 0.25).abs() < 1e-6);
assert!((v - 0.0).abs() < 1e-6);
let (u, v) = region.remap(1.0, 1.0);
assert!((u - 0.75).abs() < 1e-6);
assert!((v - 0.5).abs() < 1e-6);
}
#[test]
fn atlas_overflow() {
let big = Texture::checkerboard(128, 8);
let mut atlas = TextureAtlas::new(64);
atlas.add("too_big", &big);
assert!(!atlas.pack(), "128x128 texture should not fit in 64x64 atlas");
}
#[test]
fn atlas_texture_and_region() {
let a = Texture::checkerboard(16, 4);
let b = Texture::solid(0, 255, 0);
let mut atlas = TextureAtlas::new(64);
atlas.add("checker", &a);
atlas.add("green", &b);
atlas.pack();
let (tex, region) = atlas.texture_and_region("checker").unwrap();
assert_eq!(tex.width, 64);
assert!(region.u_max > region.u_min);
}
}