Skip to main content

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 (and the atlas edge), both in x axis
41    ///   and y axis
42    pub fn new(size: UVec2, padding: u32) -> Self {
43        // This doesn't need to be >= since `AtlasAllocator` requires non-zero size.
44        debug_assert!(size.x > padding && size.y > padding);
45        Self {
46            // Leave out padding at the right and bottom, so we don't put textures on the edge of
47            // atlas.
48            atlas_allocator: AtlasAllocator::new(to_size2(size - padding)),
49            padding,
50        }
51    }
52
53    /// Add a new texture to `atlas_layout`.
54    ///
55    /// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`].
56    /// Also, the asset that `atlas_texture_handle` points to must have a usage matching
57    /// [`RenderAssetUsages::MAIN_WORLD`].
58    ///
59    /// # Arguments
60    ///
61    /// * `atlas_layout` - The atlas layout to add the texture to.
62    /// * `texture` - The source texture to add to the atlas.
63    /// * `atlas_texture` - The destination atlas texture to copy the source texture to.
64    pub fn add_texture(
65        &mut self,
66        atlas_layout: &mut TextureAtlasLayout,
67        texture: &Image,
68        atlas_texture: &mut Image,
69    ) -> Result<usize, DynamicTextureAtlasBuilderError> {
70        // Allocate enough space for the texture and the padding to the top and left (bottom and
71        // right padding are taken care off since the allocator size omits it on creation).
72        let allocation = self.atlas_allocator.allocate(size2(
73            (texture.width() + self.padding).try_into().unwrap(),
74            (texture.height() + self.padding).try_into().unwrap(),
75        ));
76        if let Some(mut allocation) = allocation {
77            assert!(
78                atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),
79                "The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"
80            );
81            let rect = &mut allocation.rectangle;
82            // Remove the padding from the top and left (bottom and right padding is taken care of
83            // by the "next" allocation and the border restriction).
84            rect.min.x += self.padding as i32;
85            rect.min.y += self.padding as i32;
86
87            self.place_texture(atlas_texture, allocation, texture)?;
88            Ok(atlas_layout.add_texture(to_rect(allocation.rectangle)))
89        } else {
90            Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)
91        }
92    }
93
94    fn place_texture(
95        &mut self,
96        atlas_texture: &mut Image,
97        allocation: Allocation,
98        texture: &Image,
99    ) -> Result<(), DynamicTextureAtlasBuilderError> {
100        let rect = &allocation.rectangle;
101        let atlas_width = atlas_texture.width() as usize;
102        let rect_width = rect.width() as usize;
103        let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
104
105        let Some(ref mut atlas_data) = atlas_texture.data else {
106            return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);
107        };
108        let Some(ref data) = texture.data else {
109            return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);
110        };
111        for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
112            let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
113            let end = begin + rect_width * format_size;
114            let texture_begin = texture_y * rect_width * format_size;
115            let texture_end = texture_begin + rect_width * format_size;
116            atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
117        }
118        Ok(())
119    }
120}
121
122fn to_rect(rectangle: guillotiere::Rectangle) -> URect {
123    URect {
124        min: UVec2::new(
125            rectangle.min.x.try_into().unwrap(),
126            rectangle.min.y.try_into().unwrap(),
127        ),
128        max: UVec2::new(
129            rectangle.max.x.try_into().unwrap(),
130            rectangle.max.y.try_into().unwrap(),
131        ),
132    }
133}
134
135fn to_size2(vec2: UVec2) -> guillotiere::Size {
136    guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
137}
138
139#[cfg(test)]
140mod tests {
141    use bevy_asset::RenderAssetUsages;
142    use bevy_math::{URect, UVec2};
143
144    use crate::{DynamicTextureAtlasBuilder, Image, TextureAtlasLayout};
145
146    fn make_filled_image(size: UVec2, pixel_rgba_bytes: [u8; 4]) -> Image {
147        Image::new_fill(
148            wgpu_types::Extent3d {
149                width: size.x,
150                height: size.y,
151                depth_or_array_layers: 1,
152            },
153            wgpu_types::TextureDimension::D2,
154            &pixel_rgba_bytes,
155            wgpu_types::TextureFormat::Rgba8Unorm,
156            RenderAssetUsages::all(),
157        )
158    }
159
160    fn rect_contains_value(image: &Image, rect: URect, pixel_rgba_bytes: [u8; 4]) -> bool {
161        let image_data = image.data.as_ref().unwrap();
162        for y in rect.min.y..rect.max.y {
163            for x in rect.min.x..rect.max.x {
164                let byte_start = ((x + y * image.width()) * 4) as usize;
165                if image_data[byte_start..(byte_start + 4)] != pixel_rgba_bytes {
166                    return false;
167                }
168            }
169        }
170        true
171    }
172
173    #[test]
174    fn allocate_textures() {
175        let size = UVec2::new(30, 30);
176
177        let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
178        let mut layout = TextureAtlasLayout::new_empty(size);
179        let mut builder = DynamicTextureAtlasBuilder::new(size, 0);
180
181        let square = UVec2::new(10, 10);
182        let colors = [
183            [255, 0, 0, 255],
184            [0, 255, 0, 255],
185            [0, 0, 255, 255],
186            [255, 0, 255, 255],
187            [0, 255, 255, 255],
188            [0, 255, 255, 255],
189        ];
190        let texture_0 = builder
191            .add_texture(
192                &mut layout,
193                &make_filled_image(square, colors[0]),
194                &mut atlas_texture,
195            )
196            .unwrap();
197        let texture_1 = builder
198            .add_texture(
199                &mut layout,
200                &make_filled_image(square, colors[1]),
201                &mut atlas_texture,
202            )
203            .unwrap();
204        let texture_2 = builder
205            .add_texture(
206                &mut layout,
207                &make_filled_image(square, colors[2]),
208                &mut atlas_texture,
209            )
210            .unwrap();
211        let texture_3 = builder
212            .add_texture(
213                &mut layout,
214                &make_filled_image(square, colors[3]),
215                &mut atlas_texture,
216            )
217            .unwrap();
218        let texture_4 = builder
219            .add_texture(
220                &mut layout,
221                &make_filled_image(square, colors[4]),
222                &mut atlas_texture,
223            )
224            .unwrap();
225        let texture_5 = builder
226            .add_texture(
227                &mut layout,
228                &make_filled_image(square, colors[5]),
229                &mut atlas_texture,
230            )
231            .unwrap();
232
233        let expected_rects = [
234            URect::from_corners(UVec2::new(0, 0), UVec2::new(10, 10)),
235            URect::from_corners(UVec2::new(10, 0), UVec2::new(20, 10)),
236            URect::from_corners(UVec2::new(20, 0), UVec2::new(30, 10)),
237            URect::from_corners(UVec2::new(0, 10), UVec2::new(10, 20)),
238            URect::from_corners(UVec2::new(0, 20), UVec2::new(10, 30)),
239            URect::from_corners(UVec2::new(10, 10), UVec2::new(20, 20)),
240        ];
241        assert_eq!(layout.textures[texture_0], expected_rects[0]);
242        assert_eq!(layout.textures[texture_1], expected_rects[1]);
243        assert_eq!(layout.textures[texture_2], expected_rects[2]);
244        assert_eq!(layout.textures[texture_3], expected_rects[3]);
245        assert_eq!(layout.textures[texture_4], expected_rects[4]);
246        assert_eq!(layout.textures[texture_5], expected_rects[5]);
247
248        assert!(rect_contains_value(
249            &atlas_texture,
250            expected_rects[0],
251            colors[0]
252        ));
253        assert!(rect_contains_value(
254            &atlas_texture,
255            expected_rects[1],
256            colors[1]
257        ));
258        assert!(rect_contains_value(
259            &atlas_texture,
260            expected_rects[2],
261            colors[2]
262        ));
263        assert!(rect_contains_value(
264            &atlas_texture,
265            expected_rects[3],
266            colors[3]
267        ));
268        assert!(rect_contains_value(
269            &atlas_texture,
270            expected_rects[4],
271            colors[4]
272        ));
273        assert!(rect_contains_value(
274            &atlas_texture,
275            expected_rects[5],
276            colors[5]
277        ));
278    }
279
280    #[test]
281    fn allocate_textures_with_padding() {
282        let size = UVec2::new(12, 12);
283
284        let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
285        let mut layout = TextureAtlasLayout::new_empty(size);
286        let mut builder = DynamicTextureAtlasBuilder::new(size, 1);
287
288        let square = UVec2::new(3, 3);
289        let colors = [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]];
290        let texture_0 = builder
291            .add_texture(
292                &mut layout,
293                &make_filled_image(square, colors[0]),
294                &mut atlas_texture,
295            )
296            .unwrap();
297        let texture_1 = builder
298            .add_texture(
299                &mut layout,
300                &make_filled_image(square, colors[1]),
301                &mut atlas_texture,
302            )
303            .unwrap();
304        let texture_2 = builder
305            .add_texture(
306                &mut layout,
307                &make_filled_image(square, colors[2]),
308                &mut atlas_texture,
309            )
310            .unwrap();
311
312        let expected_rects = [
313            URect::from_corners(UVec2::new(1, 1), UVec2::new(4, 4)),
314            URect::from_corners(UVec2::new(5, 1), UVec2::new(8, 4)),
315            // If we didn't pad the right of the texture atlas, there would be just enough space to
316            // fit this in the first row, but since we do pad the right, this gets pushed to the
317            // next row.
318            URect::from_corners(UVec2::new(1, 5), UVec2::new(4, 8)),
319        ];
320        assert_eq!(layout.textures[texture_0], expected_rects[0]);
321        assert_eq!(layout.textures[texture_1], expected_rects[1]);
322        assert_eq!(layout.textures[texture_2], expected_rects[2]);
323
324        assert!(rect_contains_value(
325            &atlas_texture,
326            expected_rects[0],
327            colors[0]
328        ));
329        assert!(rect_contains_value(
330            &atlas_texture,
331            expected_rects[1],
332            colors[1]
333        ));
334        assert!(rect_contains_value(
335            &atlas_texture,
336            expected_rects[2],
337            colors[2]
338        ));
339    }
340}