 af9b073b0f
			
		
	
	
		af9b073b0f
		
			
		
	
	
	
	
		
			
			# Objective Mostly covers the first point in https://github.com/bevyengine/bevy/issues/13713#issuecomment-2364786694 The idea here is that a lot of people want to load their own texture atlases, and many of them do this by deserializing some custom version of `TextureAtlasLayout`. This makes that a little easier by providing `serde` impls for them. ## Solution In order to make `TextureAtlasLayout` serializable, the custom texture mappings that are added by `TextureAtlasBuilder` were separated into their own type, `TextureAtlasSources`. The inner fields are made public so people can create their own version of this type, although because it embeds asset IDs, it's not as easily serializable. In particular, atlases that are loaded directly (e.g. sprite sheets) will not have a copy of this map, and so, don't need to construct it at all. As an aside, since this is the very first thing in `bevy_sprite` with `serde` impls, I've added a `serialize` feature to the crate and made sure it gets activated when the `serialize` feature is enabled on the parent `bevy` crate. ## Testing I was kind of shocked that there isn't anywhere in the code besides a single example that actually used this functionality, so, it was relatively straightforward to do. In #13713, among other places, folks have mentioned adding custom serialization into their pipelines. It would be nice to hear from people whether this change matches what they're doing in their code, and if it's relatively seamless to adapt to. I suspect that the answer is yes, but, that's mainly the only other kind of testing that can be added. ## Migration Guide `TextureAtlasBuilder` no longer stores a mapping back to the original images in `TextureAtlasLayout`; that functionality has been added to a new struct, `TextureAtlasSources`, instead. This also means that the signature for `TextureAtlasBuilder::finish` has changed, meaning that calls of the form: ```rust let (atlas_layout, image) = builder.build()?; ``` Will now change to the form: ```rust let (atlas_layout, atlas_sources, image) = builder.build()?; ``` And instead of performing a reverse-lookup from the layout, like so: ```rust let atlas_layout_handle = texture_atlases.add(atlas_layout.clone()); let index = atlas_layout.get_texture_index(&my_handle); let handle = TextureAtlas { layout: atlas_layout_handle, index, }; ``` You can perform the lookup from the sources instead: ```rust let atlas_layout = texture_atlases.add(atlas_layout); let index = atlas_sources.get_texture_index(&my_handle); let handle = TextureAtlas { layout: atlas_layout, index, }; ``` Additionally, `TextureAtlasSources` also has a convenience method, `handle`, which directly combines the index and an existing `TextureAtlasLayout` handle into a new `TextureAtlas`: ```rust let atlas_layout = texture_atlases.add(atlas_layout); let handle = atlas_sources.handle(atlas_layout, &my_handle); ``` ## Extra notes In the future, it might make sense to combine the three types returned by `TextureAtlasBuilder` into their own struct, just so that people don't need to assign variable names to all three parts. In particular, when creating a version that can be loaded directly (like #11873), we could probably use this new type.
		
			
				
	
	
		
			192 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use bevy_asset::{Asset, AssetId, Assets, Handle};
 | |
| use bevy_ecs::{component::Component, reflect::ReflectComponent};
 | |
| use bevy_math::{URect, UVec2};
 | |
| use bevy_reflect::{std_traits::ReflectDefault, Reflect};
 | |
| #[cfg(feature = "serialize")]
 | |
| use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
 | |
| use bevy_render::texture::Image;
 | |
| use bevy_utils::HashMap;
 | |
| 
 | |
| /// Stores a mapping from sub texture handles to the related area index.
 | |
| ///
 | |
| /// Generated by [`TextureAtlasBuilder`].
 | |
| ///
 | |
| /// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
 | |
| #[derive(Debug)]
 | |
| pub struct TextureAtlasSources {
 | |
|     /// Maps from a specific image handle to the index in `textures` where they can be found.
 | |
|     pub(crate) texture_ids: HashMap<AssetId<Image>, usize>,
 | |
| }
 | |
| impl TextureAtlasSources {
 | |
|     /// Retrieves the texture *section* index of the given `texture` handle.
 | |
|     pub fn texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
 | |
|         let id = texture.into();
 | |
|         self.texture_ids.get(&id).cloned()
 | |
|     }
 | |
| 
 | |
|     /// Creates a [`TextureAtlas`] handle for the given `texture` handle.
 | |
|     pub fn handle(
 | |
|         &self,
 | |
|         layout: Handle<TextureAtlasLayout>,
 | |
|         texture: impl Into<AssetId<Image>>,
 | |
|     ) -> Option<TextureAtlas> {
 | |
|         Some(TextureAtlas {
 | |
|             layout,
 | |
|             index: self.texture_index(texture)?,
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     /// Retrieves the texture *section* rectangle of the given `texture` handle.
 | |
|     pub fn texture_rect(
 | |
|         &self,
 | |
|         layout: &TextureAtlasLayout,
 | |
|         texture: impl Into<AssetId<Image>>,
 | |
|     ) -> Option<URect> {
 | |
|         layout.textures.get(self.texture_index(texture)?).cloned()
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Stores a map used to lookup the position of a texture in a [`TextureAtlas`].
 | |
| /// 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.
 | |
| ///
 | |
| /// Optionally it can store a mapping from sub texture handles to the related area index (see
 | |
| /// [`TextureAtlasBuilder`]).
 | |
| ///
 | |
| /// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
 | |
| /// [Example usage animating sprite in response to an event.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
 | |
| /// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
 | |
| ///
 | |
| /// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
 | |
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 | |
| #[derive(Asset, Reflect, PartialEq, Eq, Debug, Clone)]
 | |
| #[reflect(Debug, PartialEq)]
 | |
| #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
 | |
| pub struct TextureAtlasLayout {
 | |
|     /// Total size of texture atlas.
 | |
|     pub size: UVec2,
 | |
|     /// The specific areas of the atlas where each texture can be found
 | |
|     pub textures: Vec<URect>,
 | |
| }
 | |
| 
 | |
| impl TextureAtlasLayout {
 | |
|     /// Create a new empty layout with custom `dimensions`
 | |
|     pub fn new_empty(dimensions: UVec2) -> Self {
 | |
|         Self {
 | |
|             size: dimensions,
 | |
|             textures: Vec::new(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Generate a [`TextureAtlasLayout`] as a grid where each
 | |
|     /// `tile_size` by `tile_size` grid-cell is one of the *section* in the
 | |
|     /// atlas. Grid cells are separated by some `padding`, and the grid starts
 | |
|     /// at `offset` pixels from the top left corner. Resulting layout is
 | |
|     /// indexed left to right, top to bottom.
 | |
|     ///
 | |
|     /// # Arguments
 | |
|     ///
 | |
|     /// * `tile_size` - Each layout grid cell size
 | |
|     /// * `columns` - Grid column count
 | |
|     /// * `rows` - Grid row count
 | |
|     /// * `padding` - Optional padding between cells
 | |
|     /// * `offset` - Optional global grid offset
 | |
|     pub fn from_grid(
 | |
|         tile_size: UVec2,
 | |
|         columns: u32,
 | |
|         rows: u32,
 | |
|         padding: Option<UVec2>,
 | |
|         offset: Option<UVec2>,
 | |
|     ) -> Self {
 | |
|         let padding = padding.unwrap_or_default();
 | |
|         let offset = offset.unwrap_or_default();
 | |
|         let mut sprites = Vec::new();
 | |
|         let mut current_padding = UVec2::ZERO;
 | |
| 
 | |
|         for y in 0..rows {
 | |
|             if y > 0 {
 | |
|                 current_padding.y = padding.y;
 | |
|             }
 | |
|             for x in 0..columns {
 | |
|                 if x > 0 {
 | |
|                     current_padding.x = padding.x;
 | |
|                 }
 | |
| 
 | |
|                 let cell = UVec2::new(x, y);
 | |
|                 let rect_min = (tile_size + current_padding) * cell + offset;
 | |
| 
 | |
|                 sprites.push(URect {
 | |
|                     min: rect_min,
 | |
|                     max: rect_min + tile_size,
 | |
|                 });
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let grid_size = UVec2::new(columns, rows);
 | |
| 
 | |
|         Self {
 | |
|             size: ((tile_size + current_padding) * grid_size) - current_padding,
 | |
|             textures: sprites,
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Add a *section* to the list in the layout and returns its index
 | |
|     /// which can be used with [`TextureAtlas`]
 | |
|     ///
 | |
|     /// # Arguments
 | |
|     ///
 | |
|     /// * `rect` - The section of the texture to be added
 | |
|     ///
 | |
|     /// [`TextureAtlas`]: crate::TextureAtlas
 | |
|     pub fn add_texture(&mut self, rect: URect) -> usize {
 | |
|         self.textures.push(rect);
 | |
|         self.textures.len() - 1
 | |
|     }
 | |
| 
 | |
|     /// The number of textures in the [`TextureAtlasLayout`]
 | |
|     pub fn len(&self) -> usize {
 | |
|         self.textures.len()
 | |
|     }
 | |
| 
 | |
|     pub fn is_empty(&self) -> bool {
 | |
|         self.textures.is_empty()
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Component used to draw a specific section of a texture.
 | |
| ///
 | |
| /// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
 | |
| /// The texture atlas contains various *sections* of a given texture, allowing users to have a single
 | |
| /// image file for either sprite animation or global mapping.
 | |
| /// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
 | |
| /// for efficient rendering of related game objects.
 | |
| ///
 | |
| /// Check the following examples for usage:
 | |
| /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
 | |
| /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
 | |
| /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
 | |
| #[derive(Component, Default, Debug, Clone, Reflect)]
 | |
| #[reflect(Component, Default, Debug)]
 | |
| pub struct TextureAtlas {
 | |
|     /// Texture atlas layout handle
 | |
|     pub layout: Handle<TextureAtlasLayout>,
 | |
|     /// Texture atlas section index
 | |
|     pub index: usize,
 | |
| }
 | |
| 
 | |
| impl TextureAtlas {
 | |
|     /// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
 | |
|     pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
 | |
|         let atlas = texture_atlases.get(&self.layout)?;
 | |
|         atlas.textures.get(self.index).copied()
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl From<Handle<TextureAtlasLayout>> for TextureAtlas {
 | |
|     fn from(texture_atlas: Handle<TextureAtlasLayout>) -> Self {
 | |
|         Self {
 | |
|             layout: texture_atlas,
 | |
|             index: 0,
 | |
|         }
 | |
|     }
 | |
| }
 |