mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
a7b99f0500
commit
01139b3472
14 changed files with 847 additions and 21 deletions
20
Cargo.toml
20
Cargo.toml
|
@ -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"
|
||||
|
|
BIN
assets/textures/slice_square.png
Normal file
BIN
assets/textures/slice_square.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
assets/textures/slice_square_2.png
Normal file
BIN
assets/textures/slice_square_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
texture_atlas::{TextureAtlas, TextureAtlasSprite},
|
||||
Sprite,
|
||||
ImageScaleMode, Sprite,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_ecs::bundle::Bundle;
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
|
|
@ -6,6 +6,7 @@ mod render;
|
|||
mod sprite;
|
||||
mod texture_atlas;
|
||||
mod texture_atlas_builder;
|
||||
mod texture_slice;
|
||||
|
||||
pub mod collide_aabb;
|
||||
|
||||
|
@ -13,8 +14,9 @@ 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,
|
||||
};
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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::{
|
||||
|
@ -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<
|
||||
|
@ -342,6 +343,7 @@ pub fn extract_sprites(
|
|||
&Sprite,
|
||||
&GlobalTransform,
|
||||
&Handle<Image>,
|
||||
Option<&ComputedTextureSlices>,
|
||||
)>,
|
||||
>,
|
||||
atlas_query: Extract<
|
||||
|
@ -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()
|
||||
|
|
|
@ -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).
|
||||
|
@ -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)]
|
||||
|
|
59
crates/bevy_sprite/src/texture_slice/border_rect.rs
Normal file
59
crates/bevy_sprite/src/texture_slice/border_rect.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
150
crates/bevy_sprite/src/texture_slice/computed_slices.rs
Normal file
150
crates/bevy_sprite/src/texture_slice/computed_slices.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
86
crates/bevy_sprite/src/texture_slice/mod.rs
Normal file
86
crates/bevy_sprite/src/texture_slice/mod.rs
Normal 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
|
||||
}
|
||||
}
|
267
crates/bevy_sprite/src/texture_slice/slicer.rs
Normal file
267
crates/bevy_sprite/src/texture_slice/slicer.rs
Normal 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
144
examples/2d/sprite_slice.rs
Normal 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.,
|
||||
);
|
||||
}
|
48
examples/2d/sprite_tile.rs
Normal file
48
examples/2d/sprite_tile.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -106,6 +106,8 @@ Example | Description
|
|||
[Sprite](../examples/2d/sprite.rs) | Renders a sprite
|
||||
[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 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
|
||||
[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
|
||||
|
|
Loading…
Reference in a new issue