diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 68db98f3d009c..566e07ef58f8c 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -1,11 +1,17 @@ -//! In this example we generate a new texture atlas (sprite sheet) from a folder containing +//! 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::*}; +use bevy::{asset::LoadedFolder, prelude::*, render::texture::ImageSampler}; fn main() { App::new() - .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // prevents blurry sprites + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // fallback to nearest sampling .add_state::() .add_systems(OnEnter(AppState::Setup), load_textures) .add_systems(Update, check_textures.run_if(in_state(AppState::Setup))) @@ -34,6 +40,7 @@ fn check_textures( mut events: EventReader>, ) { // Advance the `AppState` once all sprite handles have been loaded by the `AssetServer` + // and that the the font has been loaded by the `FontSystem`. for event in events.read() { if event.is_loaded_with_dependencies(&rpg_sprite_folder.0) { next_state.set(AppState::Finished); @@ -49,10 +56,145 @@ fn setup( mut texture_atlases: ResMut>, mut textures: ResMut>, ) { - // Build a `TextureAtlas` using the individual sprites - let mut texture_atlas_builder = TextureAtlasBuilder::default(); let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap(); - for handle in loaded_folder.handles.iter() { + + // create texture atlases with different padding and sampling + + let texture_atlas_linear = create_texture_atlas( + loaded_folder, + None, + Some(ImageSampler::linear()), + &mut textures, + ); + let atlas_linear_handle = texture_atlases.add(texture_atlas_linear.clone()); + + let texture_atlas_nearest = 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 = 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 = 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: texture_atlas_linear_padded.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: texture_atlas_linear_padded.texture, + 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: 50.0, + color: Color::WHITE, + }; + + // 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 = asset_server + .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, f32); 4] = [ + ("Linear", atlas_linear_handle, -350.0), + ("Nearest", atlas_nearest_handle, -150.0), + ("Linear", atlas_linear_padded_handle, 150.0), + ("Nearest", atlas_nearest_padded_handle, 350.0), + ]; + + // label text style + let sampling_label_style = TextStyle { + font, + font_size: 30.0, + color: Color::WHITE, + }; + + let base_y = 170.0; // y position of the sprites + + for (sampling, atlas_handle, x) in configurations { + // render a sprite from the texture_atlas + create_sprite_from_atlas(&mut commands, (x, base_y, 0.0), vendor_index, atlas_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, + sampling: Option, + textures: &mut ResMut>, +) -> TextureAtlas { + // Build a `TextureAtlas` using the individual sprites + let mut texture_atlas_builder = + TextureAtlasBuilder::default().padding(padding.unwrap_or_default()); + for handle in folder.handles.iter() { let id = handle.id().typed_unchecked::(); let Some(texture) = textures.get(id) else { warn!( @@ -65,31 +207,47 @@ fn setup( texture_atlas_builder.add_texture(id, texture); } - let texture_atlas = texture_atlas_builder.finish(&mut textures).unwrap(); - let texture_atlas_texture = texture_atlas.texture.clone(); - let vendor_handle = asset_server - .get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png") - .unwrap(); - let vendor_index = texture_atlas.get_texture_index(&vendor_handle).unwrap(); - let atlas_handle = texture_atlases.add(texture_atlas); + let texture_atlas = texture_atlas_builder.finish(textures).unwrap(); - // set up a scene to display our texture atlas - commands.spawn(Camera2dBundle::default()); - // draw a sprite from the atlas + // Update the sampling settings of the texture atlas + let image = textures.get_mut(&texture_atlas.texture).unwrap(); + image.sampler = sampling.unwrap_or_default(); + + texture_atlas +} + +/// 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_handle: Handle, +) { commands.spawn(SpriteSheetBundle { transform: Transform { - translation: Vec3::new(150.0, 0.0, 0.0), - scale: Vec3::splat(4.0), + translation: Vec3::new(translation.0, translation.1, translation.2), + scale: Vec3::splat(3.0), ..default() }, - sprite: TextureAtlasSprite::new(vendor_index), + sprite: TextureAtlasSprite::new(sprite_index), texture_atlas: atlas_handle, ..default() }); - // draw the atlas itself - commands.spawn(SpriteBundle { - texture: texture_atlas_texture, - transform: Transform::from_xyz(-300.0, 0.0, 0.0), +} + +/// 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() }); }