Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slicing support for texture atlas #12059

Merged
merged 13 commits into from
Mar 5, 2024
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2475,6 +2475,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 altas layout assets, used to retrieve the texture atlas section rect
#[must_use]
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
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
6 changes: 5 additions & 1 deletion crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ pub struct ImageBundle {

/// A UI node that is a texture atlas sprite
///
/// This bundle is identical to [`ImageBundle`] with an additional [`TextureAtlas`] component.
/// # 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
#[derive(Bundle, Debug, Default)]
pub struct AtlasImageBundle {
/// Describes the logical size of the node
Expand Down Expand Up @@ -296,6 +299,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 section of the texture
#[derive(Bundle, Clone, Debug)]
pub struct ButtonBundle {
/// Describes the logical size of the node
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 @@ -75,25 +75,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 altas layout assets, used to retrieve the texture atlas section rect
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
#[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 @@ -119,7 +142,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 @@ -133,11 +163,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 @@ -148,13 +185,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 @@ -401,6 +401,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
Loading