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)]
17pub enum TextureAtlasBuilderError {
18 #[error("could not pack textures into an atlas within the given bounds")]
20 NotEnoughSpace,
21 #[error("added a texture with the wrong format in an atlas")]
23 WrongFormat,
24 #[error("cannot add texture to uninitialized atlas texture")]
26 UninitializedAtlas,
27 #[error("cannot add uninitialized texture to atlas")]
29 UninitializedSourceTexture,
30 #[error("texture access error: {0}")]
32 TextureAccess(#[from] TextureAccessError),
33}
34
35#[derive(Debug)]
36#[must_use]
37pub struct TextureAtlasBuilder<'a> {
40 textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
42 initial_size: UVec2,
44 max_size: UVec2,
46 format: TextureFormat,
48 auto_format_conversion: bool,
50 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
67pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;
69
70impl<'a> TextureAtlasBuilder<'a> {
71 pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
73 self.initial_size = size;
74 self
75 }
76
77 pub fn max_size(&mut self, size: UVec2) -> &mut Self {
79 self.max_size = size;
80 self
81 }
82
83 pub fn format(&mut self, format: TextureFormat) -> &mut Self {
85 self.format = format;
86 self
87 }
88
89 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 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 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 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 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 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}