> 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:  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>
151 lines
5.0 KiB
Rust
151 lines
5.0 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|