Skip to main content

bevy_image/
texture_atlas_builder.rs

1use bevy_asset::{AssetId, RenderAssetUsages};
2use bevy_math::{URect, UVec2};
3use bevy_platform::collections::HashMap;
4use rectangle_pack::{
5    contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,
6    RectToInsert, TargetBin,
7};
8use thiserror::Error;
9use tracing::{debug, error, warn};
10use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
11
12use crate::{Image, TextureAccessError, TextureFormatPixelInfo};
13use crate::{TextureAtlasLayout, TextureAtlasSources};
14
15/// Errors returned by [`TextureAtlasBuilder`].
16#[derive(Debug, Error)]
17pub enum TextureAtlasBuilderError {
18    /// The atlas texture wasn't large enough to fit the texture
19    #[error("could not pack textures into an atlas within the given bounds")]
20    NotEnoughSpace,
21    /// Attempted to add a texture with a different format
22    #[error("added a texture with the wrong format in an atlas")]
23    WrongFormat,
24    /// Attempted to add a texture to an uninitialized atlas
25    #[error("cannot add texture to uninitialized atlas texture")]
26    UninitializedAtlas,
27    /// Attempted to add an uninitialized texture to an atlas
28    #[error("cannot add uninitialized texture to atlas")]
29    UninitializedSourceTexture,
30    /// A texture access error occurred
31    #[error("texture access error: {0}")]
32    TextureAccess(#[from] TextureAccessError),
33}
34
35#[derive(Debug)]
36#[must_use]
37/// A builder which is used to create a texture atlas from many individual
38/// sprites.
39pub struct TextureAtlasBuilder<'a> {
40    /// Collection of texture's asset id (optional) and image data to be packed into an atlas
41    textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
42    /// The initial atlas size in pixels.
43    initial_size: UVec2,
44    /// The absolute maximum size of the texture atlas in pixels.
45    max_size: UVec2,
46    /// The texture format for the textures that will be loaded in the atlas.
47    format: TextureFormat,
48    /// Enable automatic format conversion for textures if they are not in the atlas format.
49    auto_format_conversion: bool,
50    /// The amount of padding in pixels to add along the right and bottom edges of the texture rects.
51    padding: UVec2,
52}
53
54impl Default for TextureAtlasBuilder<'_> {
55    fn default() -> Self {
56        Self {
57            textures_to_place: Vec::new(),
58            initial_size: UVec2::splat(256),
59            max_size: UVec2::splat(2048),
60            format: TextureFormat::Rgba8UnormSrgb,
61            auto_format_conversion: true,
62            padding: UVec2::ZERO,
63        }
64    }
65}
66
67/// The [`Result`] type used by [`TextureAtlasBuilder`].
68pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;
69
70impl<'a> TextureAtlasBuilder<'a> {
71    /// Sets the initial size of the atlas in pixels.
72    pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
73        self.initial_size = size;
74        self
75    }
76
77    /// Sets the max size of the atlas in pixels.
78    pub fn max_size(&mut self, size: UVec2) -> &mut Self {
79        self.max_size = size;
80        self
81    }
82
83    /// Sets the texture format for textures in the atlas.
84    pub fn format(&mut self, format: TextureFormat) -> &mut Self {
85        self.format = format;
86        self
87    }
88
89    /// Control whether the added texture should be converted to the atlas format, if different.
90    pub fn auto_format_conversion(&mut self, auto_format_conversion: bool) -> &mut Self {
91        self.auto_format_conversion = auto_format_conversion;
92        self
93    }
94
95    /// Adds a texture to be copied to the texture atlas.
96    ///
97    /// Optionally an asset id can be passed that can later be used with the texture layout to retrieve the index of this texture.
98    /// The insertion order will reflect the index of the added texture in the finished texture atlas.
99    pub fn add_texture(
100        &mut self,
101        image_id: Option<AssetId<Image>>,
102        texture: &'a Image,
103    ) -> &mut Self {
104        self.textures_to_place.push((image_id, texture));
105        self
106    }
107
108    /// Sets the amount of padding in pixels to add between the textures in the texture atlas.
109    ///
110    /// The `x` value provide will be added to the right edge, while the `y` value will be added to the bottom edge.
111    pub fn padding(&mut self, padding: UVec2) -> &mut Self {
112        self.padding = padding;
113        self
114    }
115
116    fn copy_texture_to_atlas(
117        atlas_texture: &mut Image,
118        texture: &Image,
119        packed_location: &PackedLocation,
120        padding: UVec2,
121    ) -> TextureAtlasBuilderResult<()> {
122        let rect_width = (packed_location.width() - padding.x) as usize;
123        let rect_height = (packed_location.height() - padding.y) as usize;
124        let rect_x = packed_location.x() as usize;
125        let rect_y = packed_location.y() as usize;
126        let atlas_width = atlas_texture.width() as usize;
127        let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
128
129        let Some(ref mut atlas_data) = atlas_texture.data else {
130            return Err(TextureAtlasBuilderError::UninitializedAtlas);
131        };
132        let Some(ref data) = texture.data else {
133            return Err(TextureAtlasBuilderError::UninitializedSourceTexture);
134        };
135        for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() {
136            let begin = (bound_y * atlas_width + rect_x) * format_size;
137            let end = begin + rect_width * format_size;
138            let texture_begin = texture_y * rect_width * format_size;
139            let texture_end = texture_begin + rect_width * format_size;
140            atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
141        }
142        Ok(())
143    }
144
145    fn copy_converted_texture(
146        &self,
147        atlas_texture: &mut Image,
148        texture: &Image,
149        packed_location: &PackedLocation,
150    ) -> TextureAtlasBuilderResult<()> {
151        if self.format == texture.texture_descriptor.format {
152            Self::copy_texture_to_atlas(atlas_texture, texture, packed_location, self.padding)?;
153        } else if let Some(converted_texture) = texture.convert(self.format) {
154            debug!(
155                "Converting texture from '{:?}' to '{:?}'",
156                texture.texture_descriptor.format, self.format
157            );
158            Self::copy_texture_to_atlas(
159                atlas_texture,
160                &converted_texture,
161                packed_location,
162                self.padding,
163            )?;
164        } else {
165            error!(
166                "Error converting texture from '{:?}' to '{:?}', ignoring",
167                texture.texture_descriptor.format, self.format
168            );
169        }
170        Ok(())
171    }
172
173    /// Consumes the builder, and returns the newly created texture atlas and
174    /// the associated atlas layout.
175    ///
176    /// Assigns indices to the textures based on the insertion order.
177    /// Internally it copies all rectangles from the textures and copies them
178    /// into a new texture.
179    ///
180    /// # Usage
181    ///
182    /// ```rust
183    /// # use bevy_ecs::prelude::*;
184    /// # use bevy_asset::*;
185    /// # use bevy_image::prelude::*;
186    ///
187    /// fn my_system(mut textures: ResMut<Assets<Image>>, mut layouts: ResMut<Assets<TextureAtlasLayout>>) {
188    ///     // Declare your builder
189    ///     let mut builder = TextureAtlasBuilder::default();
190    ///     // Customize it
191    ///     // ...
192    ///     // Build your texture and the atlas layout
193    ///     let (atlas_layout, atlas_sources, texture) = builder.build().unwrap();
194    ///     let texture = textures.add(texture);
195    ///     let layout = layouts.add(atlas_layout);
196    /// }
197    /// ```
198    ///
199    /// # Errors
200    ///
201    /// If there is not enough space in the atlas texture, an error will
202    /// be returned. It is then recommended to make a larger sprite sheet.
203    pub fn build(
204        &mut self,
205    ) -> TextureAtlasBuilderResult<(TextureAtlasLayout, TextureAtlasSources, Image)> {
206        let max_width = self.max_size.x;
207        let max_height = self.max_size.y;
208
209        let mut current_width = self.initial_size.x;
210        let mut current_height = self.initial_size.y;
211        let mut rect_placements = None;
212        let mut atlas_texture = Image::default();
213        let mut rects_to_place = GroupedRectsToPlace::<usize>::new();
214
215        // Adds textures to rectangle group packer
216        for (index, (_, texture)) in self.textures_to_place.iter().enumerate() {
217            rects_to_place.push_rect(
218                index,
219                None,
220                RectToInsert::new(
221                    texture.width() + self.padding.x,
222                    texture.height() + self.padding.y,
223                    1,
224                ),
225            );
226        }
227
228        while rect_placements.is_none() {
229            if current_width > max_width || current_height > max_height {
230                break;
231            }
232
233            let last_attempt = current_height == max_height && current_width == max_width;
234
235            let mut target_bins = alloc::collections::BTreeMap::new();
236            target_bins.insert(0, TargetBin::new(current_width, current_height, 1));
237            rect_placements = match pack_rects(
238                &rects_to_place,
239                &mut target_bins,
240                &volume_heuristic,
241                &contains_smallest_box,
242            ) {
243                Ok(rect_placements) => {
244                    atlas_texture = Image::new(
245                        Extent3d {
246                            width: current_width,
247                            height: current_height,
248                            depth_or_array_layers: 1,
249                        },
250                        TextureDimension::D2,
251                        vec![
252                            0;
253                            self.format.pixel_size()? * (current_width * current_height) as usize
254                        ],
255                        self.format,
256                        RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
257                    );
258                    Some(rect_placements)
259                }
260                Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => {
261                    current_height = (current_height * 2).clamp(0, max_height);
262                    current_width = (current_width * 2).clamp(0, max_width);
263                    None
264                }
265            };
266
267            if last_attempt {
268                break;
269            }
270        }
271
272        let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?;
273
274        let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len());
275        let mut texture_ids = <HashMap<_, _>>::default();
276        // We iterate through the textures to place to respect the insertion order for the texture indices
277        for (index, (image_id, texture)) in self.textures_to_place.iter().enumerate() {
278            let (_, packed_location) = rect_placements.packed_locations().get(&index).unwrap();
279
280            let min = UVec2::new(packed_location.x(), packed_location.y());
281            let max =
282                min + UVec2::new(packed_location.width(), packed_location.height()) - self.padding;
283            if let Some(image_id) = image_id {
284                texture_ids.insert(*image_id, index);
285            }
286            texture_rects.push(URect { min, max });
287            if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {
288                warn!(
289                    "Loading a texture of format '{:?}' in an atlas with format '{:?}'",
290                    texture.texture_descriptor.format, self.format
291                );
292                return Err(TextureAtlasBuilderError::WrongFormat);
293            }
294            self.copy_converted_texture(&mut atlas_texture, texture, packed_location)?;
295        }
296
297        Ok((
298            TextureAtlasLayout {
299                size: atlas_texture.size(),
300                textures: texture_rects,
301            },
302            TextureAtlasSources { texture_ids },
303            atlas_texture,
304        ))
305    }
306}