bevy_image/
dynamic_texture_atlas_builder.rs

1use crate::{Image, TextureAccessError, TextureAtlasLayout, TextureFormatPixelInfo as _};
2use bevy_asset::RenderAssetUsages;
3use bevy_math::{URect, UVec2};
4use guillotiere::{size2, Allocation, AtlasAllocator};
5use thiserror::Error;
6
7/// An error produced by [`DynamicTextureAtlasBuilder`] when trying to add a new
8/// texture to a [`TextureAtlasLayout`].
9#[derive(Debug, Error)]
10pub enum DynamicTextureAtlasBuilderError {
11    /// Unable to allocate space within the atlas for the new texture
12    #[error("Couldn't allocate space to add the image requested")]
13    FailedToAllocateSpace,
14    /// Attempted to add a texture to an uninitialized atlas
15    #[error("cannot add texture to uninitialized atlas texture")]
16    UninitializedAtlas,
17    /// Attempted to add an uninitialized texture to an atlas
18    #[error("cannot add uninitialized texture to atlas")]
19    UninitializedSourceTexture,
20    /// A texture access error occurred
21    #[error("texture access error: {0}")]
22    TextureAccess(#[from] TextureAccessError),
23}
24
25/// Helper utility to update [`TextureAtlasLayout`] on the fly.
26///
27/// Helpful in cases when texture is created procedurally,
28/// e.g: in a font glyph [`TextureAtlasLayout`], only add the [`Image`] texture for letters to be rendered.
29pub struct DynamicTextureAtlasBuilder {
30    atlas_allocator: AtlasAllocator,
31    padding: u32,
32}
33
34impl DynamicTextureAtlasBuilder {
35    /// Create a new [`DynamicTextureAtlasBuilder`]
36    ///
37    /// # Arguments
38    ///
39    /// * `size` - total size for the atlas
40    /// * `padding` - gap added between textures in the atlas, both in x axis and y axis
41    pub fn new(size: UVec2, padding: u32) -> Self {
42        Self {
43            atlas_allocator: AtlasAllocator::new(to_size2(size)),
44            padding,
45        }
46    }
47
48    /// Add a new texture to `atlas_layout`.
49    ///
50    /// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`].
51    /// Also, the asset that `atlas_texture_handle` points to must have a usage matching
52    /// [`RenderAssetUsages::MAIN_WORLD`].
53    ///
54    /// # Arguments
55    ///
56    /// * `atlas_layout` - The atlas layout to add the texture to.
57    /// * `texture` - The source texture to add to the atlas.
58    /// * `atlas_texture` - The destination atlas texture to copy the source texture to.
59    pub fn add_texture(
60        &mut self,
61        atlas_layout: &mut TextureAtlasLayout,
62        texture: &Image,
63        atlas_texture: &mut Image,
64    ) -> Result<usize, DynamicTextureAtlasBuilderError> {
65        let allocation = self.atlas_allocator.allocate(size2(
66            (texture.width() + self.padding).try_into().unwrap(),
67            (texture.height() + self.padding).try_into().unwrap(),
68        ));
69        if let Some(allocation) = allocation {
70            assert!(
71                atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),
72                "The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"
73            );
74
75            self.place_texture(atlas_texture, allocation, texture)?;
76            let mut rect: URect = to_rect(allocation.rectangle);
77            rect.max = rect.max.saturating_sub(UVec2::splat(self.padding));
78            Ok(atlas_layout.add_texture(rect))
79        } else {
80            Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)
81        }
82    }
83
84    fn place_texture(
85        &mut self,
86        atlas_texture: &mut Image,
87        allocation: Allocation,
88        texture: &Image,
89    ) -> Result<(), DynamicTextureAtlasBuilderError> {
90        let mut rect = allocation.rectangle;
91        rect.max.x -= self.padding as i32;
92        rect.max.y -= self.padding as i32;
93        let atlas_width = atlas_texture.width() as usize;
94        let rect_width = rect.width() as usize;
95        let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
96
97        let Some(ref mut atlas_data) = atlas_texture.data else {
98            return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);
99        };
100        let Some(ref data) = texture.data else {
101            return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);
102        };
103        for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
104            let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
105            let end = begin + rect_width * format_size;
106            let texture_begin = texture_y * rect_width * format_size;
107            let texture_end = texture_begin + rect_width * format_size;
108            atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
109        }
110        Ok(())
111    }
112}
113
114fn to_rect(rectangle: guillotiere::Rectangle) -> URect {
115    URect {
116        min: UVec2::new(
117            rectangle.min.x.try_into().unwrap(),
118            rectangle.min.y.try_into().unwrap(),
119        ),
120        max: UVec2::new(
121            rectangle.max.x.try_into().unwrap(),
122            rectangle.max.y.try_into().unwrap(),
123        ),
124    }
125}
126
127fn to_size2(vec2: UVec2) -> guillotiere::Size {
128    guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
129}