hephae_atlas/
atlas.rs

1//! Provides texture atlas functionality.
2//!
3//! A texture atlas contains atlas pages, i.e. lists of textures packed into one large texture in
4//! order to reduce the amount of bind groups necessary to hold the information passed to shaders.
5//! This means integrating a texture atlas into `Vertex` rendering will significantly increase
6//! batching potential, leading to fewer GPU render calls.
7//!
8//! This module provides the [`TextureAtlas`] type. See [this module](crate::asset) for more
9//! information on how the atlas implements [`Asset`].
10//!
11//! This module provides [`AtlasEntry`] and [`AtlasIndex`] components; the former being the
12//! atlas lookup key, and the latter being the cached sprite index. The dedicated
13//! [`update_atlas_index`] system listens to changes/additions to texture atlas assets and updates
14//! the `AtlasIndex` of entities accordingly.
15//!
16//! See the `examples/atlas` for a full example.
17
18use std::borrow::Cow;
19
20use bevy_asset::{ReflectAsset, prelude::*};
21use bevy_ecs::prelude::*;
22use bevy_image::prelude::*;
23use bevy_math::prelude::*;
24use bevy_reflect::prelude::*;
25use bevy_utils::{HashMap, HashSet, prelude::*};
26use nonmax::NonMaxUsize;
27
28/// A list of textures packed into one large texture. See the [module-level](crate::atlas)
29/// documentation for more specific information on how to integrate this into your rendering
30/// framework.
31#[derive(Asset, Reflect, Clone, Debug)]
32#[reflect(Asset, Debug)]
33pub struct TextureAtlas {
34    /// The list of pages contained in this atlas. Items may be modified, but growing or shrinking
35    /// this vector is **discouraged**.
36    pub pages: Vec<AtlasPage>,
37    /// Mapping of sprite names to `(P, Q)` where `P` is the [page index](Self::pages) and `Q` is
38    /// the [sprite index](AtlasPage::sprites). Only ever modify if you know what you're doing.
39    pub sprite_map: HashMap<String, (usize, usize)>,
40}
41
42/// A page located in a [`TextureAtlas`]. Contains the handle to the page image, and rectangle
43/// placements of each sprites.
44#[derive(Reflect, Clone, Debug)]
45#[reflect(Debug)]
46pub struct AtlasPage {
47    /// The page handle.
48    pub image: Handle<Image>,
49    /// List of sprite rectangle placements in the page; may be looked up from
50    /// [`TextureAtlas::sprite_map`]. Each elements consist of the sprite's placement in the page,
51    /// and a nine-slice cuts if any.
52    pub sprites: Vec<(URect, Option<NineSliceCuts>)>,
53}
54
55/// Defines horizontal and vertical slashes that split a sprite into nine patches.
56#[derive(Reflect, Copy, Clone, Debug)]
57#[reflect(Debug)]
58pub struct NineSliceCuts {
59    /// The leftmost vertical cut. Pixels that `x < left` are considered the left side edge.
60    pub left: u32,
61    /// The rightmost vertical cut. Pixels that `x > right` are considered the right side edge.
62    pub right: u32,
63    /// The topmost vertical cut. Pixels that `y < top` are considered the top side edge.
64    pub top: u32,
65    /// The bottommost vertical cut. Pixels that `y > bottom` are considered the bottom side edge.
66    pub bottom: u32,
67}
68
69/// Component denoting a texture atlas sprite lookup key. See the [module-level](crate::atlas)
70/// documentation for more specific information on how to integrate this into your rendering
71/// framework.
72#[derive(Reflect, Component, Clone, Debug)]
73#[reflect(Component, Debug)]
74#[require(AtlasIndex)]
75pub struct AtlasEntry {
76    /// The handle to the texture atlas.
77    pub atlas: Handle<TextureAtlas>,
78    /// The lookup key.
79    pub key: Cow<'static, str>,
80}
81
82/// Component denoting a texture atlas cached sprite index. See the [module-level](crate::atlas)
83/// documentation for more specific information on how to integrate this into your rendering
84/// framework.
85#[derive(Component, Default, Copy, Clone, Debug)]
86pub struct AtlasIndex {
87    page_index: Option<NonMaxUsize>,
88    sprite_index: Option<NonMaxUsize>,
89}
90
91impl AtlasIndex {
92    /// Obtains the [page index](TextureAtlas::pages) and [sprite index](AtlasPage::sprites), or
93    /// [`None`] if the [key](AtlasIndex) is invalid.
94    #[inline]
95    pub const fn indices(self) -> Option<(usize, usize)> {
96        match (self.page_index, self.sprite_index) {
97            (Some(page), Some(sprite)) => Some((page.get(), sprite.get())),
98            _ => None,
99        }
100    }
101}
102
103/// System to update [`AtlasIndex`] according to changes [`AtlasEntry`] and [`TextureAtlas`] assets.
104pub fn update_atlas_index(
105    mut events: EventReader<AssetEvent<TextureAtlas>>,
106    atlases: Res<Assets<TextureAtlas>>,
107    mut entries: ParamSet<(
108        Query<(&AtlasEntry, &mut AtlasIndex), Or<(Changed<AtlasEntry>, Added<AtlasIndex>)>>,
109        Query<(&AtlasEntry, &mut AtlasIndex)>,
110    )>,
111    mut changed: Local<HashSet<AssetId<TextureAtlas>>>,
112) {
113    changed.clear();
114    for &event in events.read() {
115        if let AssetEvent::Added { id } | AssetEvent::Modified { id } = event {
116            changed.insert(id);
117        }
118    }
119
120    let update = |entry: &AtlasEntry, mut index: Mut<AtlasIndex>| {
121        let Some(atlas) = atlases.get(&entry.atlas) else {
122            return;
123        };
124        let Some(&(page, sprite)) = atlas.sprite_map.get(&*entry.key) else {
125            *index = default();
126            return;
127        };
128
129        *index = AtlasIndex {
130            page_index: NonMaxUsize::new(page),
131            sprite_index: NonMaxUsize::new(sprite),
132        };
133    };
134
135    if changed.is_empty() {
136        for (entry, index) in &mut entries.p0() {
137            update(entry, index);
138        }
139    } else {
140        for (entry, mut index) in &mut entries.p1() {
141            if !changed.contains(&entry.atlas.id()) {
142                *index = default();
143                continue;
144            }
145
146            update(entry, index);
147        }
148    }
149}