From fc202f2e3dc471acdf64e04d8f4076d2c3f198f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Lescaudey=20de=20Maneville?= Date: Tue, 5 Mar 2024 17:05:39 +0100 Subject: [PATCH] Slicing support for texture atlas (#12059) # Objective Follow up to #11600 and #10588 https://github.com/bevyengine/bevy/issues/11944 made clear that some people want to use slicing with texture atlases ## Changelog * Added support for `TextureAtlas` slicing and tiling. `SpriteSheetBundle` and `AtlasImageBundle` can now use `ImageScaleMode` * Added new `ui_texture_atlas_slice` example using a texture sheet Screenshot 2024-02-23 at 11 58 35 --------- Co-authored-by: Alice Cecile Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> --- Cargo.toml | 11 ++ .../fantasy_ui_borders/border_sheet.png | Bin 0 -> 10539 bytes crates/bevy_sprite/src/sprite.rs | 2 - .../src/texture_slice/computed_slices.rs | 95 +++++++++++---- .../bevy_sprite/src/texture_slice/slicer.rs | 5 +- crates/bevy_ui/src/node_bundles.rs | 7 +- crates/bevy_ui/src/texture_slice.rs | 92 +++++++++++--- examples/README.md | 1 + examples/ui/ui_texture_atlas_slice.rs | 115 ++++++++++++++++++ examples/ui/ui_texture_slice.rs | 2 +- 10 files changed, 283 insertions(+), 47 deletions(-) create mode 100644 assets/textures/fantasy_ui_borders/border_sheet.png create mode 100644 examples/ui/ui_texture_atlas_slice.rs diff --git a/Cargo.toml b/Cargo.toml index cf5d69ef54183..216db0ff923b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2510,6 +2510,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_atlas_slice" +path = "examples/ui/ui_texture_atlas_slice.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_atlas_slice] +name = "UI Texture Atlas Slice" +description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/assets/textures/fantasy_ui_borders/border_sheet.png b/assets/textures/fantasy_ui_borders/border_sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..8ee2d2a9b2d34952420adfe8110b23f2cc28657d GIT binary patch literal 10539 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 literal 0 HcmV?d00001 diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 40d0dd5f3ca68..ae57eb760c091 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -32,8 +32,6 @@ pub struct Sprite { } /// Controls how the image is altered when scaled. -/// -/// Note: This is not yet compatible with texture atlases #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component)] pub enum ImageScaleMode { diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index cbc5a370b43f9..1f08e9a817d49 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -1,4 +1,4 @@ -use crate::{ExtractedSprite, ImageScaleMode, Sprite}; +use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlas, TextureAtlasLayout}; use super::TextureSlice; use bevy_asset::{AssetEvent, Assets, Handle}; @@ -63,37 +63,55 @@ impl ComputedTextureSlices { /// will be computed according to the `image_handle` dimensions or the sprite rect. /// /// Returns `None` if the image asset is not loaded +/// +/// # Arguments +/// +/// * `sprite` - The sprite component, will be used to find the draw area size +/// * `scale_mode` - The image scaling component +/// * `image_handle` - The texture to slice or tile +/// * `images` - The image assets, use to retrieve the image dimensions +/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section +/// of the texture +/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect #[must_use] fn compute_sprite_slices( sprite: &Sprite, scale_mode: &ImageScaleMode, image_handle: &Handle, images: &Assets, + atlas: Option<&TextureAtlas>, + atlas_layouts: &Assets, ) -> Option { - 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::Sliced(slicer) => slicer.compute_slices( - sprite.rect.unwrap_or(Rect { + let (image_size, texture_rect) = match atlas { + Some(a) => { + let layout = atlas_layouts.get(&a.layout)?; + ( + layout.size.as_vec2(), + layout.textures.get(a.index)?.as_rect(), + ) + } + None => { + let image = images.get(image_handle)?; + let size = Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ); + let rect = sprite.rect.unwrap_or(Rect { min: Vec2::ZERO, - max: image_size, - }), - sprite.custom_size, - ), + max: size, + }); + (size, rect) + } + }; + let slices = match scale_mode { + ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, 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, - }), + texture_rect, draw_size: sprite.custom_size.unwrap_or(image_size), offset: Vec2::ZERO, }; @@ -109,7 +127,14 @@ pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, images: Res>, - sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle)>, + atlas_layouts: Res>, + sprites: Query<( + Entity, + &ImageScaleMode, + &Sprite, + &Handle, + Option<&TextureAtlas>, + )>, ) { // We store the asset ids of added/modified image assets let added_handles: HashSet<_> = events @@ -123,11 +148,18 @@ pub(crate) fn compute_slices_on_asset_event( return; } // We recompute the sprite slices for sprite entities with a matching asset handle id - for (entity, scale_mode, sprite, image_handle) in &sprites { + for (entity, scale_mode, sprite, image_handle, atlas) in &sprites { if !added_handles.contains(&image_handle.id()) { continue; } - if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + if let Some(slices) = compute_sprite_slices( + sprite, + scale_mode, + image_handle, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } @@ -138,17 +170,32 @@ pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_sprite_change( mut commands: Commands, images: Res>, + atlas_layouts: Res>, changed_sprites: Query< - (Entity, &ImageScaleMode, &Sprite, &Handle), + ( + Entity, + &ImageScaleMode, + &Sprite, + &Handle, + Option<&TextureAtlas>, + ), Or<( Changed, Changed>, Changed, + Changed, )>, >, ) { - for (entity, scale_mode, sprite, image_handle) in &changed_sprites { - if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + for (entity, scale_mode, sprite, image_handle, atlas) in &changed_sprites { + if let Some(slices) = compute_sprite_slices( + sprite, + scale_mode, + image_handle, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } diff --git a/crates/bevy_sprite/src/texture_slice/slicer.rs b/crates/bevy_sprite/src/texture_slice/slicer.rs index ab12ce47488e5..d930aab705d12 100644 --- a/crates/bevy_sprite/src/texture_slice/slicer.rs +++ b/crates/bevy_sprite/src/texture_slice/slicer.rs @@ -72,7 +72,7 @@ impl TextureSlicer { TextureSlice { texture_rect: Rect { min: vec2(base_rect.max.x - right, base_rect.min.y), - max: vec2(base_rect.max.x, top), + max: vec2(base_rect.max.x, base_rect.min.y + top), }, draw_size: vec2(right, top) * min_coef, offset: vec2( @@ -198,6 +198,9 @@ impl TextureSlicer { /// /// * `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. + // + // TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`) + // #[must_use] pub fn compute_slices(&self, rect: Rect, render_size: Option) -> Vec { let render_size = render_size.unwrap_or_else(|| rect.size()); diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index f760b5377086f..29164c6e22186 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -122,6 +122,11 @@ pub struct ImageBundle { /// A UI node that is a texture atlas sprite /// +/// # Extra behaviours +/// +/// You may add the following components to enable additional behaviours +/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture +/// /// This bundle is identical to [`ImageBundle`] with an additional [`TextureAtlas`] component. #[deprecated( since = "0.14.0", @@ -295,7 +300,7 @@ where /// /// You may add the following components to enable additional behaviours: /// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture -/// - [`TextureAtlas`] to draw specific sections of the texture +/// - [`TextureAtlas`] to draw specific section of the texture /// /// Note that `ImageScaleMode` is currently not compatible with `TextureAtlas`. #[derive(Bundle, Clone, Debug)] diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index 8d07000caaab8..c0836a3b870bb 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -6,7 +6,7 @@ use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_math::{Rect, Vec2}; use bevy_render::texture::Image; -use bevy_sprite::{ImageScaleMode, TextureSlice}; +use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice}; use bevy_transform::prelude::*; use bevy_utils::HashSet; @@ -74,25 +74,48 @@ impl ComputedTextureSlices { } /// 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. +/// will be computed according to the `image_handle` dimensions. /// /// Returns `None` if the image asset is not loaded +/// +/// # Arguments +/// +/// * `draw_area` - The size of the drawing area the slices will have to fit into +/// * `scale_mode` - The image scaling component +/// * `image_handle` - The texture to slice or tile +/// * `images` - The image assets, use to retrieve the image dimensions +/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section +/// of the texture +/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect #[must_use] fn compute_texture_slices( draw_area: Vec2, scale_mode: &ImageScaleMode, image_handle: &UiImage, images: &Assets, + atlas: Option<&TextureAtlas>, + atlas_layouts: &Assets, ) -> Option { - let image_size = images.get(&image_handle.texture).map(|i| { - Vec2::new( - i.texture_descriptor.size.width as f32, - i.texture_descriptor.size.height as f32, - ) - })?; - let texture_rect = Rect { - min: Vec2::ZERO, - max: image_size, + let (image_size, texture_rect) = match atlas { + Some(a) => { + let layout = atlas_layouts.get(&a.layout)?; + ( + layout.size.as_vec2(), + layout.textures.get(a.index)?.as_rect(), + ) + } + None => { + let image = images.get(&image_handle.texture)?; + let size = Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ); + let rect = Rect { + min: Vec2::ZERO, + max: size, + }; + (size, rect) + } }; let slices = match scale_mode { ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), @@ -118,7 +141,14 @@ pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, images: Res>, - ui_nodes: Query<(Entity, &ImageScaleMode, &Node, &UiImage)>, + atlas_layouts: Res>, + ui_nodes: Query<( + Entity, + &ImageScaleMode, + &Node, + &UiImage, + Option<&TextureAtlas>, + )>, ) { // We store the asset ids of added/modified image assets let added_handles: HashSet<_> = events @@ -132,11 +162,18 @@ pub(crate) fn compute_slices_on_asset_event( return; } // We recompute the sprite slices for sprite entities with a matching asset handle id - for (entity, scale_mode, ui_node, image) in &ui_nodes { + for (entity, scale_mode, ui_node, image, atlas) in &ui_nodes { if !added_handles.contains(&image.texture.id()) { continue; } - if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) { + if let Some(slices) = compute_texture_slices( + ui_node.size(), + scale_mode, + image, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } @@ -147,13 +184,32 @@ pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_image_change( mut commands: Commands, images: Res>, + atlas_layouts: Res>, changed_nodes: Query< - (Entity, &ImageScaleMode, &Node, &UiImage), - Or<(Changed, Changed, Changed)>, + ( + Entity, + &ImageScaleMode, + &Node, + &UiImage, + Option<&TextureAtlas>, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + )>, >, ) { - for (entity, scale_mode, ui_node, image) in &changed_nodes { - if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) { + for (entity, scale_mode, ui_node, image, atlas) in &changed_nodes { + if let Some(slices) = compute_texture_slices( + ui_node.size(), + scale_mode, + image, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } diff --git a/examples/README.md b/examples/README.md index 61eca36f5113f..3ee06cbb5a793 100644 --- a/examples/README.md +++ b/examples/README.md @@ -405,6 +405,7 @@ Example | Description [UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials [UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI +[UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates diff --git a/examples/ui/ui_texture_atlas_slice.rs b/examples/ui/ui_texture_atlas_slice.rs new file mode 100644 index 0000000000000..77901211b7ed0 --- /dev/null +++ b/examples/ui/ui_texture_atlas_slice.rs @@ -0,0 +1,115 @@ +//! This example illustrates how to create buttons with their texture atlases sliced +//! and kept in proportion instead of being stretched by the button dimensions + +use bevy::{ + color::palettes::css::{GOLD, ORANGE}, + prelude::*, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .add_systems(Update, button_system) + .run(); +} + +fn button_system( + mut interaction_query: Query< + (&Interaction, &mut TextureAtlas, &Children, &mut UiImage), + (Changed, With