Skip to main content

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}