1use crate::{Image, TextureAccessError, TextureAtlasLayout, TextureFormatPixelInfo as _};
2use bevy_asset::RenderAssetUsages;
3use bevy_math::{URect, UVec2};
4use guillotiere::{size2, Allocation, AtlasAllocator};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
10pub enum DynamicTextureAtlasBuilderError {
11 #[error("Couldn't allocate space to add the image requested")]
13 FailedToAllocateSpace,
14 #[error("cannot add texture to uninitialized atlas texture")]
16 UninitializedAtlas,
17 #[error("cannot add uninitialized texture to atlas")]
19 UninitializedSourceTexture,
20 #[error("texture access error: {0}")]
22 TextureAccess(#[from] TextureAccessError),
23}
24
25pub struct DynamicTextureAtlasBuilder {
30 atlas_allocator: AtlasAllocator,
31 padding: u32,
32}
33
34impl DynamicTextureAtlasBuilder {
35 pub fn new(size: UVec2, padding: u32) -> Self {
43 debug_assert!(size.x > padding && size.y > padding);
45 Self {
46 atlas_allocator: AtlasAllocator::new(to_size2(size - padding)),
49 padding,
50 }
51 }
52
53 pub fn add_texture(
65 &mut self,
66 atlas_layout: &mut TextureAtlasLayout,
67 texture: &Image,
68 atlas_texture: &mut Image,
69 ) -> Result<usize, DynamicTextureAtlasBuilderError> {
70 let allocation = self.atlas_allocator.allocate(size2(
73 (texture.width() + self.padding).try_into().unwrap(),
74 (texture.height() + self.padding).try_into().unwrap(),
75 ));
76 if let Some(mut allocation) = allocation {
77 assert!(
78 atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),
79 "The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"
80 );
81 let rect = &mut allocation.rectangle;
82 rect.min.x += self.padding as i32;
85 rect.min.y += self.padding as i32;
86
87 self.place_texture(atlas_texture, allocation, texture)?;
88 Ok(atlas_layout.add_texture(to_rect(allocation.rectangle)))
89 } else {
90 Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)
91 }
92 }
93
94 fn place_texture(
95 &mut self,
96 atlas_texture: &mut Image,
97 allocation: Allocation,
98 texture: &Image,
99 ) -> Result<(), DynamicTextureAtlasBuilderError> {
100 let rect = &allocation.rectangle;
101 let atlas_width = atlas_texture.width() as usize;
102 let rect_width = rect.width() as usize;
103 let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
104
105 let Some(ref mut atlas_data) = atlas_texture.data else {
106 return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);
107 };
108 let Some(ref data) = texture.data else {
109 return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);
110 };
111 for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
112 let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
113 let end = begin + rect_width * format_size;
114 let texture_begin = texture_y * rect_width * format_size;
115 let texture_end = texture_begin + rect_width * format_size;
116 atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
117 }
118 Ok(())
119 }
120}
121
122fn to_rect(rectangle: guillotiere::Rectangle) -> URect {
123 URect {
124 min: UVec2::new(
125 rectangle.min.x.try_into().unwrap(),
126 rectangle.min.y.try_into().unwrap(),
127 ),
128 max: UVec2::new(
129 rectangle.max.x.try_into().unwrap(),
130 rectangle.max.y.try_into().unwrap(),
131 ),
132 }
133}
134
135fn to_size2(vec2: UVec2) -> guillotiere::Size {
136 guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
137}
138
139#[cfg(test)]
140mod tests {
141 use bevy_asset::RenderAssetUsages;
142 use bevy_math::{URect, UVec2};
143
144 use crate::{DynamicTextureAtlasBuilder, Image, TextureAtlasLayout};
145
146 fn make_filled_image(size: UVec2, pixel_rgba_bytes: [u8; 4]) -> Image {
147 Image::new_fill(
148 wgpu_types::Extent3d {
149 width: size.x,
150 height: size.y,
151 depth_or_array_layers: 1,
152 },
153 wgpu_types::TextureDimension::D2,
154 &pixel_rgba_bytes,
155 wgpu_types::TextureFormat::Rgba8Unorm,
156 RenderAssetUsages::all(),
157 )
158 }
159
160 fn rect_contains_value(image: &Image, rect: URect, pixel_rgba_bytes: [u8; 4]) -> bool {
161 let image_data = image.data.as_ref().unwrap();
162 for y in rect.min.y..rect.max.y {
163 for x in rect.min.x..rect.max.x {
164 let byte_start = ((x + y * image.width()) * 4) as usize;
165 if image_data[byte_start..(byte_start + 4)] != pixel_rgba_bytes {
166 return false;
167 }
168 }
169 }
170 true
171 }
172
173 #[test]
174 fn allocate_textures() {
175 let size = UVec2::new(30, 30);
176
177 let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
178 let mut layout = TextureAtlasLayout::new_empty(size);
179 let mut builder = DynamicTextureAtlasBuilder::new(size, 0);
180
181 let square = UVec2::new(10, 10);
182 let colors = [
183 [255, 0, 0, 255],
184 [0, 255, 0, 255],
185 [0, 0, 255, 255],
186 [255, 0, 255, 255],
187 [0, 255, 255, 255],
188 [0, 255, 255, 255],
189 ];
190 let texture_0 = builder
191 .add_texture(
192 &mut layout,
193 &make_filled_image(square, colors[0]),
194 &mut atlas_texture,
195 )
196 .unwrap();
197 let texture_1 = builder
198 .add_texture(
199 &mut layout,
200 &make_filled_image(square, colors[1]),
201 &mut atlas_texture,
202 )
203 .unwrap();
204 let texture_2 = builder
205 .add_texture(
206 &mut layout,
207 &make_filled_image(square, colors[2]),
208 &mut atlas_texture,
209 )
210 .unwrap();
211 let texture_3 = builder
212 .add_texture(
213 &mut layout,
214 &make_filled_image(square, colors[3]),
215 &mut atlas_texture,
216 )
217 .unwrap();
218 let texture_4 = builder
219 .add_texture(
220 &mut layout,
221 &make_filled_image(square, colors[4]),
222 &mut atlas_texture,
223 )
224 .unwrap();
225 let texture_5 = builder
226 .add_texture(
227 &mut layout,
228 &make_filled_image(square, colors[5]),
229 &mut atlas_texture,
230 )
231 .unwrap();
232
233 let expected_rects = [
234 URect::from_corners(UVec2::new(0, 0), UVec2::new(10, 10)),
235 URect::from_corners(UVec2::new(10, 0), UVec2::new(20, 10)),
236 URect::from_corners(UVec2::new(20, 0), UVec2::new(30, 10)),
237 URect::from_corners(UVec2::new(0, 10), UVec2::new(10, 20)),
238 URect::from_corners(UVec2::new(0, 20), UVec2::new(10, 30)),
239 URect::from_corners(UVec2::new(10, 10), UVec2::new(20, 20)),
240 ];
241 assert_eq!(layout.textures[texture_0], expected_rects[0]);
242 assert_eq!(layout.textures[texture_1], expected_rects[1]);
243 assert_eq!(layout.textures[texture_2], expected_rects[2]);
244 assert_eq!(layout.textures[texture_3], expected_rects[3]);
245 assert_eq!(layout.textures[texture_4], expected_rects[4]);
246 assert_eq!(layout.textures[texture_5], expected_rects[5]);
247
248 assert!(rect_contains_value(
249 &atlas_texture,
250 expected_rects[0],
251 colors[0]
252 ));
253 assert!(rect_contains_value(
254 &atlas_texture,
255 expected_rects[1],
256 colors[1]
257 ));
258 assert!(rect_contains_value(
259 &atlas_texture,
260 expected_rects[2],
261 colors[2]
262 ));
263 assert!(rect_contains_value(
264 &atlas_texture,
265 expected_rects[3],
266 colors[3]
267 ));
268 assert!(rect_contains_value(
269 &atlas_texture,
270 expected_rects[4],
271 colors[4]
272 ));
273 assert!(rect_contains_value(
274 &atlas_texture,
275 expected_rects[5],
276 colors[5]
277 ));
278 }
279
280 #[test]
281 fn allocate_textures_with_padding() {
282 let size = UVec2::new(12, 12);
283
284 let mut atlas_texture = make_filled_image(size, [0, 0, 0, 0]);
285 let mut layout = TextureAtlasLayout::new_empty(size);
286 let mut builder = DynamicTextureAtlasBuilder::new(size, 1);
287
288 let square = UVec2::new(3, 3);
289 let colors = [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]];
290 let texture_0 = builder
291 .add_texture(
292 &mut layout,
293 &make_filled_image(square, colors[0]),
294 &mut atlas_texture,
295 )
296 .unwrap();
297 let texture_1 = builder
298 .add_texture(
299 &mut layout,
300 &make_filled_image(square, colors[1]),
301 &mut atlas_texture,
302 )
303 .unwrap();
304 let texture_2 = builder
305 .add_texture(
306 &mut layout,
307 &make_filled_image(square, colors[2]),
308 &mut atlas_texture,
309 )
310 .unwrap();
311
312 let expected_rects = [
313 URect::from_corners(UVec2::new(1, 1), UVec2::new(4, 4)),
314 URect::from_corners(UVec2::new(5, 1), UVec2::new(8, 4)),
315 URect::from_corners(UVec2::new(1, 5), UVec2::new(4, 8)),
319 ];
320 assert_eq!(layout.textures[texture_0], expected_rects[0]);
321 assert_eq!(layout.textures[texture_1], expected_rects[1]);
322 assert_eq!(layout.textures[texture_2], expected_rects[2]);
323
324 assert!(rect_contains_value(
325 &atlas_texture,
326 expected_rects[0],
327 colors[0]
328 ));
329 assert!(rect_contains_value(
330 &atlas_texture,
331 expected_rects[1],
332 colors[1]
333 ));
334 assert!(rect_contains_value(
335 &atlas_texture,
336 expected_rects[2],
337 colors[2]
338 ));
339 }
340}