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}