Skip to content

Commit 01139b3

Browse files
ManevilleFalice-i-cecileickshonpeAlice Cecile
authored
Sprite slicing and tiling (#10588)
> Replaces #5213 # Objective Implement sprite tiling and [9 slice scaling](https://en.wikipedia.org/wiki/9-slice_scaling) for `bevy_sprite`. Allowing slice scaling and texture tiling. Basic scaling vs 9 slice scaling: ![Traditional_scaling_vs_9-slice_scaling](https://user-images.githubusercontent.com/26703856/177335801-27f6fa27-c569-4ce6-b0e6-4f54e8f4e80a.svg) Slicing example: <img width="481" alt="Screenshot 2022-07-05 at 15 05 49" src="https://user-images.githubusercontent.com/26703856/177336112-9e961af0-c0af-4197-aec9-430c1170a79d.png"> Tiling example: <img width="1329" alt="Screenshot 2023-11-16 at 13 53 32" src="https://github.com/bevyengine/bevy/assets/26703856/14db39b7-d9e0-4bc3-ba0e-b1f2db39ae8f"> # Solution - `SpriteBundlue` now has a `scale_mode` component storing a `SpriteScaleMode` enum with three variants: - `Stretched` (default) - `Tiled` to have sprites tile horizontally and/or vertically - `Sliced` allowing 9 slicing the texture and optionally tile some sections with a `Textureslicer`. - `bevy_sprite` has two extra systems to compute a `ComputedTextureSlices` if necessary,: - One system react to changes on `Sprite`, `Handle<Image>` or `SpriteScaleMode` - The other listens to `AssetEvent<Image>` to compute slices on sprites when the texture is ready or changed - I updated the `bevy_sprite` extraction stage to extract potentially multiple textures instead of one, depending on the presence of `ComputedTextureSlices` - I added two examples showcasing the slicing and tiling feature. The addition of `ComputedTextureSlices` as a cache is to avoid querying the image data, to retrieve its dimensions, every frame in a extract or prepare stage. Also it reacts to changes so we can have stuff like this (tiling example): https://github.com/bevyengine/bevy/assets/26703856/a349a9f3-33c3-471f-8ef4-a0e5dfce3b01 # Related - [ ] Once #5103 or #10099 is merged I can enable tiling and slicing for texture sheets as ui # To discuss There is an other option, to consider slice/tiling as part of the asset, using the new asset preprocessing but I have no clue on how to do it. Also, instead of retrieving the Image dimensions, we could use the same system as the sprite sheet and have the user give the image dimensions directly (grid). But I think it's less user friendly --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: ickshonpe <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent a7b99f0 commit 01139b3

File tree

14 files changed

+847
-21
lines changed

14 files changed

+847
-21
lines changed

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,26 @@ description = "Renders an animated sprite"
455455
category = "2D Rendering"
456456
wasm = true
457457

458+
[[example]]
459+
name = "sprite_tile"
460+
path = "examples/2d/sprite_tile.rs"
461+
462+
[package.metadata.example.sprite_tile]
463+
name = "Sprite Tile"
464+
description = "Renders a sprite tiled in a grid"
465+
category = "2D Rendering"
466+
wasm = true
467+
468+
[[example]]
469+
name = "sprite_slice"
470+
path = "examples/2d/sprite_slice.rs"
471+
472+
[package.metadata.example.sprite_slice]
473+
name = "Sprite Slice"
474+
description = "Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique"
475+
category = "2D Rendering"
476+
wasm = true
477+
458478
[[example]]
459479
name = "text2d"
460480
path = "examples/2d/text2d.rs"

assets/textures/slice_square.png

13.2 KB
Loading

assets/textures/slice_square_2.png

12.4 KB
Loading

crates/bevy_sprite/src/bundle.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
texture_atlas::{TextureAtlas, TextureAtlasSprite},
3-
Sprite,
3+
ImageScaleMode, Sprite,
44
};
55
use bevy_asset::Handle;
66
use bevy_ecs::bundle::Bundle;
@@ -15,6 +15,8 @@ use bevy_transform::components::{GlobalTransform, Transform};
1515
pub struct SpriteBundle {
1616
/// Specifies the rendering properties of the sprite, such as color tint and flip.
1717
pub sprite: Sprite,
18+
/// Controls how the image is altered when scaled.
19+
pub scale_mode: ImageScaleMode,
1820
/// The local transform of the sprite, relative to its parent.
1921
pub transform: Transform,
2022
/// The absolute transform of the sprite. This should generally not be written to directly.
@@ -35,6 +37,8 @@ pub struct SpriteBundle {
3537
pub struct SpriteSheetBundle {
3638
/// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0.
3739
pub sprite: TextureAtlasSprite,
40+
/// Controls how the image is altered when scaled.
41+
pub scale_mode: ImageScaleMode,
3842
/// A handle to the texture atlas that holds the sprite images
3943
pub texture_atlas: Handle<TextureAtlas>,
4044
/// Data pertaining to how the sprite is drawn on the screen

crates/bevy_sprite/src/lib.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ mod render;
66
mod sprite;
77
mod texture_atlas;
88
mod texture_atlas_builder;
9+
mod texture_slice;
910

1011
pub mod collide_aabb;
1112

1213
pub mod prelude {
1314
#[doc(hidden)]
1415
pub use crate::{
1516
bundle::{SpriteBundle, SpriteSheetBundle},
16-
sprite::Sprite,
17+
sprite::{ImageScaleMode, Sprite},
1718
texture_atlas::{TextureAtlas, TextureAtlasSprite},
19+
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
1820
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
1921
};
2022
}
@@ -26,6 +28,7 @@ pub use render::*;
2628
pub use sprite::*;
2729
pub use texture_atlas::*;
2830
pub use texture_atlas_builder::*;
31+
pub use texture_slice::*;
2932

3033
use bevy_app::prelude::*;
3134
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
@@ -51,6 +54,7 @@ pub const SPRITE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(27633439
5154
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
5255
pub enum SpriteSystem {
5356
ExtractSprites,
57+
ComputeSlices,
5458
}
5559

5660
impl Plugin for SpritePlugin {
@@ -64,13 +68,22 @@ impl Plugin for SpritePlugin {
6468
app.init_asset::<TextureAtlas>()
6569
.register_asset_reflect::<TextureAtlas>()
6670
.register_type::<Sprite>()
71+
.register_type::<ImageScaleMode>()
72+
.register_type::<TextureSlicer>()
6773
.register_type::<TextureAtlasSprite>()
6874
.register_type::<Anchor>()
6975
.register_type::<Mesh2dHandle>()
7076
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
7177
.add_systems(
7278
PostUpdate,
73-
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
79+
(
80+
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
81+
(
82+
compute_slices_on_asset_event,
83+
compute_slices_on_sprite_change,
84+
)
85+
.in_set(SpriteSystem::ComputeSlices),
86+
),
7487
);
7588

7689
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {

crates/bevy_sprite/src/render/mod.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::ops::Range;
22

33
use crate::{
44
texture_atlas::{TextureAtlas, TextureAtlasSprite},
5-
Sprite, SPRITE_SHADER_HANDLE,
5+
ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
66
};
77
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
88
use bevy_core_pipeline::{
@@ -333,6 +333,7 @@ pub fn extract_sprite_events(
333333
}
334334

335335
pub fn extract_sprites(
336+
mut commands: Commands,
336337
mut extracted_sprites: ResMut<ExtractedSprites>,
337338
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
338339
sprite_query: Extract<
@@ -342,6 +343,7 @@ pub fn extract_sprites(
342343
&Sprite,
343344
&GlobalTransform,
344345
&Handle<Image>,
346+
Option<&ComputedTextureSlices>,
345347
)>,
346348
>,
347349
atlas_query: Extract<
@@ -356,26 +358,34 @@ pub fn extract_sprites(
356358
) {
357359
extracted_sprites.sprites.clear();
358360

359-
for (entity, view_visibility, sprite, transform, handle) in sprite_query.iter() {
361+
for (entity, view_visibility, sprite, transform, handle, slices) in sprite_query.iter() {
360362
if !view_visibility.get() {
361363
continue;
362364
}
363-
// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive
364-
extracted_sprites.sprites.insert(
365-
entity,
366-
ExtractedSprite {
367-
color: sprite.color,
368-
transform: *transform,
369-
rect: sprite.rect,
370-
// Pass the custom size
371-
custom_size: sprite.custom_size,
372-
flip_x: sprite.flip_x,
373-
flip_y: sprite.flip_y,
374-
image_handle_id: handle.id(),
375-
anchor: sprite.anchor.as_vec(),
376-
original_entity: None,
377-
},
378-
);
365+
if let Some(slices) = slices {
366+
extracted_sprites.sprites.extend(
367+
slices
368+
.extract_sprites(transform, entity, sprite, handle)
369+
.map(|e| (commands.spawn_empty().id(), e)),
370+
);
371+
} else {
372+
// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive
373+
extracted_sprites.sprites.insert(
374+
entity,
375+
ExtractedSprite {
376+
color: sprite.color,
377+
transform: *transform,
378+
rect: sprite.rect,
379+
// Pass the custom size
380+
custom_size: sprite.custom_size,
381+
flip_x: sprite.flip_x,
382+
flip_y: sprite.flip_y,
383+
image_handle_id: handle.id(),
384+
anchor: sprite.anchor.as_vec(),
385+
original_entity: None,
386+
},
387+
);
388+
}
379389
}
380390
for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in
381391
atlas_query.iter()

crates/bevy_sprite/src/sprite.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use bevy_math::{Rect, Vec2};
33
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
44
use bevy_render::color::Color;
55

6+
use crate::TextureSlicer;
7+
68
/// Specifies the rendering properties of a sprite.
79
///
810
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
@@ -26,6 +28,27 @@ pub struct Sprite {
2628
pub anchor: Anchor,
2729
}
2830

31+
/// Controls how the image is altered when scaled.
32+
#[derive(Component, Debug, Default, Clone, Reflect)]
33+
#[reflect(Component, Default)]
34+
pub enum ImageScaleMode {
35+
/// The entire texture stretches when its dimensions change. This is the default option.
36+
#[default]
37+
Stretched,
38+
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
39+
Sliced(TextureSlicer),
40+
/// The texture will be repeated if stretched beyond `stretched_value`
41+
Tiled {
42+
/// Should the image repeat horizontally
43+
tile_x: bool,
44+
/// Should the image repeat vertically
45+
tile_y: bool,
46+
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
47+
/// *original texture size* are above this value.
48+
stretch_value: f32,
49+
},
50+
}
51+
2952
/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform).
3053
/// It defaults to `Anchor::Center`.
3154
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use bevy_reflect::Reflect;
2+
3+
/// Struct defining a [`Sprite`](crate::Sprite) border with padding values
4+
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
5+
pub struct BorderRect {
6+
/// Pixel padding to the left
7+
pub left: f32,
8+
/// Pixel padding to the right
9+
pub right: f32,
10+
/// Pixel padding to the top
11+
pub top: f32,
12+
/// Pixel padding to the bottom
13+
pub bottom: f32,
14+
}
15+
16+
impl BorderRect {
17+
/// Creates a new border as a square, with identical pixel padding values on every direction
18+
#[must_use]
19+
#[inline]
20+
pub const fn square(value: f32) -> Self {
21+
Self {
22+
left: value,
23+
right: value,
24+
top: value,
25+
bottom: value,
26+
}
27+
}
28+
29+
/// Creates a new border as a rectangle, with:
30+
/// - `horizontal` for left and right pixel padding
31+
/// - `vertical` for top and bottom pixel padding
32+
#[must_use]
33+
#[inline]
34+
pub const fn rectangle(horizontal: f32, vertical: f32) -> Self {
35+
Self {
36+
left: horizontal,
37+
right: horizontal,
38+
top: vertical,
39+
bottom: vertical,
40+
}
41+
}
42+
}
43+
44+
impl From<f32> for BorderRect {
45+
fn from(v: f32) -> Self {
46+
Self::square(v)
47+
}
48+
}
49+
50+
impl From<[f32; 4]> for BorderRect {
51+
fn from([left, right, top, bottom]: [f32; 4]) -> Self {
52+
Self {
53+
left,
54+
right,
55+
top,
56+
bottom,
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)