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