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 #[error("cannot add texture to uninitialized atlas texture")]
23 UninitializedAtlas,
24 #[error("cannot add uninitialized texture to atlas")]
26 UninitializedSourceTexture,
27 #[error("texture access error: {0}")]
29 TextureAccess(#[from] TextureAccessError),
30}
31
32#[derive(Debug)]
33#[must_use]
34pub struct TextureAtlasBuilder<'a> {
37 textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
39 initial_size: UVec2,
41 max_size: UVec2,
43 format: TextureFormat,
45 auto_format_conversion: bool,
47 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 pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
69 self.initial_size = size;
70 self
71 }
72
73 pub fn max_size(&mut self, size: UVec2) -> &mut Self {
75 self.max_size = size;
76 self
77 }
78
79 pub fn format(&mut self, format: TextureFormat) -> &mut Self {
81 self.format = format;
82 self
83 }
84
85 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 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 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 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 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 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}