Split TextureAtlasSources out of TextureAtlasLayout and make TextureAtlasLayout serializable (#15344)

# 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.
This commit is contained in:
Clar Fon 2024-09-30 13:11:56 -04:00 committed by GitHub
parent 4a1645bb8a
commit af9b073b0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 71 deletions

View file

@ -74,17 +74,18 @@ shader_format_glsl = [
shader_format_spirv = ["bevy_render/shader_format_spirv"]
serialize = [
"bevy_color?/serialize",
"bevy_core/serialize",
"bevy_input/serialize",
"bevy_ecs/serialize",
"bevy_time/serialize",
"bevy_window/serialize",
"bevy_winit?/serialize",
"bevy_transform/serialize",
"bevy_input/serialize",
"bevy_math/serialize",
"bevy_scene?/serialize",
"bevy_sprite?/serialize",
"bevy_time/serialize",
"bevy_transform/serialize",
"bevy_ui?/serialize",
"bevy_color?/serialize",
"bevy_window/serialize",
"bevy_winit?/serialize",
]
multi_threaded = [
"bevy_asset?/multi_threaded",

View file

@ -11,6 +11,7 @@ keywords = ["bevy"]
[features]
default = ["bevy_sprite_picking_backend"]
bevy_sprite_picking_backend = ["bevy_picking", "bevy_window"]
serialize = ["dep:serde"]
webgl = []
webgpu = []
@ -41,6 +42,7 @@ rectangle-pack = "0.4"
bitflags = "2.3"
radsort = "0.1"
nonmax = "0.5"
serde = { version = "1", features = ["derive"], optional = true }
[lints]
workspace = true

View file

@ -30,7 +30,7 @@ pub mod prelude {
pub use crate::{
bundle::SpriteBundle,
sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasLayout},
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
};

View file

@ -2,9 +2,50 @@ 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.
///
@ -16,18 +57,15 @@ use bevy_utils::HashMap;
/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
#[derive(Asset, Reflect, Debug, Clone)]
#[reflect(Debug)]
#[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>,
/// Maps from a specific image handle to the index in `textures` where they can be found.
///
/// This field is set by [`TextureAtlasBuilder`].
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
pub(crate) texture_handles: Option<HashMap<AssetId<Image>, usize>>,
}
impl TextureAtlasLayout {
@ -35,7 +73,6 @@ impl TextureAtlasLayout {
pub fn new_empty(dimensions: UVec2) -> Self {
Self {
size: dimensions,
texture_handles: None,
textures: Vec::new(),
}
}
@ -89,7 +126,6 @@ impl TextureAtlasLayout {
Self {
size: ((tile_size + current_padding) * grid_size) - current_padding,
textures: sprites,
texture_handles: None,
}
}
@ -114,18 +150,6 @@ impl TextureAtlasLayout {
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
/// Retrieves the texture *section* index of the given `texture` handle.
///
/// This requires the layout to have been built using a [`TextureAtlasBuilder`]
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
pub fn get_texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
let id = texture.into();
self.texture_handles
.as_ref()
.and_then(|texture_handles| texture_handles.get(&id).cloned())
}
}
/// Component used to draw a specific section of a texture.

View file

@ -15,7 +15,7 @@ use rectangle_pack::{
};
use thiserror::Error;
use crate::TextureAtlasLayout;
use crate::{TextureAtlasLayout, TextureAtlasSources};
#[derive(Debug, Error)]
pub enum TextureAtlasBuilderError {
@ -159,7 +159,9 @@ impl<'a> TextureAtlasBuilder<'a> {
since = "0.14.0",
note = "TextureAtlasBuilder::finish() was not idiomatic. Use TextureAtlasBuilder::build() instead."
)]
pub fn finish(&mut self) -> Result<(TextureAtlasLayout, Image), TextureAtlasBuilderError> {
pub fn finish(
&mut self,
) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> {
self.build()
}
@ -184,7 +186,7 @@ impl<'a> TextureAtlasBuilder<'a> {
/// // Customize it
/// // ...
/// // Build your texture and the atlas layout
/// let (atlas_layout, texture) = builder.build().unwrap();
/// let (atlas_layout, atlas_sources, texture) = builder.build().unwrap();
/// let texture = textures.add(texture);
/// let layout = layouts.add(atlas_layout);
/// // Spawn your sprite
@ -199,7 +201,9 @@ impl<'a> TextureAtlasBuilder<'a> {
///
/// If there is not enough space in the atlas texture, an error will
/// be returned. It is then recommended to make a larger sprite sheet.
pub fn build(&mut self) -> Result<(TextureAtlasLayout, Image), TextureAtlasBuilderError> {
pub fn build(
&mut self,
) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> {
let max_width = self.max_size.x;
let max_height = self.max_size.y;
@ -295,8 +299,8 @@ impl<'a> TextureAtlasBuilder<'a> {
TextureAtlasLayout {
size: atlas_texture.size(),
textures: texture_rects,
texture_handles: Some(texture_ids),
},
TextureAtlasSources { texture_ids },
atlas_texture,
))
}

View file

@ -59,15 +59,15 @@ fn setup(
// create texture atlases with different padding and sampling
let (texture_atlas_linear, linear_texture) = create_texture_atlas(
let (texture_atlas_linear, linear_sources, linear_texture) = create_texture_atlas(
loaded_folder,
None,
Some(ImageSampler::linear()),
&mut textures,
);
let atlas_linear_handle = texture_atlases.add(texture_atlas_linear.clone());
let atlas_linear_handle = texture_atlases.add(texture_atlas_linear);
let (texture_atlas_nearest, nearest_texture) = create_texture_atlas(
let (texture_atlas_nearest, nearest_sources, nearest_texture) = create_texture_atlas(
loaded_folder,
None,
Some(ImageSampler::nearest()),
@ -75,7 +75,8 @@ fn setup(
);
let atlas_nearest_handle = texture_atlases.add(texture_atlas_nearest);
let (texture_atlas_linear_padded, linear_padded_texture) = create_texture_atlas(
let (texture_atlas_linear_padded, linear_padded_sources, linear_padded_texture) =
create_texture_atlas(
loaded_folder,
Some(UVec2::new(6, 6)),
Some(ImageSampler::linear()),
@ -83,7 +84,8 @@ fn setup(
);
let atlas_linear_padded_handle = texture_atlases.add(texture_atlas_linear_padded.clone());
let (texture_atlas_nearest_padded, nearest_padded_texture) = create_texture_atlas(
let (texture_atlas_nearest_padded, nearest_padded_sources, nearest_padded_texture) =
create_texture_atlas(
loaded_folder,
Some(UVec2::new(6, 6)),
Some(ImageSampler::nearest()),
@ -145,25 +147,39 @@ fn setup(
.get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png")
.unwrap();
// get index of the sprite in the texture atlas, this is used to render the sprite
// the index is the same for all the texture atlases, since they are created from the same folder
let vendor_index = texture_atlas_linear
.get_texture_index(&vendor_handle)
.unwrap();
// configuration array to render sprites through iteration
let configurations: [(&str, Handle<TextureAtlasLayout>, Handle<Image>, f32); 4] = [
("Linear", atlas_linear_handle, linear_texture, -350.0),
("Nearest", atlas_nearest_handle, nearest_texture, -150.0),
let configurations: [(
&str,
Handle<TextureAtlasLayout>,
TextureAtlasSources,
Handle<Image>,
f32,
); 4] = [
(
"Linear",
atlas_linear_handle,
linear_sources,
linear_texture,
-350.0,
),
(
"Nearest",
atlas_nearest_handle,
nearest_sources,
nearest_texture,
-150.0,
),
(
"Linear",
atlas_linear_padded_handle,
linear_padded_sources,
linear_padded_texture,
150.0,
),
(
"Nearest",
atlas_nearest_padded_handle,
nearest_padded_sources,
nearest_padded_texture,
350.0,
),
@ -178,14 +194,15 @@ fn setup(
let base_y = 170.0; // y position of the sprites
for (sampling, atlas_handle, image_handle, x) in configurations {
for (sampling, atlas_handle, atlas_sources, atlas_texture, x) in configurations {
// render a sprite from the texture_atlas
create_sprite_from_atlas(
&mut commands,
(x, base_y, 0.0),
vendor_index,
atlas_texture,
atlas_sources,
atlas_handle,
image_handle,
&vendor_handle,
);
// render a label to indicate the sampling setting
@ -205,7 +222,7 @@ fn create_texture_atlas(
padding: Option<UVec2>,
sampling: Option<ImageSampler>,
textures: &mut ResMut<Assets<Image>>,
) -> (TextureAtlasLayout, Handle<Image>) {
) -> (TextureAtlasLayout, TextureAtlasSources, Handle<Image>) {
// Build a texture atlas using the individual sprites
let mut texture_atlas_builder = TextureAtlasBuilder::default();
texture_atlas_builder.padding(padding.unwrap_or_default());
@ -222,23 +239,25 @@ fn create_texture_atlas(
texture_atlas_builder.add_texture(Some(id), texture);
}
let (texture_atlas_layout, texture) = texture_atlas_builder.build().unwrap();
let (texture_atlas_layout, texture_atlas_sources, texture) =
texture_atlas_builder.build().unwrap();
let texture = textures.add(texture);
// Update the sampling settings of the texture atlas
let image = textures.get_mut(&texture).unwrap();
image.sampler = sampling.unwrap_or_default();
(texture_atlas_layout, texture)
(texture_atlas_layout, texture_atlas_sources, texture)
}
/// Create and spawn a sprite from a texture atlas
fn create_sprite_from_atlas(
commands: &mut Commands,
translation: (f32, f32, f32),
sprite_index: usize,
atlas_texture: Handle<Image>,
atlas_sources: TextureAtlasSources,
atlas_handle: Handle<TextureAtlasLayout>,
texture: Handle<Image>,
vendor_handle: &Handle<Image>,
) {
commands.spawn((
SpriteBundle {
@ -247,13 +266,10 @@ fn create_sprite_from_atlas(
scale: Vec3::splat(3.0),
..default()
},
texture,
texture: atlas_texture,
..default()
},
TextureAtlas {
layout: atlas_handle,
index: sprite_index,
},
atlas_sources.handle(atlas_handle, vendor_handle).unwrap(),
));
}