mirror of
https://github.com/bevyengine/bevy
synced 2024-11-29 08:00:20 +00:00
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.
291 lines
8.8 KiB
Rust
291 lines
8.8 KiB
Rust
//! In this example we generate four texture atlases (sprite sheets) from a folder containing
|
|
//! individual sprites.
|
|
//!
|
|
//! The texture atlases are generated with different padding and sampling to demonstrate the
|
|
//! effect of these settings, and how bleeding issues can be resolved by padding the sprites.
|
|
//!
|
|
//! Only one padded and one unpadded texture atlas are rendered to the screen.
|
|
//! An upscaled sprite from each of the four atlases are rendered to the screen.
|
|
|
|
use bevy::{asset::LoadedFolder, prelude::*, render::texture::ImageSampler};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // fallback to nearest sampling
|
|
.init_state::<AppState>()
|
|
.add_systems(OnEnter(AppState::Setup), load_textures)
|
|
.add_systems(Update, check_textures.run_if(in_state(AppState::Setup)))
|
|
.add_systems(OnEnter(AppState::Finished), setup)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, States)]
|
|
enum AppState {
|
|
#[default]
|
|
Setup,
|
|
Finished,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct RpgSpriteFolder(Handle<LoadedFolder>);
|
|
|
|
fn load_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
// load multiple, individual sprites from a folder
|
|
commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg")));
|
|
}
|
|
|
|
fn check_textures(
|
|
mut next_state: ResMut<NextState<AppState>>,
|
|
rpg_sprite_folder: Res<RpgSpriteFolder>,
|
|
mut events: EventReader<AssetEvent<LoadedFolder>>,
|
|
) {
|
|
// Advance the `AppState` once all sprite handles have been loaded by the `AssetServer`
|
|
for event in events.read() {
|
|
if event.is_loaded_with_dependencies(&rpg_sprite_folder.0) {
|
|
next_state.set(AppState::Finished);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
rpg_sprite_handles: Res<RpgSpriteFolder>,
|
|
asset_server: Res<AssetServer>,
|
|
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
|
|
loaded_folders: Res<Assets<LoadedFolder>>,
|
|
mut textures: ResMut<Assets<Image>>,
|
|
) {
|
|
let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap();
|
|
|
|
// create texture atlases with different padding and sampling
|
|
|
|
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);
|
|
|
|
let (texture_atlas_nearest, nearest_sources, nearest_texture) = create_texture_atlas(
|
|
loaded_folder,
|
|
None,
|
|
Some(ImageSampler::nearest()),
|
|
&mut textures,
|
|
);
|
|
let atlas_nearest_handle = texture_atlases.add(texture_atlas_nearest);
|
|
|
|
let (texture_atlas_linear_padded, linear_padded_sources, linear_padded_texture) =
|
|
create_texture_atlas(
|
|
loaded_folder,
|
|
Some(UVec2::new(6, 6)),
|
|
Some(ImageSampler::linear()),
|
|
&mut textures,
|
|
);
|
|
let atlas_linear_padded_handle = texture_atlases.add(texture_atlas_linear_padded.clone());
|
|
|
|
let (texture_atlas_nearest_padded, nearest_padded_sources, nearest_padded_texture) =
|
|
create_texture_atlas(
|
|
loaded_folder,
|
|
Some(UVec2::new(6, 6)),
|
|
Some(ImageSampler::nearest()),
|
|
&mut textures,
|
|
);
|
|
let atlas_nearest_padded_handle = texture_atlases.add(texture_atlas_nearest_padded);
|
|
|
|
// setup 2d scene
|
|
commands.spawn(Camera2dBundle::default());
|
|
|
|
// padded textures are to the right, unpadded to the left
|
|
|
|
// draw unpadded texture atlas
|
|
commands.spawn(SpriteBundle {
|
|
texture: linear_texture.clone(),
|
|
transform: Transform {
|
|
translation: Vec3::new(-250.0, -130.0, 0.0),
|
|
scale: Vec3::splat(0.8),
|
|
..default()
|
|
},
|
|
..default()
|
|
});
|
|
|
|
// draw padded texture atlas
|
|
commands.spawn(SpriteBundle {
|
|
texture: linear_padded_texture.clone(),
|
|
transform: Transform {
|
|
translation: Vec3::new(250.0, -130.0, 0.0),
|
|
scale: Vec3::splat(0.8),
|
|
..default()
|
|
},
|
|
..default()
|
|
});
|
|
|
|
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
|
|
|
|
// padding label text style
|
|
let text_style: TextStyle = TextStyle {
|
|
font: font.clone(),
|
|
font_size: 42.0,
|
|
..default()
|
|
};
|
|
|
|
// labels to indicate padding
|
|
|
|
// No padding
|
|
create_label(
|
|
&mut commands,
|
|
(-250.0, 330.0, 0.0),
|
|
"No padding",
|
|
text_style.clone(),
|
|
);
|
|
|
|
// Padding
|
|
create_label(&mut commands, (250.0, 330.0, 0.0), "Padding", text_style);
|
|
|
|
// get handle to a sprite to render
|
|
let vendor_handle: Handle<Image> = asset_server
|
|
.get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png")
|
|
.unwrap();
|
|
|
|
// configuration array to render sprites through iteration
|
|
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,
|
|
),
|
|
];
|
|
|
|
// label text style
|
|
let sampling_label_style = TextStyle {
|
|
font,
|
|
font_size: 25.0,
|
|
..default()
|
|
};
|
|
|
|
let base_y = 170.0; // y position of the sprites
|
|
|
|
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),
|
|
atlas_texture,
|
|
atlas_sources,
|
|
atlas_handle,
|
|
&vendor_handle,
|
|
);
|
|
|
|
// render a label to indicate the sampling setting
|
|
create_label(
|
|
&mut commands,
|
|
(x, base_y + 110.0, 0.0), // offset to y position of the sprite
|
|
sampling,
|
|
sampling_label_style.clone(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Create a texture atlas with the given padding and sampling settings
|
|
/// from the individual sprites in the given folder.
|
|
fn create_texture_atlas(
|
|
folder: &LoadedFolder,
|
|
padding: Option<UVec2>,
|
|
sampling: Option<ImageSampler>,
|
|
textures: &mut ResMut<Assets<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());
|
|
for handle in folder.handles.iter() {
|
|
let id = handle.id().typed_unchecked::<Image>();
|
|
let Some(texture) = textures.get(id) else {
|
|
warn!(
|
|
"{:?} did not resolve to an `Image` asset.",
|
|
handle.path().unwrap()
|
|
);
|
|
continue;
|
|
};
|
|
|
|
texture_atlas_builder.add_texture(Some(id), texture);
|
|
}
|
|
|
|
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_atlas_sources, texture)
|
|
}
|
|
|
|
/// Create and spawn a sprite from a texture atlas
|
|
fn create_sprite_from_atlas(
|
|
commands: &mut Commands,
|
|
translation: (f32, f32, f32),
|
|
atlas_texture: Handle<Image>,
|
|
atlas_sources: TextureAtlasSources,
|
|
atlas_handle: Handle<TextureAtlasLayout>,
|
|
vendor_handle: &Handle<Image>,
|
|
) {
|
|
commands.spawn((
|
|
SpriteBundle {
|
|
transform: Transform {
|
|
translation: Vec3::new(translation.0, translation.1, translation.2),
|
|
scale: Vec3::splat(3.0),
|
|
..default()
|
|
},
|
|
texture: atlas_texture,
|
|
..default()
|
|
},
|
|
atlas_sources.handle(atlas_handle, vendor_handle).unwrap(),
|
|
));
|
|
}
|
|
|
|
/// Create and spawn a label (text)
|
|
fn create_label(
|
|
commands: &mut Commands,
|
|
translation: (f32, f32, f32),
|
|
text: &str,
|
|
text_style: TextStyle,
|
|
) {
|
|
commands.spawn(Text2dBundle {
|
|
text: Text::from_section(text, text_style).with_justify(JustifyText::Center),
|
|
transform: Transform {
|
|
translation: Vec3::new(translation.0, translation.1, translation.2),
|
|
..default()
|
|
},
|
|
..default()
|
|
});
|
|
}
|