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}