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 #[error("cannot add texture to uninitialized atlas texture")]
23 UninitializedAtlas,
24 #[error("cannot add uninitialized texture to atlas")]
26 UninitializedSourceTexture,
27}
28
29#[derive(Debug)]
30#[must_use]
31pub struct TextureAtlasBuilder<'a> {
34 textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
36 initial_size: UVec2,
38 max_size: UVec2,
40 format: TextureFormat,
42 auto_format_conversion: bool,
44 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 pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
66 self.initial_size = size;
67 self
68 }
69
70 pub fn max_size(&mut self, size: UVec2) -> &mut Self {
72 self.max_size = size;
73 self
74 }
75
76 pub fn format(&mut self, format: TextureFormat) -> &mut Self {
78 self.format = format;
79 self
80 }
81
82 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 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 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 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 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 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}