Skip to content

Commit 64103ec

Browse files
ManevilleFalice-i-cecilepablo-lua
authored andcommittedJun 11, 2024
Slicing support for texture atlas (bevyengine#12059)
Follow up to bevyengine#11600 and bevyengine#10588 bevyengine#11944 made clear that some people want to use slicing with texture atlases * Added support for `TextureAtlas` slicing and tiling. `SpriteSheetBundle` and `AtlasImageBundle` can now use `ImageScaleMode` * Added new `ui_texture_atlas_slice` example using a texture sheet <img width="798" alt="Screenshot 2024-02-23 at 11 58 35" src="https://github.com/bevyengine/bevy/assets/26703856/47a8b764-127c-4a06-893f-181703777501"> --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Pablo Reinhardt <[email protected]>
1 parent aa80e2d commit 64103ec

File tree

10 files changed

+283
-46
lines changed

10 files changed

+283
-46
lines changed
 

Diff for: ‎Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -2497,6 +2497,17 @@ description = "Illustrates how to use 9 Slicing in UI"
24972497
category = "UI (User Interface)"
24982498
wasm = true
24992499

2500+
[[example]]
2501+
name = "ui_texture_atlas_slice"
2502+
path = "examples/ui/ui_texture_atlas_slice.rs"
2503+
doc-scrape-examples = true
2504+
2505+
[package.metadata.example.ui_texture_atlas_slice]
2506+
name = "UI Texture Atlas Slice"
2507+
description = "Illustrates how to use 9 Slicing for TextureAtlases in UI"
2508+
category = "UI (User Interface)"
2509+
wasm = true
2510+
25002511
[[example]]
25012512
name = "viewport_debug"
25022513
path = "examples/ui/viewport_debug.rs"

Diff for: ‎assets/textures/fantasy_ui_borders/border_sheet.png

10.3 KB
Loading

Diff for: ‎crates/bevy_sprite/src/sprite.rs

-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ pub struct Sprite {
3232
}
3333

3434
/// Controls how the image is altered when scaled.
35-
///
36-
/// Note: This is not yet compatible with texture atlases
3735
#[derive(Component, Debug, Clone, Reflect)]
3836
#[reflect(Component)]
3937
pub enum ImageScaleMode {

Diff for: ‎crates/bevy_sprite/src/texture_slice/computed_slices.rs

+71-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{ExtractedSprite, ImageScaleMode, Sprite};
1+
use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlas, TextureAtlasLayout};
22

33
use super::TextureSlice;
44
use bevy_asset::{AssetEvent, Assets, Handle};
@@ -63,37 +63,55 @@ impl ComputedTextureSlices {
6363
/// will be computed according to the `image_handle` dimensions or the sprite rect.
6464
///
6565
/// Returns `None` if the image asset is not loaded
66+
///
67+
/// # Arguments
68+
///
69+
/// * `sprite` - The sprite component, will be used to find the draw area size
70+
/// * `scale_mode` - The image scaling component
71+
/// * `image_handle` - The texture to slice or tile
72+
/// * `images` - The image assets, use to retrieve the image dimensions
73+
/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section
74+
/// of the texture
75+
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
6676
#[must_use]
6777
fn compute_sprite_slices(
6878
sprite: &Sprite,
6979
scale_mode: &ImageScaleMode,
7080
image_handle: &Handle<Image>,
7181
images: &Assets<Image>,
82+
atlas: Option<&TextureAtlas>,
83+
atlas_layouts: &Assets<TextureAtlasLayout>,
7284
) -> Option<ComputedTextureSlices> {
73-
let image_size = images.get(image_handle).map(|i| {
74-
Vec2::new(
75-
i.texture_descriptor.size.width as f32,
76-
i.texture_descriptor.size.height as f32,
77-
)
78-
})?;
79-
let slices = match scale_mode {
80-
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
81-
sprite.rect.unwrap_or(Rect {
85+
let (image_size, texture_rect) = match atlas {
86+
Some(a) => {
87+
let layout = atlas_layouts.get(&a.layout)?;
88+
(
89+
layout.size.as_vec2(),
90+
layout.textures.get(a.index)?.as_rect(),
91+
)
92+
}
93+
None => {
94+
let image = images.get(image_handle)?;
95+
let size = Vec2::new(
96+
image.texture_descriptor.size.width as f32,
97+
image.texture_descriptor.size.height as f32,
98+
);
99+
let rect = sprite.rect.unwrap_or(Rect {
82100
min: Vec2::ZERO,
83-
max: image_size,
84-
}),
85-
sprite.custom_size,
86-
),
101+
max: size,
102+
});
103+
(size, rect)
104+
}
105+
};
106+
let slices = match scale_mode {
107+
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
87108
ImageScaleMode::Tiled {
88109
tile_x,
89110
tile_y,
90111
stretch_value,
91112
} => {
92113
let slice = TextureSlice {
93-
texture_rect: sprite.rect.unwrap_or(Rect {
94-
min: Vec2::ZERO,
95-
max: image_size,
96-
}),
114+
texture_rect,
97115
draw_size: sprite.custom_size.unwrap_or(image_size),
98116
offset: Vec2::ZERO,
99117
};
@@ -109,7 +127,14 @@ pub(crate) fn compute_slices_on_asset_event(
109127
mut commands: Commands,
110128
mut events: EventReader<AssetEvent<Image>>,
111129
images: Res<Assets<Image>>,
112-
sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle<Image>)>,
130+
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
131+
sprites: Query<(
132+
Entity,
133+
&ImageScaleMode,
134+
&Sprite,
135+
&Handle<Image>,
136+
Option<&TextureAtlas>,
137+
)>,
113138
) {
114139
// We store the asset ids of added/modified image assets
115140
let added_handles: HashSet<_> = events
@@ -123,11 +148,18 @@ pub(crate) fn compute_slices_on_asset_event(
123148
return;
124149
}
125150
// We recompute the sprite slices for sprite entities with a matching asset handle id
126-
for (entity, scale_mode, sprite, image_handle) in &sprites {
151+
for (entity, scale_mode, sprite, image_handle, atlas) in &sprites {
127152
if !added_handles.contains(&image_handle.id()) {
128153
continue;
129154
}
130-
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) {
155+
if let Some(slices) = compute_sprite_slices(
156+
sprite,
157+
scale_mode,
158+
image_handle,
159+
&images,
160+
atlas,
161+
&atlas_layouts,
162+
) {
131163
commands.entity(entity).insert(slices);
132164
}
133165
}
@@ -138,17 +170,32 @@ pub(crate) fn compute_slices_on_asset_event(
138170
pub(crate) fn compute_slices_on_sprite_change(
139171
mut commands: Commands,
140172
images: Res<Assets<Image>>,
173+
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
141174
changed_sprites: Query<
142-
(Entity, &ImageScaleMode, &Sprite, &Handle<Image>),
175+
(
176+
Entity,
177+
&ImageScaleMode,
178+
&Sprite,
179+
&Handle<Image>,
180+
Option<&TextureAtlas>,
181+
),
143182
Or<(
144183
Changed<ImageScaleMode>,
145184
Changed<Handle<Image>>,
146185
Changed<Sprite>,
186+
Changed<TextureAtlas>,
147187
)>,
148188
>,
149189
) {
150-
for (entity, scale_mode, sprite, image_handle) in &changed_sprites {
151-
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) {
190+
for (entity, scale_mode, sprite, image_handle, atlas) in &changed_sprites {
191+
if let Some(slices) = compute_sprite_slices(
192+
sprite,
193+
scale_mode,
194+
image_handle,
195+
&images,
196+
atlas,
197+
&atlas_layouts,
198+
) {
152199
commands.entity(entity).insert(slices);
153200
}
154201
}

Diff for: ‎crates/bevy_sprite/src/texture_slice/slicer.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl TextureSlicer {
7272
TextureSlice {
7373
texture_rect: Rect {
7474
min: vec2(base_rect.max.x - right, base_rect.min.y),
75-
max: vec2(base_rect.max.x, top),
75+
max: vec2(base_rect.max.x, base_rect.min.y + top),
7676
},
7777
draw_size: vec2(right, top) * min_coef,
7878
offset: vec2(
@@ -198,6 +198,9 @@ impl TextureSlicer {
198198
///
199199
/// * `rect` - The section of the texture to slice in 9 parts
200200
/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.
201+
//
202+
// TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`)
203+
//
201204
#[must_use]
202205
pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {
203206
let render_size = render_size.unwrap_or_else(|| rect.size());

Diff for: ‎crates/bevy_ui/src/node_bundles.rs

+6
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ pub struct ImageBundle {
123123

124124
/// A UI node that is a texture atlas sprite
125125
///
126+
/// # Extra behaviours
127+
///
128+
/// You may add the following components to enable additional behaviours
129+
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
130+
///
126131
/// This bundle is identical to [`ImageBundle`] with an additional [`TextureAtlas`] component.
127132
#[derive(Bundle, Debug, Default)]
128133
pub struct AtlasImageBundle {
@@ -296,6 +301,7 @@ where
296301
///
297302
/// You may add the following components to enable additional behaviours
298303
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
304+
/// - [`TextureAtlas`] to draw specific section of the texture
299305
#[derive(Bundle, Clone, Debug)]
300306
pub struct ButtonBundle {
301307
/// Describes the logical size of the node

Diff for: ‎crates/bevy_ui/src/texture_slice.rs

+74-18
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bevy_asset::{AssetEvent, Assets};
66
use bevy_ecs::prelude::*;
77
use bevy_math::{Rect, Vec2};
88
use bevy_render::texture::Image;
9-
use bevy_sprite::{ImageScaleMode, TextureSlice};
9+
use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice};
1010
use bevy_transform::prelude::*;
1111
use bevy_utils::HashSet;
1212

@@ -75,25 +75,48 @@ impl ComputedTextureSlices {
7575
}
7676

7777
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
78-
/// will be computed according to the `image_handle` dimensions or the sprite rect.
78+
/// will be computed according to the `image_handle` dimensions.
7979
///
8080
/// Returns `None` if the image asset is not loaded
81+
///
82+
/// # Arguments
83+
///
84+
/// * `draw_area` - The size of the drawing area the slices will have to fit into
85+
/// * `scale_mode` - The image scaling component
86+
/// * `image_handle` - The texture to slice or tile
87+
/// * `images` - The image assets, use to retrieve the image dimensions
88+
/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section
89+
/// of the texture
90+
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
8191
#[must_use]
8292
fn compute_texture_slices(
8393
draw_area: Vec2,
8494
scale_mode: &ImageScaleMode,
8595
image_handle: &UiImage,
8696
images: &Assets<Image>,
97+
atlas: Option<&TextureAtlas>,
98+
atlas_layouts: &Assets<TextureAtlasLayout>,
8799
) -> Option<ComputedTextureSlices> {
88-
let image_size = images.get(&image_handle.texture).map(|i| {
89-
Vec2::new(
90-
i.texture_descriptor.size.width as f32,
91-
i.texture_descriptor.size.height as f32,
92-
)
93-
})?;
94-
let texture_rect = Rect {
95-
min: Vec2::ZERO,
96-
max: image_size,
100+
let (image_size, texture_rect) = match atlas {
101+
Some(a) => {
102+
let layout = atlas_layouts.get(&a.layout)?;
103+
(
104+
layout.size.as_vec2(),
105+
layout.textures.get(a.index)?.as_rect(),
106+
)
107+
}
108+
None => {
109+
let image = images.get(&image_handle.texture)?;
110+
let size = Vec2::new(
111+
image.texture_descriptor.size.width as f32,
112+
image.texture_descriptor.size.height as f32,
113+
);
114+
let rect = Rect {
115+
min: Vec2::ZERO,
116+
max: size,
117+
};
118+
(size, rect)
119+
}
97120
};
98121
let slices = match scale_mode {
99122
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
@@ -119,7 +142,14 @@ pub(crate) fn compute_slices_on_asset_event(
119142
mut commands: Commands,
120143
mut events: EventReader<AssetEvent<Image>>,
121144
images: Res<Assets<Image>>,
122-
ui_nodes: Query<(Entity, &ImageScaleMode, &Node, &UiImage)>,
145+
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
146+
ui_nodes: Query<(
147+
Entity,
148+
&ImageScaleMode,
149+
&Node,
150+
&UiImage,
151+
Option<&TextureAtlas>,
152+
)>,
123153
) {
124154
// We store the asset ids of added/modified image assets
125155
let added_handles: HashSet<_> = events
@@ -133,11 +163,18 @@ pub(crate) fn compute_slices_on_asset_event(
133163
return;
134164
}
135165
// We recompute the sprite slices for sprite entities with a matching asset handle id
136-
for (entity, scale_mode, ui_node, image) in &ui_nodes {
166+
for (entity, scale_mode, ui_node, image, atlas) in &ui_nodes {
137167
if !added_handles.contains(&image.texture.id()) {
138168
continue;
139169
}
140-
if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) {
170+
if let Some(slices) = compute_texture_slices(
171+
ui_node.size(),
172+
scale_mode,
173+
image,
174+
&images,
175+
atlas,
176+
&atlas_layouts,
177+
) {
141178
commands.entity(entity).insert(slices);
142179
}
143180
}
@@ -148,13 +185,32 @@ pub(crate) fn compute_slices_on_asset_event(
148185
pub(crate) fn compute_slices_on_image_change(
149186
mut commands: Commands,
150187
images: Res<Assets<Image>>,
188+
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
151189
changed_nodes: Query<
152-
(Entity, &ImageScaleMode, &Node, &UiImage),
153-
Or<(Changed<ImageScaleMode>, Changed<UiImage>, Changed<Node>)>,
190+
(
191+
Entity,
192+
&ImageScaleMode,
193+
&Node,
194+
&UiImage,
195+
Option<&TextureAtlas>,
196+
),
197+
Or<(
198+
Changed<ImageScaleMode>,
199+
Changed<UiImage>,
200+
Changed<Node>,
201+
Changed<TextureAtlas>,
202+
)>,
154203
>,
155204
) {
156-
for (entity, scale_mode, ui_node, image) in &changed_nodes {
157-
if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) {
205+
for (entity, scale_mode, ui_node, image, atlas) in &changed_nodes {
206+
if let Some(slices) = compute_texture_slices(
207+
ui_node.size(),
208+
scale_mode,
209+
image,
210+
&images,
211+
atlas,
212+
&atlas_layouts,
213+
) {
158214
commands.entity(entity).insert(slices);
159215
}
160216
}

Diff for: ‎examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ Example | Description
395395
[UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials
396396
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
397397
[UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI
398+
[UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI
398399
[UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI
399400
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
400401
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates

Diff for: ‎examples/ui/ui_texture_atlas_slice.rs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//! This example illustrates how to create buttons with their texture atlases sliced
2+
//! and kept in proportion instead of being stretched by the button dimensions
3+
4+
use bevy::{
5+
color::palettes::css::{GOLD, ORANGE},
6+
prelude::*,
7+
winit::WinitSettings,
8+
};
9+
10+
fn main() {
11+
App::new()
12+
.add_plugins(DefaultPlugins)
13+
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
14+
.insert_resource(WinitSettings::desktop_app())
15+
.add_systems(Startup, setup)
16+
.add_systems(Update, button_system)
17+
.run();
18+
}
19+
20+
fn button_system(
21+
mut interaction_query: Query<
22+
(&Interaction, &mut TextureAtlas, &Children, &mut UiImage),
23+
(Changed<Interaction>, With<Button>),
24+
>,
25+
mut text_query: Query<&mut Text>,
26+
) {
27+
for (interaction, mut atlas, children, mut image) in &mut interaction_query {
28+
let mut text = text_query.get_mut(children[0]).unwrap();
29+
match *interaction {
30+
Interaction::Pressed => {
31+
text.sections[0].value = "Press".to_string();
32+
atlas.index = (atlas.index + 1) % 30;
33+
image.color = GOLD.into();
34+
}
35+
Interaction::Hovered => {
36+
text.sections[0].value = "Hover".to_string();
37+
image.color = ORANGE.into();
38+
}
39+
Interaction::None => {
40+
text.sections[0].value = "Button".to_string();
41+
image.color = Color::WHITE;
42+
}
43+
}
44+
}
45+
}
46+
47+
fn setup(
48+
mut commands: Commands,
49+
asset_server: Res<AssetServer>,
50+
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
51+
) {
52+
let texture_handle = asset_server.load("textures/fantasy_ui_borders/border_sheet.png");
53+
let atlas_layout = TextureAtlasLayout::from_grid(UVec2::new(50, 50), 6, 6, None, None);
54+
let atlas_layout_handle = texture_atlases.add(atlas_layout);
55+
56+
let slicer = TextureSlicer {
57+
border: BorderRect::square(22.0),
58+
center_scale_mode: SliceScaleMode::Stretch,
59+
sides_scale_mode: SliceScaleMode::Stretch,
60+
max_corner_scale: 1.0,
61+
};
62+
// ui camera
63+
commands.spawn(Camera2dBundle::default());
64+
commands
65+
.spawn(NodeBundle {
66+
style: Style {
67+
width: Val::Percent(100.0),
68+
height: Val::Percent(100.0),
69+
align_items: AlignItems::Center,
70+
justify_content: JustifyContent::Center,
71+
..default()
72+
},
73+
..default()
74+
})
75+
.with_children(|parent| {
76+
for (idx, [w, h]) in [
77+
(0, [150.0, 150.0]),
78+
(7, [300.0, 150.0]),
79+
(13, [150.0, 300.0]),
80+
] {
81+
parent
82+
.spawn((
83+
ButtonBundle {
84+
style: Style {
85+
width: Val::Px(w),
86+
height: Val::Px(h),
87+
// horizontally center child text
88+
justify_content: JustifyContent::Center,
89+
// vertically center child text
90+
align_items: AlignItems::Center,
91+
margin: UiRect::all(Val::Px(20.0)),
92+
..default()
93+
},
94+
image: texture_handle.clone().into(),
95+
..default()
96+
},
97+
ImageScaleMode::Sliced(slicer.clone()),
98+
TextureAtlas {
99+
index: idx,
100+
layout: atlas_layout_handle.clone(),
101+
},
102+
))
103+
.with_children(|parent| {
104+
parent.spawn(TextBundle::from_section(
105+
"Button",
106+
TextStyle {
107+
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
108+
font_size: 40.0,
109+
color: Color::srgb(0.9, 0.9, 0.9),
110+
},
111+
));
112+
});
113+
}
114+
});
115+
}

Diff for: ‎examples/ui/ui_texture_slice.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! This example illustrates how to create a button that has its image sliced
1+
//! This example illustrates how to create buttons with their textures sliced
22
//! and kept in proportion instead of being stretched by the button dimensions
33
44
use bevy::{prelude::*, winit::WinitSettings};

0 commit comments

Comments
 (0)
Please sign in to comment.