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