bevy_image/
dynamic_texture_atlas_builder.rs

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