Skip to content

Commit

Permalink
Sprite slicing and tiling (#10588)
Browse files Browse the repository at this point in the history
> 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>
  • Loading branch information
4 people authored Jan 15, 2024
1 parent a7b99f0 commit 01139b3
Show file tree
Hide file tree
Showing 14 changed files with 847 additions and 21 deletions.
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,26 @@ description = "Renders an animated sprite"
category = "2D Rendering"
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]]
name = "text2d"
path = "examples/2d/text2d.rs"
Expand Down
Binary file added assets/textures/slice_square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/textures/slice_square_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite,
ImageScaleMode, Sprite,
};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
Expand All @@ -15,6 +15,8 @@ use bevy_transform::components::{GlobalTransform, Transform};
pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
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.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
Expand All @@ -35,6 +37,8 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle {
/// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0.
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
pub texture_atlas: Handle<TextureAtlas>,
/// Data pertaining to how the sprite is drawn on the screen
Expand Down
17 changes: 15 additions & 2 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ mod render;
mod sprite;
mod texture_atlas;
mod texture_atlas_builder;
mod texture_slice;

pub mod collide_aabb;

pub mod prelude {
#[doc(hidden)]
pub use crate::{
bundle::{SpriteBundle, SpriteSheetBundle},
sprite::Sprite,
sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasSprite},
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
};
}
Expand All @@ -26,6 +28,7 @@ pub use render::*;
pub use sprite::*;
pub use texture_atlas::*;
pub use texture_atlas_builder::*;
pub use texture_slice::*;

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

impl Plugin for SpritePlugin {
Expand All @@ -64,13 +68,22 @@ impl Plugin for SpritePlugin {
app.init_asset::<TextureAtlas>()
.register_asset_reflect::<TextureAtlas>()
.register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<TextureSlicer>()
.register_type::<TextureAtlasSprite>()
.register_type::<Anchor>()
.register_type::<Mesh2dHandle>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems(
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) {
Expand Down
46 changes: 28 additions & 18 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::ops::Range;

use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite, SPRITE_SHADER_HANDLE,
ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_core_pipeline::{
Expand Down Expand Up @@ -333,6 +333,7 @@ pub fn extract_sprite_events(
}

pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
sprite_query: Extract<
Expand All @@ -342,6 +343,7 @@ pub fn extract_sprites(
&Sprite,
&GlobalTransform,
&Handle<Image>,
Option<&ComputedTextureSlices>,
)>,
>,
atlas_query: Extract<
Expand All @@ -356,26 +358,34 @@ pub fn extract_sprites(
) {
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() {
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
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: sprite.color,
transform: *transform,
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,
},
);
if let Some(slices) = slices {
extracted_sprites.sprites.extend(
slices
.extract_sprites(transform, entity, sprite, handle)
.map(|e| (commands.spawn_empty().id(), e)),
);
} else {
// 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
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: sprite.color,
transform: *transform,
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
atlas_query.iter()
Expand Down
23 changes: 23 additions & 0 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use bevy_math::{Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::color::Color;

use crate::TextureSlicer;

/// Specifies the rendering properties of a sprite.
///
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
Expand All @@ -26,6 +28,27 @@ pub struct Sprite {
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).
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
Expand Down
59 changes: 59 additions & 0 deletions crates/bevy_sprite/src/texture_slice/border_rect.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Loading

0 comments on commit 01139b3

Please sign in to comment.