texture_atlas/
texture_atlas.rs

1//! In this example we generate four texture atlases (sprite sheets) from a folder containing
2//! individual sprites.
3//!
4//! The texture atlases are generated with different padding and sampling to demonstrate the
5//! effect of these settings, and how bleeding issues can be resolved by padding the sprites.
6//!
7//! Only one padded and one unpadded texture atlas are rendered to the screen.
8//! An upscaled sprite from each of the four atlases are rendered to the screen.
9
10use bevy::{asset::LoadedFolder, image::ImageSampler, prelude::*};
11
12fn main() {
13    App::new()
14        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // fallback to nearest sampling
15        .init_state::<AppState>()
16        .add_systems(OnEnter(AppState::Setup), load_textures)
17        .add_systems(Update, check_textures.run_if(in_state(AppState::Setup)))
18        .add_systems(OnEnter(AppState::Finished), setup)
19        .run();
20}
21
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, States)]
23enum AppState {
24    #[default]
25    Setup,
26    Finished,
27}
28
29#[derive(Resource, Default)]
30struct RpgSpriteFolder(Handle<LoadedFolder>);
31
32fn load_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
33    // load multiple, individual sprites from a folder
34    commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg")));
35}
36
37fn check_textures(
38    mut next_state: ResMut<NextState<AppState>>,
39    rpg_sprite_folder: Res<RpgSpriteFolder>,
40    mut events: EventReader<AssetEvent<LoadedFolder>>,
41) {
42    // Advance the `AppState` once all sprite handles have been loaded by the `AssetServer`
43    for event in events.read() {
44        if event.is_loaded_with_dependencies(&rpg_sprite_folder.0) {
45            next_state.set(AppState::Finished);
46        }
47    }
48}
49
50fn setup(
51    mut commands: Commands,
52    rpg_sprite_handles: Res<RpgSpriteFolder>,
53    asset_server: Res<AssetServer>,
54    mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
55    loaded_folders: Res<Assets<LoadedFolder>>,
56    mut textures: ResMut<Assets<Image>>,
57) {
58    let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap();
59
60    // create texture atlases with different padding and sampling
61
62    let (texture_atlas_linear, linear_sources, linear_texture) = create_texture_atlas(
63        loaded_folder,
64        None,
65        Some(ImageSampler::linear()),
66        &mut textures,
67    );
68    let atlas_linear_handle = texture_atlases.add(texture_atlas_linear);
69
70    let (texture_atlas_nearest, nearest_sources, nearest_texture) = create_texture_atlas(
71        loaded_folder,
72        None,
73        Some(ImageSampler::nearest()),
74        &mut textures,
75    );
76    let atlas_nearest_handle = texture_atlases.add(texture_atlas_nearest);
77
78    let (texture_atlas_linear_padded, linear_padded_sources, linear_padded_texture) =
79        create_texture_atlas(
80            loaded_folder,
81            Some(UVec2::new(6, 6)),
82            Some(ImageSampler::linear()),
83            &mut textures,
84        );
85    let atlas_linear_padded_handle = texture_atlases.add(texture_atlas_linear_padded.clone());
86
87    let (texture_atlas_nearest_padded, nearest_padded_sources, nearest_padded_texture) =
88        create_texture_atlas(
89            loaded_folder,
90            Some(UVec2::new(6, 6)),
91            Some(ImageSampler::nearest()),
92            &mut textures,
93        );
94    let atlas_nearest_padded_handle = texture_atlases.add(texture_atlas_nearest_padded);
95
96    // setup 2d scene
97    commands.spawn(Camera2d);
98
99    // padded textures are to the right, unpadded to the left
100
101    // draw unpadded texture atlas
102    commands.spawn((
103        Sprite::from_image(linear_texture.clone()),
104        Transform {
105            translation: Vec3::new(-250.0, -130.0, 0.0),
106            scale: Vec3::splat(0.8),
107            ..default()
108        },
109    ));
110
111    // draw padded texture atlas
112    commands.spawn((
113        Sprite::from_image(linear_padded_texture.clone()),
114        Transform {
115            translation: Vec3::new(250.0, -130.0, 0.0),
116            scale: Vec3::splat(0.8),
117            ..default()
118        },
119    ));
120
121    let font = asset_server.load("fonts/FiraSans-Bold.ttf");
122
123    // padding label text style
124    let text_style: TextFont = TextFont {
125        font: font.clone(),
126        font_size: 42.0,
127        ..default()
128    };
129
130    // labels to indicate padding
131
132    // No padding
133    create_label(
134        &mut commands,
135        (-250.0, 330.0, 0.0),
136        "No padding",
137        text_style.clone(),
138    );
139
140    // Padding
141    create_label(&mut commands, (250.0, 330.0, 0.0), "Padding", text_style);
142
143    // get handle to a sprite to render
144    let vendor_handle: Handle<Image> = asset_server
145        .get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png")
146        .unwrap();
147
148    // configuration array to render sprites through iteration
149    let configurations: [(
150        &str,
151        Handle<TextureAtlasLayout>,
152        TextureAtlasSources,
153        Handle<Image>,
154        f32,
155    ); 4] = [
156        (
157            "Linear",
158            atlas_linear_handle,
159            linear_sources,
160            linear_texture,
161            -350.0,
162        ),
163        (
164            "Nearest",
165            atlas_nearest_handle,
166            nearest_sources,
167            nearest_texture,
168            -150.0,
169        ),
170        (
171            "Linear",
172            atlas_linear_padded_handle,
173            linear_padded_sources,
174            linear_padded_texture,
175            150.0,
176        ),
177        (
178            "Nearest",
179            atlas_nearest_padded_handle,
180            nearest_padded_sources,
181            nearest_padded_texture,
182            350.0,
183        ),
184    ];
185
186    // label text style
187    let sampling_label_style = TextFont {
188        font,
189        font_size: 25.0,
190        ..default()
191    };
192
193    let base_y = 170.0; // y position of the sprites
194
195    for (sampling, atlas_handle, atlas_sources, atlas_texture, x) in configurations {
196        // render a sprite from the texture_atlas
197        create_sprite_from_atlas(
198            &mut commands,
199            (x, base_y, 0.0),
200            atlas_texture,
201            atlas_sources,
202            atlas_handle,
203            &vendor_handle,
204        );
205
206        // render a label to indicate the sampling setting
207        create_label(
208            &mut commands,
209            (x, base_y + 110.0, 0.0), // offset to y position of the sprite
210            sampling,
211            sampling_label_style.clone(),
212        );
213    }
214}
215
216/// Create a texture atlas with the given padding and sampling settings
217/// from the individual sprites in the given folder.
218fn create_texture_atlas(
219    folder: &LoadedFolder,
220    padding: Option<UVec2>,
221    sampling: Option<ImageSampler>,
222    textures: &mut ResMut<Assets<Image>>,
223) -> (TextureAtlasLayout, TextureAtlasSources, Handle<Image>) {
224    // Build a texture atlas using the individual sprites
225    let mut texture_atlas_builder = TextureAtlasBuilder::default();
226    texture_atlas_builder.padding(padding.unwrap_or_default());
227    for handle in folder.handles.iter() {
228        let id = handle.id().typed_unchecked::<Image>();
229        let Some(texture) = textures.get(id) else {
230            warn!(
231                "{:?} did not resolve to an `Image` asset.",
232                handle.path().unwrap()
233            );
234            continue;
235        };
236
237        texture_atlas_builder.add_texture(Some(id), texture);
238    }
239
240    let (texture_atlas_layout, texture_atlas_sources, texture) =
241        texture_atlas_builder.build().unwrap();
242    let texture = textures.add(texture);
243
244    // Update the sampling settings of the texture atlas
245    let image = textures.get_mut(&texture).unwrap();
246    image.sampler = sampling.unwrap_or_default();
247
248    (texture_atlas_layout, texture_atlas_sources, texture)
249}
250
251/// Create and spawn a sprite from a texture atlas
252fn create_sprite_from_atlas(
253    commands: &mut Commands,
254    translation: (f32, f32, f32),
255    atlas_texture: Handle<Image>,
256    atlas_sources: TextureAtlasSources,
257    atlas_handle: Handle<TextureAtlasLayout>,
258    vendor_handle: &Handle<Image>,
259) {
260    commands.spawn((
261        Transform {
262            translation: Vec3::new(translation.0, translation.1, translation.2),
263            scale: Vec3::splat(3.0),
264            ..default()
265        },
266        Sprite::from_atlas_image(
267            atlas_texture,
268            atlas_sources.handle(atlas_handle, vendor_handle).unwrap(),
269        ),
270    ));
271}
272
273/// Create and spawn a label (text)
274fn create_label(
275    commands: &mut Commands,
276    translation: (f32, f32, f32),
277    text: &str,
278    text_style: TextFont,
279) {
280    commands.spawn((
281        Text2d::new(text),
282        text_style,
283        TextLayout::new_with_justify(JustifyText::Center),
284        Transform {
285            translation: Vec3::new(translation.0, translation.1, translation.2),
286            ..default()
287        },
288    ));
289}