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: MessageReader<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    commands.spawn(Camera2d);
97
98    // Padded textures are to the right, unpadded to the left
99
100    // Draw unpadded texture atlas
101    commands.spawn((
102        Sprite::from_image(linear_texture.clone()),
103        Transform {
104            translation: Vec3::new(-250.0, -160.0, 0.0),
105            scale: Vec3::splat(0.5),
106            ..default()
107        },
108    ));
109
110    // Draw padded texture atlas
111    commands.spawn((
112        Sprite::from_image(linear_padded_texture.clone()),
113        Transform {
114            translation: Vec3::new(250.0, -160.0, 0.0),
115            scale: Vec3::splat(0.5),
116            ..default()
117        },
118    ));
119
120    let font = asset_server.load("fonts/FiraSans-Bold.ttf");
121
122    // Padding label text style
123    let text_style: TextFont = TextFont {
124        font: font.clone(),
125        font_size: 42.0,
126        ..default()
127    };
128
129    // Labels to indicate padding
130
131    // No padding
132    create_label(
133        &mut commands,
134        (-250.0, 250.0, 0.0),
135        "No padding",
136        text_style.clone(),
137    );
138
139    // Padding
140    create_label(&mut commands, (250.0, 250.0, 0.0), "Padding", text_style);
141
142    // Get handle to a sprite to render
143    let vendor_handle: Handle<Image> = asset_server
144        .get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png")
145        .unwrap();
146
147    // Configuration array to render sprites through iteration
148    let configurations: [(
149        &str,
150        Handle<TextureAtlasLayout>,
151        TextureAtlasSources,
152        Handle<Image>,
153        f32,
154    ); 4] = [
155        (
156            "Linear",
157            atlas_linear_handle,
158            linear_sources,
159            linear_texture,
160            -350.0,
161        ),
162        (
163            "Nearest",
164            atlas_nearest_handle,
165            nearest_sources,
166            nearest_texture,
167            -150.0,
168        ),
169        (
170            "Linear",
171            atlas_linear_padded_handle,
172            linear_padded_sources,
173            linear_padded_texture,
174            150.0,
175        ),
176        (
177            "Nearest",
178            atlas_nearest_padded_handle,
179            nearest_padded_sources,
180            nearest_padded_texture,
181            350.0,
182        ),
183    ];
184
185    // Label text style
186    let sampling_label_style = TextFont {
187        font,
188        font_size: 25.0,
189        ..default()
190    };
191
192    let base_y = 80.0; // y position of the sprites
193
194    for (sampling, atlas_handle, atlas_sources, atlas_texture, x) in configurations {
195        // Render a sprite from the texture_atlas
196        create_sprite_from_atlas(
197            &mut commands,
198            (x, base_y, 0.0),
199            atlas_texture,
200            atlas_sources,
201            atlas_handle,
202            &vendor_handle,
203        );
204
205        // Render a label to indicate the sampling setting
206        create_label(
207            &mut commands,
208            (x, base_y + 110.0, 0.0), // Offset to y position of the sprite
209            sampling,
210            sampling_label_style.clone(),
211        );
212    }
213}
214
215/// Create a texture atlas with the given padding and sampling settings
216/// from the individual sprites in the given folder.
217fn create_texture_atlas(
218    folder: &LoadedFolder,
219    padding: Option<UVec2>,
220    sampling: Option<ImageSampler>,
221    textures: &mut ResMut<Assets<Image>>,
222) -> (TextureAtlasLayout, TextureAtlasSources, Handle<Image>) {
223    // Build a texture atlas using the individual sprites
224    let mut texture_atlas_builder = TextureAtlasBuilder::default();
225    texture_atlas_builder.padding(padding.unwrap_or_default());
226    for handle in folder.handles.iter() {
227        let id = handle.id().typed_unchecked::<Image>();
228        let Some(texture) = textures.get(id) else {
229            warn!(
230                "{} did not resolve to an `Image` asset.",
231                handle.path().unwrap()
232            );
233            continue;
234        };
235
236        texture_atlas_builder.add_texture(Some(id), texture);
237    }
238
239    let (texture_atlas_layout, texture_atlas_sources, texture) =
240        texture_atlas_builder.build().unwrap();
241    let texture = textures.add(texture);
242
243    // Update the sampling settings of the texture atlas
244    let image = textures.get_mut(&texture).unwrap();
245    image.sampler = sampling.unwrap_or_default();
246
247    (texture_atlas_layout, texture_atlas_sources, texture)
248}
249
250/// Create and spawn a sprite from a texture atlas
251fn create_sprite_from_atlas(
252    commands: &mut Commands,
253    translation: (f32, f32, f32),
254    atlas_texture: Handle<Image>,
255    atlas_sources: TextureAtlasSources,
256    atlas_handle: Handle<TextureAtlasLayout>,
257    vendor_handle: &Handle<Image>,
258) {
259    commands.spawn((
260        Transform {
261            translation: Vec3::new(translation.0, translation.1, translation.2),
262            scale: Vec3::splat(3.0),
263            ..default()
264        },
265        Sprite::from_atlas_image(
266            atlas_texture,
267            atlas_sources.handle(atlas_handle, vendor_handle).unwrap(),
268        ),
269    ));
270}
271
272/// Create and spawn a label (text)
273fn create_label(
274    commands: &mut Commands,
275    translation: (f32, f32, f32),
276    text: &str,
277    text_style: TextFont,
278) {
279    commands.spawn((
280        Text2d::new(text),
281        text_style,
282        TextLayout::new_with_justify(Justify::Center),
283        Transform {
284            translation: Vec3::new(translation.0, translation.1, translation.2),
285            ..default()
286        },
287    ));
288}