mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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 <img width="798" alt="Screenshot 2024-02-23 at 11 58 35" src="https://github.com/bevyengine/bevy/assets/26703856/47a8b764-127c-4a06-893f-181703777501"> --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com>
This commit is contained in:
parent
dc40cd134f
commit
fc202f2e3d
10 changed files with 283 additions and 47 deletions
11
Cargo.toml
11
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"
|
||||
|
|
BIN
assets/textures/fantasy_ui_borders/border_sheet.png
Normal file
BIN
assets/textures/fantasy_ui_borders/border_sheet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -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 {
|
||||
|
|
|
@ -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<Image>,
|
||||
images: &Assets<Image>,
|
||||
atlas: Option<&TextureAtlas>,
|
||||
atlas_layouts: &Assets<TextureAtlasLayout>,
|
||||
) -> Option<ComputedTextureSlices> {
|
||||
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 (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(),
|
||||
)
|
||||
})?;
|
||||
let slices = match scale_mode {
|
||||
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
|
||||
sprite.rect.unwrap_or(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<AssetEvent<Image>>,
|
||||
images: Res<Assets<Image>>,
|
||||
sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle<Image>)>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
sprites: Query<(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Sprite,
|
||||
&Handle<Image>,
|
||||
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<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
changed_sprites: Query<
|
||||
(Entity, &ImageScaleMode, &Sprite, &Handle<Image>),
|
||||
(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Sprite,
|
||||
&Handle<Image>,
|
||||
Option<&TextureAtlas>,
|
||||
),
|
||||
Or<(
|
||||
Changed<ImageScaleMode>,
|
||||
Changed<Handle<Image>>,
|
||||
Changed<Sprite>,
|
||||
Changed<TextureAtlas>,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vec2>) -> Vec<TextureSlice> {
|
||||
let render_size = render_size.unwrap_or_else(|| rect.size());
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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<Image>,
|
||||
atlas: Option<&TextureAtlas>,
|
||||
atlas_layouts: &Assets<TextureAtlasLayout>,
|
||||
) -> Option<ComputedTextureSlices> {
|
||||
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 (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(),
|
||||
)
|
||||
})?;
|
||||
let texture_rect = 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: image_size,
|
||||
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<AssetEvent<Image>>,
|
||||
images: Res<Assets<Image>>,
|
||||
ui_nodes: Query<(Entity, &ImageScaleMode, &Node, &UiImage)>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
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<Assets<Image>>,
|
||||
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
||||
changed_nodes: Query<
|
||||
(Entity, &ImageScaleMode, &Node, &UiImage),
|
||||
Or<(Changed<ImageScaleMode>, Changed<UiImage>, Changed<Node>)>,
|
||||
(
|
||||
Entity,
|
||||
&ImageScaleMode,
|
||||
&Node,
|
||||
&UiImage,
|
||||
Option<&TextureAtlas>,
|
||||
),
|
||||
Or<(
|
||||
Changed<ImageScaleMode>,
|
||||
Changed<UiImage>,
|
||||
Changed<Node>,
|
||||
Changed<TextureAtlas>,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
115
examples/ui/ui_texture_atlas_slice.rs
Normal file
115
examples/ui/ui_texture_atlas_slice.rs
Normal file
|
@ -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<Interaction>, With<Button>),
|
||||
>,
|
||||
mut text_query: Query<&mut Text>,
|
||||
) {
|
||||
for (interaction, mut atlas, children, mut image) in &mut interaction_query {
|
||||
let mut text = text_query.get_mut(children[0]).unwrap();
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
text.sections[0].value = "Press".to_string();
|
||||
atlas.index = (atlas.index + 1) % 30;
|
||||
image.color = GOLD.into();
|
||||
}
|
||||
Interaction::Hovered => {
|
||||
text.sections[0].value = "Hover".to_string();
|
||||
image.color = ORANGE.into();
|
||||
}
|
||||
Interaction::None => {
|
||||
text.sections[0].value = "Button".to_string();
|
||||
image.color = Color::WHITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
|
||||
) {
|
||||
let texture_handle = asset_server.load("textures/fantasy_ui_borders/border_sheet.png");
|
||||
let atlas_layout = TextureAtlasLayout::from_grid(UVec2::new(50, 50), 6, 6, None, None);
|
||||
let atlas_layout_handle = texture_atlases.add(atlas_layout);
|
||||
|
||||
let slicer = TextureSlicer {
|
||||
border: BorderRect::square(22.0),
|
||||
center_scale_mode: SliceScaleMode::Stretch,
|
||||
sides_scale_mode: SliceScaleMode::Stretch,
|
||||
max_corner_scale: 1.0,
|
||||
};
|
||||
// ui camera
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
for (idx, [w, h]) in [
|
||||
(0, [150.0, 150.0]),
|
||||
(7, [300.0, 150.0]),
|
||||
(13, [150.0, 300.0]),
|
||||
] {
|
||||
parent
|
||||
.spawn((
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(w),
|
||||
height: Val::Px(h),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect::all(Val::Px(20.0)),
|
||||
..default()
|
||||
},
|
||||
image: texture_handle.clone().into(),
|
||||
..default()
|
||||
},
|
||||
ImageScaleMode::Sliced(slicer.clone()),
|
||||
TextureAtlas {
|
||||
index: idx,
|
||||
layout: atlas_layout_handle.clone(),
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Button",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 40.0,
|
||||
color: Color::srgb(0.9, 0.9, 0.9),
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//! This example illustrates how to create a button that has its image sliced
|
||||
//! This example illustrates how to create buttons with their textures sliced
|
||||
//! and kept in proportion instead of being stretched by the button dimensions
|
||||
|
||||
use bevy::{
|
||||
|
|
Loading…
Reference in a new issue