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