use crate::{Image, TextureAccessError, TextureAtlasLayout, TextureFormatPixelInfo as _};
use bevy_asset::RenderAssetUsages;
use bevy_math::{URect, UVec2};
use guillotiere::{size2, Allocation, AtlasAllocator};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DynamicTextureAtlasBuilderError {
#[error("Couldn't allocate space to add the image requested")]
FailedToAllocateSpace,
#[error("cannot add texture to uninitialized atlas texture")]
UninitializedAtlas,
#[error("cannot add uninitialized texture to atlas")]
UninitializedSourceTexture,
#[error("texture access error: {0}")]
TextureAccess(#[from] TextureAccessError),
}
pub struct DynamicTextureAtlasBuilder {
atlas_allocator: AtlasAllocator,
padding: u32,
}
impl DynamicTextureAtlasBuilder {
pub fn new(size: UVec2, padding: u32) -> Self {
debug_assert!(size.x > padding && size.y > padding);
Self {
atlas_allocator: AtlasAllocator::new(to_size2(size - padding)),
padding,
}
}
pub fn add_texture(
&mut self,
atlas_layout: &mut TextureAtlasLayout,
texture: &Image,
atlas_texture: &mut Image,
) -> Result<usize, DynamicTextureAtlasBuilderError> {
let allocation = self.atlas_allocator.allocate(size2(
(texture.width() + self.padding).try_into().unwrap(),
(texture.height() + self.padding).try_into().unwrap(),
));
if let Some(mut allocation) = allocation {
assert!(
atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),
"The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"
);
let rect = &mut allocation.rectangle;
rect.min.x += self.padding as i32;
rect.min.y += self.padding as i32;
self.place_texture(atlas_texture, allocation, texture)?;
Ok(atlas_layout.add_texture(to_rect(allocation.rectangle)))
} else {
Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)
}
}
fn place_texture(
&mut self,
atlas_texture: &mut Image,
allocation: Allocation,
texture: &Image,
) -> Result<(), DynamicTextureAtlasBuilderError> {
let rect = &allocation.rectangle;
let atlas_width = atlas_texture.width() as usize;
let rect_width = rect.width() as usize;
let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
let Some(ref mut atlas_data) = atlas_texture.data else {
return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);
};
let Some(ref data) = texture.data else {
return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);
};
for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
let end = begin + rect_width * format_size;
let texture_begin = texture_y * rect_width * format_size;
let texture_end = texture_begin + rect_width * format_size;
atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
}
Ok(())
}
}
fn to_rect(rectangle: guillotiere::Rectangle) -> URect {
URect {
min: UVec2::new(
rectangle.min.x.try_into().unwrap(),
rectangle.min.y.try_into().unwrap(),
),
max: UVec2::new(
rectangle.max.x.try_into().unwrap(),
rectangle.max.y.try_into().unwrap(),
),
}
}
fn to_size2(vec2: UVec2) -> guillotiere::Size {
guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
}
#[cfg(test)]
mod tests {
use bevy_asset::RenderAssetUsages;
use bevy_math::{URect, UVec2};
use crate::{DynamicTextureAtlasBuilder, Image, TextureAtlasLayout};
fn make_filled_image(size: UVec2, pixel_rgba_bytes: [u8; 4]) -> Image {
Image::new_fill(
wgpu_types::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
wgpu_types::TextureDimension::D2,
&pixel_rgba_bytes,
wgpu_types::TextureFormat::Rgba8Unorm,
RenderAssetUsages::all(),
)
}
fn rect_contains_value(image: &Image, rect: URect, pixel_rgba_bytes: [u8; 4]) -> bool {
let image_data = image.data.as_ref().unwrap();
for y in rect.min.y..rect.max.y {
for x in rect.min.x..rect.max.x {
let byte_start = ((x + y * image.width()) * 4) as usize;
if image_data[byte_start..(byte_start + 4)] != pixel_rgba_bytes {
return false;
}
}
}
true
}
#[test]
fn allocate_textures() {
let size = UVec2::new(30, 30);
let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
let mut layout = TextureAtlasLayout::new_empty(size);
let mut builder = DynamicTextureAtlasBuilder::new(size, 0);
let square = UVec2::new(10, 10);
let colors = [
[255, 0, 0, 255],
[0, 255, 0, 255],
[0, 0, 255, 255],
[255, 0, 255, 255],
[0, 255, 255, 255],
[0, 255, 255, 255],
];
let texture_0 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[0]),
&mut atlas_texture,
)
.unwrap();
let texture_1 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[1]),
&mut atlas_texture,
)
.unwrap();
let texture_2 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[2]),
&mut atlas_texture,
)
.unwrap();
let texture_3 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[3]),
&mut atlas_texture,
)
.unwrap();
let texture_4 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[4]),
&mut atlas_texture,
)
.unwrap();
let texture_5 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[5]),
&mut atlas_texture,
)
.unwrap();
let expected_rects = [
URect::from_corners(UVec2::new(0, 0), UVec2::new(10, 10)),
URect::from_corners(UVec2::new(10, 0), UVec2::new(20, 10)),
URect::from_corners(UVec2::new(20, 0), UVec2::new(30, 10)),
URect::from_corners(UVec2::new(0, 10), UVec2::new(10, 20)),
URect::from_corners(UVec2::new(0, 20), UVec2::new(10, 30)),
URect::from_corners(UVec2::new(10, 10), UVec2::new(20, 20)),
];
assert_eq!(layout.textures[texture_0], expected_rects[0]);
assert_eq!(layout.textures[texture_1], expected_rects[1]);
assert_eq!(layout.textures[texture_2], expected_rects[2]);
assert_eq!(layout.textures[texture_3], expected_rects[3]);
assert_eq!(layout.textures[texture_4], expected_rects[4]);
assert_eq!(layout.textures[texture_5], expected_rects[5]);
assert!(rect_contains_value(
&atlas_texture,
expected_rects[0],
colors[0]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[1],
colors[1]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[2],
colors[2]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[3],
colors[3]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[4],
colors[4]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[5],
colors[5]
));
}
#[test]
fn allocate_textures_with_padding() {
let size = UVec2::new(12, 12);
let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
let mut layout = TextureAtlasLayout::new_empty(size);
let mut builder = DynamicTextureAtlasBuilder::new(size, 1);
let square = UVec2::new(3, 3);
let colors = [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]];
let texture_0 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[0]),
&mut atlas_texture,
)
.unwrap();
let texture_1 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[1]),
&mut atlas_texture,
)
.unwrap();
let texture_2 = builder
.add_texture(
&mut layout,
&make_filled_image(square, colors[2]),
&mut atlas_texture,
)
.unwrap();
let expected_rects = [
URect::from_corners(UVec2::new(1, 1), UVec2::new(4, 4)),
URect::from_corners(UVec2::new(5, 1), UVec2::new(8, 4)),
URect::from_corners(UVec2::new(1, 5), UVec2::new(4, 8)),
];
assert_eq!(layout.textures[texture_0], expected_rects[0]);
assert_eq!(layout.textures[texture_1], expected_rects[1]);
assert_eq!(layout.textures[texture_2], expected_rects[2]);
assert!(rect_contains_value(
&atlas_texture,
expected_rects[0],
colors[0]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[1],
colors[1]
));
assert!(rect_contains_value(
&atlas_texture,
expected_rects[2],
colors[2]
));
}
}