bevy_image/texture_atlas.rs
1use bevy_app::prelude::*;
2use bevy_asset::{Asset, AssetApp as _, AssetId, Assets, Handle};
3use bevy_math::{Rect, URect, UVec2};
4use bevy_platform::collections::HashMap;
5#[cfg(not(feature = "bevy_reflect"))]
6use bevy_reflect::TypePath;
7#[cfg(feature = "bevy_reflect")]
8use bevy_reflect::{std_traits::ReflectDefault, Reflect};
9#[cfg(feature = "serialize")]
10use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
11
12use crate::Image;
13
14/// Adds support for texture atlases.
15pub struct TextureAtlasPlugin;
16
17impl Plugin for TextureAtlasPlugin {
18 fn build(&self, app: &mut App) {
19 app.init_asset::<TextureAtlasLayout>();
20
21 #[cfg(feature = "bevy_reflect")]
22 app.register_asset_reflect::<TextureAtlasLayout>()
23 .register_type::<TextureAtlas>();
24 }
25}
26
27/// Stores a mapping from sub texture handles to the related area index.
28///
29/// Generated by [`TextureAtlasBuilder`].
30///
31/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
32#[derive(Debug)]
33pub struct TextureAtlasSources {
34 /// Maps from a specific image handle to the index in `textures` where they can be found.
35 pub texture_ids: HashMap<AssetId<Image>, usize>,
36}
37impl TextureAtlasSources {
38 /// Retrieves the texture *section* index of the given `texture` handle.
39 pub fn texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
40 let id = texture.into();
41 self.texture_ids.get(&id).cloned()
42 }
43
44 /// Creates a [`TextureAtlas`] handle for the given `texture` handle.
45 pub fn handle(
46 &self,
47 layout: Handle<TextureAtlasLayout>,
48 texture: impl Into<AssetId<Image>>,
49 ) -> Option<TextureAtlas> {
50 Some(TextureAtlas {
51 layout,
52 index: self.texture_index(texture)?,
53 })
54 }
55
56 /// Retrieves the texture *section* rectangle of the given `texture` handle in pixels.
57 pub fn texture_rect(
58 &self,
59 layout: &TextureAtlasLayout,
60 texture: impl Into<AssetId<Image>>,
61 ) -> Option<URect> {
62 layout.textures.get(self.texture_index(texture)?).cloned()
63 }
64
65 /// Retrieves the texture *section* rectangle of the given `texture` handle in UV coordinates.
66 /// These are within the range [0..1], as a fraction of the entire texture atlas' size.
67 pub fn uv_rect(
68 &self,
69 layout: &TextureAtlasLayout,
70 texture: impl Into<AssetId<Image>>,
71 ) -> Option<Rect> {
72 self.texture_rect(layout, texture).map(|rect| {
73 let rect = rect.as_rect();
74 let size = layout.size.as_vec2();
75 Rect::from_corners(rect.min / size, rect.max / size)
76 })
77 }
78}
79
80/// Stores a map used to lookup the position of a texture in a [`TextureAtlas`].
81/// This can be used to either use and look up a specific section of a texture, or animate frame-by-frame as a sprite sheet.
82///
83/// Optionally it can store a mapping from sub texture handles to the related area index (see
84/// [`TextureAtlasBuilder`]).
85///
86/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
87/// [Example usage animating sprite in response to an event.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
88/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
89///
90/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
91#[derive(Asset, PartialEq, Eq, Debug, Clone)]
92#[cfg_attr(
93 feature = "bevy_reflect",
94 derive(Reflect),
95 reflect(Debug, PartialEq, Clone)
96)]
97#[cfg_attr(
98 feature = "serialize",
99 derive(serde::Serialize, serde::Deserialize),
100 reflect(Serialize, Deserialize)
101)]
102#[cfg_attr(not(feature = "bevy_reflect"), derive(TypePath))]
103pub struct TextureAtlasLayout {
104 /// Total size of texture atlas.
105 pub size: UVec2,
106 /// The specific areas of the atlas where each texture can be found
107 pub textures: Vec<URect>,
108}
109
110impl TextureAtlasLayout {
111 /// Create a new empty layout with custom `dimensions`
112 pub fn new_empty(dimensions: UVec2) -> Self {
113 Self {
114 size: dimensions,
115 textures: Vec::new(),
116 }
117 }
118
119 /// Generate a [`TextureAtlasLayout`] as a grid where each
120 /// `tile_size` by `tile_size` grid-cell is one of the *section* in the
121 /// atlas. Grid cells are separated by some `padding`, and the grid starts
122 /// at `offset` pixels from the top left corner. Resulting layout is
123 /// indexed left to right, top to bottom.
124 ///
125 /// # Arguments
126 ///
127 /// * `tile_size` - Each layout grid cell size
128 /// * `columns` - Grid column count
129 /// * `rows` - Grid row count
130 /// * `padding` - Optional padding between cells
131 /// * `offset` - Optional global grid offset
132 pub fn from_grid(
133 tile_size: UVec2,
134 columns: u32,
135 rows: u32,
136 padding: Option<UVec2>,
137 offset: Option<UVec2>,
138 ) -> Self {
139 let padding = padding.unwrap_or_default();
140 let offset = offset.unwrap_or_default();
141 let mut sprites = Vec::new();
142 let mut current_padding = UVec2::ZERO;
143
144 for y in 0..rows {
145 if y > 0 {
146 current_padding.y = padding.y;
147 }
148 for x in 0..columns {
149 if x > 0 {
150 current_padding.x = padding.x;
151 }
152
153 let cell = UVec2::new(x, y);
154 let rect_min = (tile_size + current_padding) * cell + offset;
155
156 sprites.push(URect {
157 min: rect_min,
158 max: rect_min + tile_size,
159 });
160 }
161 }
162
163 let grid_size = UVec2::new(columns, rows);
164
165 Self {
166 size: ((tile_size + current_padding) * grid_size) - current_padding,
167 textures: sprites,
168 }
169 }
170
171 /// Add a *section* to the list in the layout and returns its index
172 /// which can be used with [`TextureAtlas`]
173 ///
174 /// # Arguments
175 ///
176 /// * `rect` - The section of the texture to be added
177 ///
178 /// [`TextureAtlas`]: crate::TextureAtlas
179 pub fn add_texture(&mut self, rect: URect) -> usize {
180 self.textures.push(rect);
181 self.textures.len() - 1
182 }
183
184 /// The number of textures in the [`TextureAtlasLayout`]
185 pub fn len(&self) -> usize {
186 self.textures.len()
187 }
188
189 pub fn is_empty(&self) -> bool {
190 self.textures.is_empty()
191 }
192}
193
194/// An index into a [`TextureAtlasLayout`], which corresponds to a specific section of a texture.
195///
196/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
197/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
198/// image file for either sprite animation or global mapping.
199/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
200/// for efficient rendering of related game objects.
201///
202/// Check the following examples for usage:
203/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
204/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
205/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
206#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
207#[cfg_attr(
208 feature = "bevy_reflect",
209 derive(Reflect),
210 reflect(Default, Debug, PartialEq, Hash, Clone)
211)]
212pub struct TextureAtlas {
213 /// Texture atlas layout handle
214 pub layout: Handle<TextureAtlasLayout>,
215 /// Texture atlas section index
216 pub index: usize,
217}
218
219impl TextureAtlas {
220 /// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
221 pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
222 let atlas = texture_atlases.get(&self.layout)?;
223 atlas.textures.get(self.index).copied()
224 }
225}
226
227impl From<Handle<TextureAtlasLayout>> for TextureAtlas {
228 fn from(texture_atlas: Handle<TextureAtlasLayout>) -> Self {
229 Self {
230 layout: texture_atlas,
231 index: 0,
232 }
233 }
234}