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 <alice.i.cecile@gmail.com>
Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
This commit is contained in:
Félix Lescaudey de Maneville 2024-01-15 16:40:06 +01:00 committed by GitHub
parent a7b99f0500
commit 01139b3472
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 847 additions and 21 deletions

View file

@ -455,6 +455,26 @@ description = "Renders an animated sprite"
category = "2D Rendering" category = "2D Rendering"
wasm = true wasm = true
[[example]]
name = "sprite_tile"
path = "examples/2d/sprite_tile.rs"
[package.metadata.example.sprite_tile]
name = "Sprite Tile"
description = "Renders a sprite tiled in a grid"
category = "2D Rendering"
wasm = true
[[example]]
name = "sprite_slice"
path = "examples/2d/sprite_slice.rs"
[package.metadata.example.sprite_slice]
name = "Sprite Slice"
description = "Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique"
category = "2D Rendering"
wasm = true
[[example]] [[example]]
name = "text2d" name = "text2d"
path = "examples/2d/text2d.rs" path = "examples/2d/text2d.rs"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite}, texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite, ImageScaleMode, Sprite,
}; };
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
@ -15,6 +15,8 @@ use bevy_transform::components::{GlobalTransform, Transform};
pub struct SpriteBundle { pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip. /// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite, pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent. /// The local transform of the sprite, relative to its parent.
pub transform: Transform, pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly. /// The absolute transform of the sprite. This should generally not be written to directly.
@ -35,6 +37,8 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle { pub struct SpriteSheetBundle {
/// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0. /// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0.
pub sprite: TextureAtlasSprite, pub sprite: TextureAtlasSprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// A handle to the texture atlas that holds the sprite images /// A handle to the texture atlas that holds the sprite images
pub texture_atlas: Handle<TextureAtlas>, pub texture_atlas: Handle<TextureAtlas>,
/// Data pertaining to how the sprite is drawn on the screen /// Data pertaining to how the sprite is drawn on the screen

View file

@ -6,6 +6,7 @@ mod render;
mod sprite; mod sprite;
mod texture_atlas; mod texture_atlas;
mod texture_atlas_builder; mod texture_atlas_builder;
mod texture_slice;
pub mod collide_aabb; pub mod collide_aabb;
@ -13,8 +14,9 @@ pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{
bundle::{SpriteBundle, SpriteSheetBundle}, bundle::{SpriteBundle, SpriteSheetBundle},
sprite::Sprite, sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasSprite}, texture_atlas::{TextureAtlas, TextureAtlasSprite},
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
}; };
} }
@ -26,6 +28,7 @@ pub use render::*;
pub use sprite::*; pub use sprite::*;
pub use texture_atlas::*; pub use texture_atlas::*;
pub use texture_atlas_builder::*; pub use texture_atlas_builder::*;
pub use texture_slice::*;
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; 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
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SpriteSystem { pub enum SpriteSystem {
ExtractSprites, ExtractSprites,
ComputeSlices,
} }
impl Plugin for SpritePlugin { impl Plugin for SpritePlugin {
@ -64,13 +68,22 @@ impl Plugin for SpritePlugin {
app.init_asset::<TextureAtlas>() app.init_asset::<TextureAtlas>()
.register_asset_reflect::<TextureAtlas>() .register_asset_reflect::<TextureAtlas>()
.register_type::<Sprite>() .register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<TextureSlicer>()
.register_type::<TextureAtlasSprite>() .register_type::<TextureAtlasSprite>()
.register_type::<Anchor>() .register_type::<Anchor>()
.register_type::<Mesh2dHandle>() .register_type::<Mesh2dHandle>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems( .add_systems(
PostUpdate, PostUpdate,
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), (
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
compute_slices_on_asset_event,
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
),
); );
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {

View file

@ -2,7 +2,7 @@ use std::ops::Range;
use crate::{ use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite}, texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite, SPRITE_SHADER_HANDLE, ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
}; };
use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
@ -333,6 +333,7 @@ pub fn extract_sprite_events(
} }
pub fn extract_sprites( pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>, mut extracted_sprites: ResMut<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>, texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
sprite_query: Extract< sprite_query: Extract<
@ -342,6 +343,7 @@ pub fn extract_sprites(
&Sprite, &Sprite,
&GlobalTransform, &GlobalTransform,
&Handle<Image>, &Handle<Image>,
Option<&ComputedTextureSlices>,
)>, )>,
>, >,
atlas_query: Extract< atlas_query: Extract<
@ -356,26 +358,34 @@ pub fn extract_sprites(
) { ) {
extracted_sprites.sprites.clear(); extracted_sprites.sprites.clear();
for (entity, view_visibility, sprite, transform, handle) in sprite_query.iter() { for (entity, view_visibility, sprite, transform, handle, slices) in sprite_query.iter() {
if !view_visibility.get() { if !view_visibility.get() {
continue; continue;
} }
// 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 if let Some(slices) = slices {
extracted_sprites.sprites.insert( extracted_sprites.sprites.extend(
entity, slices
ExtractedSprite { .extract_sprites(transform, entity, sprite, handle)
color: sprite.color, .map(|e| (commands.spawn_empty().id(), e)),
transform: *transform, );
rect: sprite.rect, } else {
// Pass the custom size // 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
custom_size: sprite.custom_size, extracted_sprites.sprites.insert(
flip_x: sprite.flip_x, entity,
flip_y: sprite.flip_y, ExtractedSprite {
image_handle_id: handle.id(), color: sprite.color,
anchor: sprite.anchor.as_vec(), transform: *transform,
original_entity: None, rect: sprite.rect,
}, // Pass the custom size
); custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: handle.id(),
anchor: sprite.anchor.as_vec(),
original_entity: None,
},
);
}
} }
for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in
atlas_query.iter() atlas_query.iter()

View file

@ -3,6 +3,8 @@ use bevy_math::{Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::color::Color; use bevy_render::color::Color;
use crate::TextureSlicer;
/// Specifies the rendering properties of a sprite. /// Specifies the rendering properties of a sprite.
/// ///
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle). /// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
@ -26,6 +28,27 @@ pub struct Sprite {
pub anchor: Anchor, pub anchor: Anchor,
} }
/// Controls how the image is altered when scaled.
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component, Default)]
pub enum ImageScaleMode {
/// The entire texture stretches when its dimensions change. This is the default option.
#[default]
Stretched,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform).
/// It defaults to `Anchor::Center`. /// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)] #[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]

View file

@ -0,0 +1,59 @@
use bevy_reflect::Reflect;
/// Struct defining a [`Sprite`](crate::Sprite) border with padding values
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
pub struct BorderRect {
/// Pixel padding to the left
pub left: f32,
/// Pixel padding to the right
pub right: f32,
/// Pixel padding to the top
pub top: f32,
/// Pixel padding to the bottom
pub bottom: f32,
}
impl BorderRect {
/// Creates a new border as a square, with identical pixel padding values on every direction
#[must_use]
#[inline]
pub const fn square(value: f32) -> Self {
Self {
left: value,
right: value,
top: value,
bottom: value,
}
}
/// Creates a new border as a rectangle, with:
/// - `horizontal` for left and right pixel padding
/// - `vertical` for top and bottom pixel padding
#[must_use]
#[inline]
pub const fn rectangle(horizontal: f32, vertical: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
}
impl From<f32> for BorderRect {
fn from(v: f32) -> Self {
Self::square(v)
}
}
impl From<[f32; 4]> for BorderRect {
fn from([left, right, top, bottom]: [f32; 4]) -> Self {
Self {
left,
right,
top,
bottom,
}
}
}

View file

@ -0,0 +1,150 @@
use crate::{ExtractedSprite, ImageScaleMode, Sprite};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_math::{Rect, Vec2};
use bevy_render::texture::Image;
use bevy_transform::prelude::*;
use bevy_utils::HashSet;
/// Component storing texture slices for sprite entities with a tiled or sliced [`ImageScaleMode`]
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
pub struct ComputedTextureSlices(Vec<TextureSlice>);
impl ComputedTextureSlices {
/// Computes [`ExtractedSprite`] iterator from the sprite slices
///
/// # Arguments
///
/// * `transform` - the sprite entity global transform
/// * `original_entity` - the sprite entity
/// * `sprite` - The sprite component
/// * `handle` - The sprite texture handle
#[must_use]
pub(crate) fn extract_sprites<'a>(
&'a self,
transform: &'a GlobalTransform,
original_entity: Entity,
sprite: &'a Sprite,
handle: &'a Handle<Image>,
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a {
self.0.iter().map(move |slice| {
let transform =
transform.mul_transform(Transform::from_translation(slice.offset.extend(0.0)));
ExtractedSprite {
original_entity: Some(original_entity),
color: sprite.color,
transform,
rect: Some(slice.texture_rect),
custom_size: Some(slice.draw_size),
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: handle.id(),
anchor: sprite.anchor.as_vec(),
}
})
}
}
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if either:
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
scale_mode: &ImageScaleMode,
image_handle: &Handle<Image>,
images: &Assets<Image>,
) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(image_handle).map(|i| {
Vec2::new(
i.texture_descriptor.size.width as f32,
i.texture_descriptor.size.height as f32,
)
})?;
let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
max: image_size,
}),
sprite.custom_size,
),
ImageScaleMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => {
let slice = TextureSlice {
texture_rect: sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
max: image_size,
}),
draw_size: sprite.custom_size.unwrap_or(image_size),
offset: Vec2::ZERO,
};
slice.tiled(*stretch_value, (*tile_x, *tile_y))
}
};
Some(ComputedTextureSlices(slices))
}
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
images: Res<Assets<Image>>,
sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle<Image>)>,
) {
// We store the asset ids of added/modified image assets
let added_handles: HashSet<_> = events
.read()
.filter_map(|e| match e {
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.collect();
if added_handles.is_empty() {
return;
}
// We recompute the sprite slices for sprite entities with a matching asset handle id
for (entity, scale_mode, sprite, image_handle) in &sprites {
if !added_handles.contains(&image_handle.id()) {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) {
commands.entity(entity).insert(slices);
}
}
}
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
changed_sprites: Query<
(Entity, &ImageScaleMode, &Sprite, &Handle<Image>),
Or<(
Changed<ImageScaleMode>,
Changed<Handle<Image>>,
Changed<Sprite>,
)>,
>,
) {
for (entity, scale_mode, sprite, image_handle) in &changed_sprites {
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) {
commands.entity(entity).insert(slices);
}
}
}

View file

@ -0,0 +1,86 @@
mod border_rect;
mod computed_slices;
mod slicer;
use bevy_math::{Rect, Vec2};
pub use border_rect::BorderRect;
pub use slicer::{SliceScaleMode, TextureSlicer};
pub(crate) use computed_slices::{
compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices,
};
#[derive(Debug, Clone)]
pub(crate) struct TextureSlice {
/// texture area to draw
pub texture_rect: Rect,
/// slice draw size
pub draw_size: Vec2,
/// offset of the slice
pub offset: Vec2,
}
impl TextureSlice {
/// Transforms the given slice in an collection of tiled subdivisions.
///
/// # Arguments
///
/// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* (rect) are above `stretch_value`.
/// - `tile_x` - should the slice be tiled horizontally
/// - `tile_y` - should the slice be tiled vertically
#[must_use]
pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec<Self> {
if !tile_x && !tile_y {
return vec![self];
}
let stretch_value = stretch_value.max(0.001);
let rect_size = self.texture_rect.size();
// Each tile expected size
let expected_size = Vec2::new(
if tile_x {
rect_size.x * stretch_value
} else {
self.draw_size.x
},
if tile_y {
rect_size.y * stretch_value
} else {
self.draw_size.y
},
);
let mut slices = Vec::new();
let base_offset = Vec2::new(
-self.draw_size.x / 2.0,
self.draw_size.y / 2.0, // Start from top
);
let mut offset = base_offset;
let mut remaining_columns = self.draw_size.y;
while remaining_columns > 0.0 {
let size_y = expected_size.y.min(remaining_columns);
offset.x = base_offset.x;
offset.y -= size_y / 2.0;
let mut remaining_rows = self.draw_size.x;
while remaining_rows > 0.0 {
let size_x = expected_size.x.min(remaining_rows);
offset.x += size_x / 2.0;
let draw_size = Vec2::new(size_x, size_y);
let delta = draw_size / expected_size;
slices.push(Self {
texture_rect: Rect {
min: self.texture_rect.min,
max: self.texture_rect.min + self.texture_rect.size() * delta,
},
draw_size,
offset: self.offset + offset,
});
offset.x += size_x / 2.0;
remaining_rows -= size_x;
}
offset.y -= size_y / 2.0;
remaining_columns -= size_y;
}
slices
}
}

View file

@ -0,0 +1,267 @@
use super::{BorderRect, TextureSlice};
use bevy_math::{vec2, Rect, Vec2};
use bevy_reflect::Reflect;
/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes
/// without needing to prepare multiple assets. The associated texture will be split into nine portions,
/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion.
///
/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect)]
pub struct TextureSlicer {
/// The sprite borders, defining the 9 sections of the image
pub border: BorderRect,
/// Defines how the center part of the 9 slices will scale
pub center_scale_mode: SliceScaleMode,
/// Defines how the 4 side parts of the 9 slices will scale
pub sides_scale_mode: SliceScaleMode,
/// Defines the maximum scale of the 4 corner slices (default to `1.0`)
pub max_corner_scale: f32,
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]
Stretch,
/// The slice will be tiled to fit the area
Tile {
/// The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above `stretch_value`.
///
/// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels.
/// `2.0` means it would repeat after 20 screen pixels.
///
/// Note: The value should be inferior or equal to `1.0` to avoid quality loss.
///
/// Note: the value will be clamped to `0.001` if lower
stretch_value: f32,
},
}
impl TextureSlicer {
/// Computes the 4 corner slices
#[must_use]
fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {
let coef = render_size / base_rect.size();
let BorderRect {
left,
right,
top,
bottom,
} = self.border;
let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);
[
// Top Left Corner
TextureSlice {
texture_rect: Rect {
min: base_rect.min,
max: base_rect.min + vec2(left, top),
},
draw_size: vec2(left, top) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Top Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.min.y),
max: vec2(base_rect.max.x, top),
},
draw_size: vec2(right, top) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Bottom Left
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.min.x, base_rect.max.y - bottom),
max: vec2(base_rect.min.x + left, base_rect.max.y),
},
draw_size: vec2(left, bottom) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
// Bottom Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),
max: base_rect.max,
},
draw_size: vec2(right, bottom) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
]
}
/// Computes the 2 horizontal side slices (left and right borders)
#[must_use]
fn horizontal_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// left
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(0.0, self.border.top),
max: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
},
draw_size: vec2(
bl_corner.draw_size.x,
render_size.y - bl_corner.draw_size.y - tl_corner.draw_size.y,
),
offset: vec2(-render_size.x + bl_corner.draw_size.x, 0.0) / 2.0,
},
// right
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.bottom,
),
max: vec2(base_rect.max.x, base_rect.max.y - self.border.top),
},
draw_size: vec2(
br_corner.draw_size.x,
render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y),
),
offset: vec2(render_size.x - br_corner.draw_size.x, 0.0) / 2.0,
},
]
}
/// Computes the 2 vertical side slices (bottom and top borders)
#[must_use]
fn vertical_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Bottom
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
max: vec2(base_rect.max.x - self.border.right, base_rect.max.y),
},
draw_size: vec2(
render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),
bl_corner.draw_size.y,
),
offset: vec2(0.0, bl_corner.offset.y),
},
// Top
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(self.border.left, 0.0),
max: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
},
draw_size: vec2(
render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),
tl_corner.draw_size.y,
),
offset: vec2(0.0, tl_corner.offset.y),
},
]
}
/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,
/// a bigger number of sections will be computed.
///
/// # Arguments
///
/// * `rect` - The section of the texture to slice in 9 parts
/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.
#[must_use]
pub(crate) fn compute_slices(
&self,
rect: Rect,
render_size: Option<Vec2>,
) -> Vec<TextureSlice> {
let render_size = render_size.unwrap_or_else(|| rect.size());
let mut slices = Vec::with_capacity(9);
// Corners
let corners = self.corner_slices(rect, render_size);
// Sides
let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);
let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);
// Center
let center = TextureSlice {
texture_rect: Rect {
min: rect.min + vec2(self.border.left, self.border.bottom),
max: vec2(rect.max.x - self.border.right, rect.max.y - self.border.top),
},
draw_size: vec2(
render_size.x - (corners[2].draw_size.x + corners[3].draw_size.x),
render_size.y - (corners[2].draw_size.y + corners[0].draw_size.y),
),
offset: Vec2::ZERO,
};
slices.extend(corners);
match self.center_scale_mode {
SliceScaleMode::Stretch => {
slices.push(center);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(center.tiled(stretch_value, (true, true)));
}
}
match self.sides_scale_mode {
SliceScaleMode::Stretch => {
slices.extend(horizontal_sides);
slices.extend(vertical_sides);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(
horizontal_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (false, true))),
);
slices.extend(
vertical_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (true, false))),
);
}
}
slices
}
}
impl Default for TextureSlicer {
fn default() -> Self {
Self {
border: Default::default(),
center_scale_mode: Default::default(),
sides_scale_mode: Default::default(),
max_corner_scale: 1.0,
}
}
}

144
examples/2d/sprite_slice.rs Normal file
View file

@ -0,0 +1,144 @@
//! Showcases sprite 9 slice scaling
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: (1350.0, 700.0).into(),
..default()
}),
..default()
}))
.add_systems(Startup, setup)
.run();
}
fn spawn_sprites(
commands: &mut Commands,
texture_handle: Handle<Image>,
mut position: Vec3,
slice_border: f32,
style: TextStyle,
gap: f32,
) {
let cases = [
// Reference sprite
(
"Original texture",
style.clone(),
Vec2::splat(100.0),
ImageScaleMode::default(),
),
// Scaled regular sprite
(
"Stretched texture",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::default(),
),
// Stretched Scaled sliced sprite
(
"Stretched and sliced",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch,
..default()
}),
),
// Scaled sliced sprite
(
"Sliced and Tiled",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
..default()
}),
),
// Scaled sliced sprite horizontally
(
"Sliced and Tiled",
style.clone(),
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
..default()
}),
),
// Scaled sliced sprite horizontally with max scale
(
"Sliced and Tiled with corner constraint",
style,
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
max_corner_scale: 0.2,
}),
),
];
for (label, text_style, size, scale_mode) in cases {
position.x += 0.5 * size.x;
commands
.spawn(SpriteBundle {
transform: Transform::from_translation(position),
texture: texture_handle.clone(),
sprite: Sprite {
custom_size: Some(size),
..default()
},
scale_mode,
..default()
})
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default()
});
});
position.x += 0.5 * size.x + gap;
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
let style = TextStyle {
font: font.clone(),
font_size: 16.0,
color: Color::WHITE,
};
// Load textures
let handle_1 = asset_server.load("textures/slice_square.png");
let handle_2 = asset_server.load("textures/slice_square_2.png");
spawn_sprites(
&mut commands,
handle_1,
Vec3::new(-600.0, 200.0, 0.0),
200.0,
style.clone(),
50.,
);
spawn_sprites(
&mut commands,
handle_2,
Vec3::new(-600.0, -200.0, 0.0),
80.0,
style,
50.,
);
}

View file

@ -0,0 +1,48 @@
//! Displays a single [`Sprite`] tiled in a grid, with a scaling animation
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, animate)
.run();
}
#[derive(Resource)]
struct AnimationState {
min: f32,
max: f32,
current: f32,
speed: f32,
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands.insert_resource(AnimationState {
min: 128.0,
max: 512.0,
current: 128.0,
speed: 50.0,
});
commands.spawn(SpriteBundle {
texture: asset_server.load("branding/icon.png"),
scale_mode: ImageScaleMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 0.5, // The image will tile every 128px
},
..default()
});
}
fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) {
if state.current >= state.max || state.current <= state.min {
state.speed = -state.speed;
};
state.current += state.speed * time.delta_seconds();
for mut sprite in &mut sprites {
sprite.custom_size = Some(Vec2::splat(state.current));
}
}

View file

@ -106,6 +106,8 @@ Example | Description
[Sprite](../examples/2d/sprite.rs) | Renders a sprite [Sprite](../examples/2d/sprite.rs) | Renders a sprite
[Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis [Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis
[Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite [Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite
[Sprite Slice](../examples/2d/sprite_slice.rs) | Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique
[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid
[Text 2D](../examples/2d/text2d.rs) | Generates text in 2D [Text 2D](../examples/2d/text2d.rs) | Generates text in 2D
[Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites [Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
[Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d [Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d