Skip to content

Commit

Permalink
Slicing support for texture atlas (#12059)
Browse files Browse the repository at this point in the history
# Objective

Follow up to #11600 and #10588 
#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 <[email protected]>
Co-authored-by: Pablo Reinhardt <[email protected]>
  • Loading branch information
3 people authored Mar 5, 2024
1 parent dc40cd1 commit fc202f2
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 47 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
95 changes: 71 additions & 24 deletions crates/bevy_sprite/src/texture_slice/computed_slices.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 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,
};
Expand All @@ -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
Expand All @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_sprite/src/texture_slice/slicer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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());
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)]
Expand Down
92 changes: 74 additions & 18 deletions crates/bevy_ui/src/texture_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 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)),
Expand All @@ -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
Expand All @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit fc202f2

Please sign in to comment.