hephae_atlas/
asset.rs

1//! [`AssetLoader`] implementation for loading [`TextureAtlas`].
2//!
3//! Format is as the following example:
4//! ```ron
5//! (
6//!     padding: 4,
7//!     bleeding: 4,
8//!     usages: (
9//!         main: false,
10//!         render: true,
11//!     ),
12//!     entries: [
13//!         "some-file-relative-to-atlas-file.png",
14//!         ("some-dir-relative-to-atlas-file", [
15//!             "some-file-inside-the-dir.png",
16//!         ]),
17//!     ],
18//! )
19//! ```
20
21use 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/// Asset file representation of [`TextureAtlas`].
46///
47/// This struct `impl`s [`Serialize`] and
48/// [`Deserialize`], which means it may be (de)serialized into any implementation, albeit
49/// [`TextureAtlasLoader`] uses [RON](ron) format specifically.
50#[derive(Serialize, Deserialize, Debug, Clone)]
51pub struct TextureAtlasFile {
52    /// How far away the edges of one sprite to another and to the page boundaries, in pixels. This
53    /// may be utilized to mitigate the imperfect precision with texture sampling where a fraction
54    /// of neighboring sprites actually get sampled instead.
55    #[serde(default = "TextureAtlasFile::default_padding")]
56    pub padding: u32,
57    /// How much the sprites will "bleed" outside its edge. That is, how much times the edges of a
58    /// sprite is copied to its surrounding border, creating a bleeding effect. This may be utilized
59    /// to mitigate the imperfect precision with texture sampling where the edge of a sprite doesn't
60    /// quite reach the edge of the vertices.
61    #[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    /// Defines the usages for the resulting atlas pages.
69    pub usages: RenderAssetUsages,
70    /// File entries relative to the atlas configuration file.
71    pub entries: Vec<TextureAtlasEntry>,
72}
73
74impl TextureAtlasFile {
75    /// Default padding of a texture atlas is 4 pixels.
76    #[inline]
77    pub const fn default_padding() -> u32 {
78        4
79    }
80
81    /// Default bleeding of a texture atlas is 4 pixels.
82    #[inline]
83    pub const fn default_bleeding() -> u32 {
84        4
85    }
86
87    /// Default usage of texture atlas pages is [RenderAssetUsages::RENDER_WORLD].
88    #[inline]
89    pub const fn default_usages() -> RenderAssetUsages {
90        RenderAssetUsages::RENDER_WORLD
91    }
92
93    /// Serializes the usages into `(main: <bool>, render: <bool>)`.
94    #[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    /// Deserializes the usages from `(main: <bool>, render: <bool>)`.
103    #[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/// A [TextureAtlas] file entry. May either be a file or a directory containing files.
152#[derive(Serialize, Deserialize, Debug, Clone)]
153#[serde(untagged)]
154pub enum TextureAtlasEntry {
155    /// Defines a relative path to an [Image] file.
156    File(String),
157    /// Defines a directory relative to the current one, filled with more entries.
158    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/// Additional settings that may be adjusted when loading a [TextureAtlas]. This is typically used
169/// to limit texture sizes to what the rendering backend supports.
170#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
171pub struct TextureAtlasSettings {
172    /// The initial width of an atlas page. Gradually grows to [Self::max_width] if insufficient.
173    pub init_width: u32,
174    /// The initial height of an atlas page. Gradually grows to [Self::max_height] if insufficient.
175    pub init_height: u32,
176    /// The maximum width of an atlas page. If insufficient, a new page must be allocated.
177    pub max_width: u32,
178    /// The maximum height of an atlas page. If insufficient, a new page must be allocated.
179    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/// Errors that may arise when loading a [`TextureAtlas`].
195#[derive(Error, Debug)]
196pub enum TextureAtlasError {
197    /// Error that arises when a texture is larger than the maximum size of the atlas page.
198    #[error("Texture '{name}' is too large: [{actual_width}, {actual_height}] > [{max_width}, {max_height}]")]
199    TooLarge {
200        /// The sprite lookup key.
201        name: String,
202        /// The maximum width of the atlas page. See [`TextureAtlasSettings::max_width`].
203        max_width: u32,
204        /// The maximum width of the atlas page. See [`TextureAtlasSettings::max_height`].
205        max_height: u32,
206        /// The width of the erroneous texture.
207        actual_width: u32,
208        /// The height of the erroneous texture.
209        actual_height: u32,
210    },
211    /// Error that arises when the texture couldn't be converted into
212    /// [`TextureFormat::Rgba8UnormSrgb`].
213    #[error("Texture '{name}' has an unsupported format: {format:?}")]
214    UnsupportedFormat {
215        /// The sprite lookup key.
216        name: String,
217        /// The invalid texture format.
218        format: TextureFormat,
219    },
220    /// Error that arises when the texture couldn't be loaded at all.
221    #[error("Texture '{name}' failed to load: {error}")]
222    InvalidImage {
223        /// The sprite lookup key.
224        name: String,
225        /// The error that arises when trying to load the texture.
226        error: AssetLoadError,
227    },
228    /// Error that arises when a texture has an invalid path string.
229    #[error(transparent)]
230    InvalidPath(#[from] ParseAssetPathError),
231    /// Error that arises when the `.atlas` file has an invalid RON syntax.
232    #[error(transparent)]
233    InvalidFile(#[from] SpannedError),
234    /// Error that arises when an IO error occurs.
235    #[error(transparent)]
236    Io(#[from] IoError),
237}
238
239/// Dedicated [`AssetLoader`] to load [`TextureAtlas`].
240///
241/// Parses file into [`TextureAtlasFile`]
242/// representation, and accepts [`TextureAtlasSettings`] as additional optional configuration. May
243/// throw [`TextureAtlasError`] for erroneous assets.
244///
245/// This asset loader adds each texture atlas entry as a "load dependency." As much, coupled with a
246/// file system watcher, mutating these input image files will cause reprocessing of the atlas.
247///
248/// This asset loader also adds [`pages[i].image`](AtlasPage::image) as a labelled asset with label
249/// `"page-{i}"` (without the brackets). Therefore, doing (for example)
250/// `server.load::<Image>("sprites.atlas.ron#page-0")` is possible and will return the 0th page
251/// image of the atlas, provided the atlas actually has a 0th page.
252#[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                // Set topleft-wards bleeding to topleft pixel and topright-wards bleeding to topright pixel. This
394                // is so that the subsequent bleeding operation may just use a split-off copy.
395                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                // Copy top-most edge to bleed upwards.
404                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                // Copy the actual image, while performing sideways bleeding.
415                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                // Copy the bottom-most edge to bleed downwards.
429                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                // Finally, insert to the sprite map.
438                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                                // Re-insert the entry to the back, since we didn't end up packing that one.
529                                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}