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(#[automatically_derived]
impl ::core::fmt::Debug for TextureAtlasBuilderError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            TextureAtlasBuilderError::NotEnoughSpace =>
                ::core::fmt::Formatter::write_str(f, "NotEnoughSpace"),
            TextureAtlasBuilderError::WrongFormat =>
                ::core::fmt::Formatter::write_str(f, "WrongFormat"),
            TextureAtlasBuilderError::UninitializedAtlas =>
                ::core::fmt::Formatter::write_str(f, "UninitializedAtlas"),
            TextureAtlasBuilderError::UninitializedSourceTexture =>
                ::core::fmt::Formatter::write_str(f,
                    "UninitializedSourceTexture"),
            TextureAtlasBuilderError::TextureAccess(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "TextureAccess", &__self_0),
        }
    }
}Debug, fn from(source: TextureAccessError) -> Self {
    TextureAtlasBuilderError::TextureAccess { 0: source }
}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(#[automatically_derived]
impl<'a> ::core::fmt::Debug for TextureAtlasBuilder<'a> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["textures_to_place", "initial_size", "max_size", "format",
                        "auto_format_conversion", "padding"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.textures_to_place, &self.initial_size, &self.max_size,
                        &self.format, &self.auto_format_conversion, &&self.padding];
        ::core::fmt::Formatter::debug_struct_fields_finish(f,
            "TextureAtlasBuilder", names, values)
    }
}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            {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event src/texture_atlas_builder.rs:154",
                        "bevy_image::texture_atlas_builder",
                        ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("src/texture_atlas_builder.rs"),
                        ::tracing_core::__macro_support::Option::Some(154u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_image::texture_atlas_builder"),
                        ::tracing_core::field::FieldSet::new(&["message"],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::DEBUG <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::DEBUG <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("Converting texture from \'{0:?}\' to \'{1:?}\'",
                                                    texture.texture_descriptor.format, self.format) as
                                            &dyn ::tracing::field::Value))])
            });
    } else { ; }
};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            {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event src/texture_atlas_builder.rs:165",
                        "bevy_image::texture_atlas_builder",
                        ::tracing::Level::ERROR,
                        ::tracing_core::__macro_support::Option::Some("src/texture_atlas_builder.rs"),
                        ::tracing_core::__macro_support::Option::Some(165u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_image::texture_atlas_builder"),
                        ::tracing_core::field::FieldSet::new(&["message"],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::ERROR <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::ERROR <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("Error converting texture from \'{0:?}\' to \'{1:?}\', ignoring",
                                                    texture.texture_descriptor.format, self.format) as
                                            &dyn ::tracing::field::Value))])
            });
    } else { ; }
};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                        ::alloc::vec::from_elem(0,
    self.format.pixel_size()? * (current_width * current_height) as usize)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                {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event src/texture_atlas_builder.rs:288",
                        "bevy_image::texture_atlas_builder", ::tracing::Level::WARN,
                        ::tracing_core::__macro_support::Option::Some("src/texture_atlas_builder.rs"),
                        ::tracing_core::__macro_support::Option::Some(288u32),
                        ::tracing_core::__macro_support::Option::Some("bevy_image::texture_atlas_builder"),
                        ::tracing_core::field::FieldSet::new(&["message"],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::WARN <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::WARN <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("Loading a texture of format \'{0:?}\' in an atlas with format \'{1:?}\'",
                                                    texture.texture_descriptor.format, self.format) as
                                            &dyn ::tracing::field::Value))])
            });
    } else { ; }
};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}