1use std::io::Error as IoError;
22
23use bevy_asset::{
24 AssetLoadError, AssetLoader, AssetPath, LoadContext, ParseAssetPathError, RenderAssetUsages, io::Reader, ron,
25 ron::error::SpannedError,
26};
27use bevy_image::{TextureFormatPixelInfo, prelude::*};
28use bevy_math::{prelude::*, uvec2};
29use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat};
30use bevy_utils::HashMap;
31use guillotiere::{
32 AllocId, AtlasAllocator, Change, ChangeList,
33 euclid::{Box2D, Size2D},
34 size2,
35};
36use serde::{
37 Deserialize, Deserializer, Serialize, Serializer,
38 de::{Error as DeError, MapAccess, Visitor},
39 ser::SerializeStruct,
40};
41use thiserror::Error;
42
43use crate::atlas::{AtlasPage, NineSliceCuts, TextureAtlas};
44
45#[derive(Serialize, Deserialize, Debug, Clone)]
51pub struct TextureAtlasFile {
52 #[serde(default = "TextureAtlasFile::default_padding")]
56 pub padding: u32,
57 #[serde(default = "TextureAtlasFile::default_bleeding")]
62 pub bleeding: u32,
63 #[serde(
64 default = "TextureAtlasFile::default_usages",
65 serialize_with = "TextureAtlasFile::serialize_usages",
66 deserialize_with = "TextureAtlasFile::deserialize_usages"
67 )]
68 pub usages: RenderAssetUsages,
70 pub entries: Vec<TextureAtlasEntry>,
72}
73
74impl TextureAtlasFile {
75 #[inline]
77 pub const fn default_padding() -> u32 {
78 4
79 }
80
81 #[inline]
83 pub const fn default_bleeding() -> u32 {
84 4
85 }
86
87 #[inline]
89 pub const fn default_usages() -> RenderAssetUsages {
90 RenderAssetUsages::RENDER_WORLD
91 }
92
93 #[inline]
95 pub fn serialize_usages<S: Serializer>(usages: &RenderAssetUsages, ser: S) -> Result<S::Ok, S::Error> {
96 let mut u = ser.serialize_struct("RenderAssetUsages", 2)?;
97 u.serialize_field("main", &usages.contains(RenderAssetUsages::MAIN_WORLD))?;
98 u.serialize_field("render", &usages.contains(RenderAssetUsages::RENDER_WORLD))?;
99 u.end()
100 }
101
102 #[inline]
104 pub fn deserialize_usages<'de, D: Deserializer<'de>>(de: D) -> Result<RenderAssetUsages, D::Error> {
105 const FIELDS: &[&str] = &["main", "render"];
106
107 struct Visit;
108 impl<'de> Visitor<'de> for Visit {
109 type Value = RenderAssetUsages;
110
111 #[inline]
112 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
113 write!(formatter, "struct RenderAssetUsages {{ main: bool, render: bool }}")
114 }
115
116 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
117 where
118 A: MapAccess<'de>,
119 {
120 let mut main = None::<bool>;
121 let mut render = None::<bool>;
122 while let Some(key) = map.next_key()? {
123 match key {
124 "main" => match main {
125 None => main = Some(map.next_value()?),
126 Some(..) => return Err(DeError::duplicate_field("main")),
127 },
128 "render" => match render {
129 None => render = Some(map.next_value()?),
130 Some(..) => return Err(DeError::duplicate_field("render")),
131 },
132 e => return Err(DeError::unknown_field(e, FIELDS)),
133 }
134 }
135
136 let main = main.ok_or(DeError::missing_field("main"))?;
137 let render = render.ok_or(DeError::missing_field("render"))?;
138 Ok(match (main, render) {
139 (false, false) => RenderAssetUsages::empty(),
140 (true, false) => RenderAssetUsages::MAIN_WORLD,
141 (false, true) => RenderAssetUsages::RENDER_WORLD,
142 (true, true) => RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
143 })
144 }
145 }
146
147 de.deserialize_struct("RenderAssetUsages", FIELDS, Visit)
148 }
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone)]
153#[serde(untagged)]
154pub enum TextureAtlasEntry {
155 File(String),
157 Directory(String, Vec<TextureAtlasEntry>),
159}
160
161impl<T: ToString> From<T> for TextureAtlasEntry {
162 #[inline]
163 fn from(value: T) -> Self {
164 Self::File(value.to_string())
165 }
166}
167
168#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
171pub struct TextureAtlasSettings {
172 pub init_width: u32,
174 pub init_height: u32,
176 pub max_width: u32,
178 pub max_height: u32,
180}
181
182impl Default for TextureAtlasSettings {
183 #[inline]
184 fn default() -> Self {
185 Self {
186 init_width: 1024,
187 init_height: 1024,
188 max_width: 2048,
189 max_height: 2048,
190 }
191 }
192}
193
194#[derive(Error, Debug)]
196pub enum TextureAtlasError {
197 #[error("Texture '{name}' is too large: [{actual_width}, {actual_height}] > [{max_width}, {max_height}]")]
199 TooLarge {
200 name: String,
202 max_width: u32,
204 max_height: u32,
206 actual_width: u32,
208 actual_height: u32,
210 },
211 #[error("Texture '{name}' has an unsupported format: {format:?}")]
214 UnsupportedFormat {
215 name: String,
217 format: TextureFormat,
219 },
220 #[error("Texture '{name}' failed to load: {error}")]
222 InvalidImage {
223 name: String,
225 error: AssetLoadError,
227 },
228 #[error(transparent)]
230 InvalidPath(#[from] ParseAssetPathError),
231 #[error(transparent)]
233 InvalidFile(#[from] SpannedError),
234 #[error(transparent)]
236 Io(#[from] IoError),
237}
238
239#[derive(Debug, Copy, Clone, Default)]
253pub struct TextureAtlasLoader;
254impl AssetLoader for TextureAtlasLoader {
255 type Asset = TextureAtlas;
256 type Settings = TextureAtlasSettings;
257 type Error = TextureAtlasError;
258
259 async fn load(
260 &self,
261 reader: &mut dyn Reader,
262 settings: &Self::Settings,
263 load_context: &mut LoadContext<'_>,
264 ) -> Result<Self::Asset, Self::Error> {
265 let &Self::Settings {
266 init_width,
267 init_height,
268 max_width,
269 max_height,
270 } = settings;
271
272 let mut bytes = Vec::new();
273 reader.read_to_end(&mut bytes).await?;
274
275 let TextureAtlasFile {
276 padding,
277 bleeding,
278 usages,
279 entries: file_entries,
280 } = ron::de::from_bytes(&bytes)?;
281
282 drop(bytes);
283 let pad = padding as usize;
284 let bleed = (bleeding as usize).min(pad);
285
286 async fn collect(
287 entry: TextureAtlasEntry,
288 base: &AssetPath<'_>,
289 load_context: &mut LoadContext<'_>,
290 accum: &mut Vec<(String, Image, bool)>,
291 ) -> Result<(), TextureAtlasError> {
292 match entry {
293 TextureAtlasEntry::File(path) => {
294 let path = base.resolve(&path)?;
295 let Some(name) = path.path().file_stem() else {
296 return Ok(());
297 };
298
299 let mut name = name.to_string_lossy().into_owned();
300 let has_nine_slice = if let Some((split, "9")) = name.rsplit_once('.') {
301 name = String::from(split);
302 true
303 } else {
304 false
305 };
306
307 let src = match load_context.loader().immediate().load::<Image>(&path).await {
308 Err(e) => return Err(TextureAtlasError::InvalidImage { name, error: e.error }),
309 Ok(src) => src,
310 }
311 .take();
312
313 accum.push((name, src, has_nine_slice));
314 }
315 TextureAtlasEntry::Directory(dir, paths) => {
316 let base = base.resolve(&dir)?;
317 for path in paths {
318 Box::pin(collect(path, &base, load_context, accum)).await?;
319 }
320 }
321 }
322
323 Ok(())
324 }
325
326 let mut entries = Vec::new();
327 for file_entry in file_entries {
328 collect(
329 file_entry,
330 &load_context.asset_path().parent().unwrap(),
331 load_context,
332 &mut entries,
333 )
334 .await?;
335 }
336
337 entries.sort_by_key(|&(.., ref texture, has_nine_slice)| {
338 let UVec2 { mut x, mut y } = texture.size();
339 if has_nine_slice {
340 x = x.saturating_sub(2);
341 y = y.saturating_sub(2);
342 }
343
344 2 * (x + y)
345 });
346
347 let mut atlas = TextureAtlas {
348 pages: Vec::new(),
349 sprite_map: HashMap::new(),
350 };
351
352 let mut end = |ids: HashMap<AllocId, (String, Image, bool)>, packer: AtlasAllocator| {
353 let Size2D {
354 width: page_width,
355 height: page_height,
356 ..
357 } = packer.size().to_u32();
358
359 let pixel_size = TextureFormat::Rgba8UnormSrgb.pixel_size();
360 let mut image = Image::new(
361 Extent3d {
362 width: page_width,
363 height: page_height,
364 depth_or_array_layers: 1,
365 },
366 TextureDimension::D2,
367 vec![0; page_width as usize * page_height as usize * pixel_size],
368 TextureFormat::Rgba8UnormSrgb,
369 usages,
370 );
371
372 let mut sprites = Vec::new();
373 for (id, (name, texture, has_nine_slice)) in ids {
374 let Box2D { min, max } = packer[id].to_usize();
375 let nine_offset = usize::from(has_nine_slice);
376
377 let Some(texture) = texture.convert(TextureFormat::Rgba8UnormSrgb) else {
378 return Err(TextureAtlasError::UnsupportedFormat {
379 name,
380 format: texture.texture_descriptor.format,
381 });
382 };
383
384 let rect_width = max.x - min.x;
385 let rect_height = max.y - min.y;
386
387 let src_row = rect_width - 2 * pad;
388 let src_pos = |x, y| ((y + nine_offset) * (src_row + nine_offset) + (x + nine_offset)) * pixel_size;
389
390 let dst_row = page_width as usize;
391 let dst_pos = |x, y| ((min.y + y) * dst_row + (min.y + x)) * pixel_size;
392
393 for bleed_x in 0..bleed {
396 image.data[dst_pos(pad - bleed_x - 1, pad - bleed)..][..pixel_size]
397 .copy_from_slice(&texture.data[src_pos(0, 0)..][..pixel_size]);
398
399 image.data[dst_pos(rect_width - pad + bleed_x, pad - bleed)..][..pixel_size]
400 .copy_from_slice(&texture.data[src_pos(src_row - 1, 0)..][..pixel_size]);
401 }
402
403 image.data[dst_pos(pad, pad - bleed)..][..src_row * pixel_size]
405 .copy_from_slice(&texture.data[src_pos(0, 0)..][..src_row * pixel_size]);
406 for bleed_y in 1..bleed {
407 let split = dst_pos(pad - bleed, pad - bleed + bleed_y);
408 let (src, dst) = image.data.split_at_mut(split);
409
410 let count = (src_row + 2 * bleed) * pixel_size;
411 dst[..count].copy_from_slice(&src[split - dst_row * pixel_size..][..count]);
412 }
413
414 for y in 0..rect_height - 2 * pad {
416 let count = src_row * pixel_size;
417 image.data[dst_pos(pad, pad + y)..][..count].copy_from_slice(&texture.data[src_pos(0, y)..][..count]);
418
419 for bleed_x in 0..bleed {
420 image.data[dst_pos(pad - bleed_x - 1, pad + y)..][..pixel_size]
421 .copy_from_slice(&texture.data[src_pos(0, y)..][..pixel_size]);
422
423 image.data[dst_pos(rect_width - pad + bleed_x, pad + y)..][..pixel_size]
424 .copy_from_slice(&texture.data[src_pos(src_row - 1, y)..][..pixel_size]);
425 }
426 }
427
428 for bleed_y in 0..bleed {
430 let split = dst_pos(pad - bleed, rect_height - pad + bleed_y);
431 let (src, dst) = image.data.split_at_mut(split);
432
433 let count = (src_row + 2 * bleed) * pixel_size;
434 dst[..count].copy_from_slice(&src[split - dst_row * pixel_size..][..count]);
435 }
436
437 atlas.sprite_map.insert(name, (atlas.pages.len(), sprites.len()));
439 sprites.push((
440 URect {
441 min: uvec2(min.x as u32 + padding, min.y as u32 + padding),
442 max: uvec2(max.x as u32 - padding, max.y as u32 - padding),
443 },
444 if has_nine_slice {
445 let mut cuts = NineSliceCuts {
446 left: 0,
447 right: 0,
448 top: src_row as u32,
449 bottom: rect_height as u32 - 2 * padding,
450 };
451
452 let mut found_left = false;
453 for x in 1..src_row + 1 {
454 let alpha = texture.data[x * pixel_size + 3];
455 if !found_left && alpha >= 127 {
456 found_left = true;
457 cuts.left = x as u32;
458 } else if found_left && alpha < 127 {
459 cuts.right = x as u32;
460 break
461 }
462 }
463
464 let mut found_top = false;
465 for y in 1..rect_height - 2 * pad + 1 {
466 let alpha = texture.data[y * (src_row + nine_offset) * pixel_size + 3];
467 if !found_top && alpha >= 127 {
468 found_top = true;
469 cuts.top = y as u32;
470 } else if found_top && alpha < 127 {
471 cuts.bottom = y as u32;
472 break
473 }
474 }
475
476 Some(cuts)
477 } else {
478 None
479 },
480 ));
481 }
482
483 let page_num = atlas.pages.len();
484 atlas.pages.push(AtlasPage {
485 image: load_context.add_labeled_asset(format!("page-{page_num}"), image),
486 sprites,
487 });
488
489 Ok(())
490 };
491
492 'pages: while !entries.is_empty() {
493 let mut packer = AtlasAllocator::new(size2(init_width as i32, init_height as i32));
494 let mut ids = HashMap::<AllocId, (String, Image, bool)>::new();
495
496 while let Some((name, texture, has_nine_slice)) = entries.pop() {
497 let UVec2 {
498 x: mut base_width,
499 y: mut base_height,
500 } = texture.size();
501
502 if has_nine_slice {
503 base_width = base_width.saturating_sub(1);
504 base_height = base_height.saturating_sub(1);
505 }
506
507 match packer.allocate(size2(
508 (base_width + 2 * pad as u32) as i32,
509 (base_height + 2 * pad as u32) as i32,
510 )) {
511 Some(alloc) => {
512 ids.insert(alloc.id, (name, texture, has_nine_slice));
513 }
514 None => {
515 let Size2D { width, height, .. } = packer.size();
516 if width == max_width as i32 && height == max_height as i32 {
517 if packer.is_empty() {
518 return Err(TextureAtlasError::TooLarge {
519 name,
520 max_width,
521 max_height,
522 actual_width: width as u32,
523 actual_height: height as u32,
524 });
525 } else {
526 end(ids, packer)?;
527
528 entries.push((name, texture, has_nine_slice));
530 continue 'pages;
531 }
532 }
533
534 let ChangeList { changes, failures } = packer.resize_and_rearrange(size2(
535 (width * 2).min(max_width as i32),
536 (height * 2).min(max_height as i32),
537 ));
538
539 if !failures.is_empty() {
540 unreachable!("resizing shouldn't cause rectangles to become unfittable")
541 }
542
543 let mut id_map = HashMap::new();
544 for Change { old, new } in changes {
545 let rect = ids.remove(&old.id).unwrap();
546 id_map.insert(new.id, rect);
547 }
548
549 if !ids.is_empty() {
550 unreachable!("resizing should clear all old rectangles")
551 }
552
553 ids = id_map;
554 }
555 }
556 }
557
558 end(ids, packer)?;
559 }
560
561 Ok(atlas)
562 }
563
564 #[inline]
565 fn extensions(&self) -> &[&str] {
566 &["atlas.ron"]
567 }
568}