UI texture atlas support (#8822)

# Objective

This adds support for using texture atlas sprites in UI. From
discussions today in the ui-dev discord it seems this is a much wanted
feature.

This was previously attempted in #5070 by @ManevilleF however that was
blocked #5103. This work can be easily modified to support #5103 changes
after that merges.

## Solution

I created a new UI bundle that reuses the existing texture atlas
infrastructure. I create a new atlas image component to prevent it from
being drawn by the existing non-UI systems and to remove unused
parameters.

In extract I added new system to calculate the required values for the
texture atlas image, this extracts into the same resource as the
existing UI Image and Text components.

This should have minimal performance impact because if texture atlas is
not present then the exact same code path is followed. Also there should
be no unintended behavior changes because without the new components the
existing systems write the extract same resulting data.

I also added an example showing the sprite working and a system to
advance the animation on space bar presses.

Naming is hard and I would accept any feedback on the bundle name! 

---

## Changelog

>  Added TextureAtlasImageBundle

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
This commit is contained in:
mwbryant 2023-06-19 15:52:02 -06:00 committed by GitHub
parent 7fc6db32ce
commit 8b5bf42c28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 298 additions and 15 deletions

View file

@ -1943,6 +1943,16 @@ description = "Illustrates how to scale the UI"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true wasm = true
[[example]]
name = "ui_texture_atlas"
path = "examples/ui/ui_texture_atlas.rs"
[package.metadata.example.ui_texture_atlas]
name = "UI Texture Atlas"
description = "Illustrates how to use TextureAtlases in UI"
category = "UI (User Interface)"
wasm = true
[[example]] [[example]]
name = "viewport_debug" name = "viewport_debug"
path = "examples/ui/viewport_debug.rs" path = "examples/ui/viewport_debug.rs"

View file

@ -157,8 +157,12 @@ impl Plugin for UiPlugin {
.ambiguous_with(widget::text_system); .ambiguous_with(widget::text_system);
system system
}) });
.add_systems( app.add_systems(
PostUpdate,
widget::update_atlas_content_size_system.before(UiSystem::Layout),
);
app.add_systems(
PostUpdate, PostUpdate,
( (
ui_layout_system ui_layout_system

View file

@ -3,13 +3,15 @@
use crate::{ use crate::{
widget::{Button, TextFlags, UiImageSize}, widget::{Button, TextFlags, UiImageSize},
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
ZIndex, UiTextureAtlasImage, ZIndex,
}; };
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
use bevy_render::{ use bevy_render::{
prelude::{Color, ComputedVisibility}, prelude::{Color, ComputedVisibility},
view::Visibility, view::Visibility,
}; };
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle}; use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_transform::prelude::{GlobalTransform, Transform};
@ -109,6 +111,51 @@ pub struct ImageBundle {
pub z_index: ZIndex, pub z_index: ZIndex,
} }
/// A UI node that is a texture atlas sprite
#[derive(Bundle, Debug, Default)]
pub struct AtlasImageBundle {
/// Describes the logical size of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub node: Node,
/// Styles which control the layout (size and position) of the node and it's children
/// In some cases these styles also affect how the node drawn/painted.
pub style: Style,
/// The calculated size based on the given image
pub calculated_size: ContentSize,
/// The background color, which serves as a "fill" for this node
///
/// Combines with `UiImage` to tint the provided image.
pub background_color: BackgroundColor,
/// A handle to the texture atlas to use for this Ui Node
pub texture_atlas: Handle<TextureAtlas>,
/// The descriptor for which sprite to use from the given texture atlas
pub texture_atlas_image: UiTextureAtlasImage,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The size of the image in pixels
///
/// This field is set automatically
pub image_size: UiImageSize,
/// The transform of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub transform: Transform,
/// The global transform of the node
///
/// This field is automatically managed by the UI layout system.
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
pub global_transform: GlobalTransform,
/// Describes the visibility properties of the node
pub visibility: Visibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
pub computed_visibility: ComputedVisibility,
/// Indicates the depth at which the node should appear in the UI
pub z_index: ZIndex,
}
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
/// A UI node that is text /// A UI node that is text
#[derive(Bundle, Debug)] #[derive(Bundle, Debug)]

View file

@ -9,6 +9,7 @@ use bevy_window::{PrimaryWindow, Window};
pub use pipeline::*; pub use pipeline::*;
pub use render_pass::*; pub use render_pass::*;
use crate::UiTextureAtlasImage;
use crate::{ use crate::{
prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack, prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack,
}; };
@ -82,6 +83,7 @@ pub fn build_ui_render(app: &mut App) {
extract_default_ui_camera_view::<Camera2d>, extract_default_ui_camera_view::<Camera2d>,
extract_default_ui_camera_view::<Camera3d>, extract_default_ui_camera_view::<Camera3d>,
extract_uinodes.in_set(RenderUiSystem::ExtractNode), extract_uinodes.in_set(RenderUiSystem::ExtractNode),
extract_atlas_uinodes.after(RenderUiSystem::ExtractNode),
extract_uinode_borders.after(RenderUiSystem::ExtractNode), extract_uinode_borders.after(RenderUiSystem::ExtractNode),
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
extract_text_uinodes.after(RenderUiSystem::ExtractNode), extract_text_uinodes.after(RenderUiSystem::ExtractNode),
@ -166,6 +168,83 @@ pub struct ExtractedUiNodes {
pub uinodes: Vec<ExtractedUiNode>, pub uinodes: Vec<ExtractedUiNode>,
} }
pub fn extract_atlas_uinodes(
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
images: Extract<Res<Assets<Image>>>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
ui_stack: Extract<Res<UiStack>>,
uinode_query: Extract<
Query<
(
&Node,
&GlobalTransform,
&BackgroundColor,
&ComputedVisibility,
Option<&CalculatedClip>,
&Handle<TextureAtlas>,
&UiTextureAtlasImage,
),
Without<UiImage>,
>,
>,
) {
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok((uinode, transform, color, visibility, clip, texture_atlas_handle, atlas_image)) =
uinode_query.get(*entity)
{
// Skip invisible and completely transparent nodes
if !visibility.is_visible() || color.0.a() == 0.0 {
continue;
}
let (mut atlas_rect, mut atlas_size, image) =
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
let atlas_rect = *texture_atlas
.textures
.get(atlas_image.index)
.unwrap_or_else(|| {
panic!(
"Atlas index {:?} does not exist for texture atlas handle {:?}.",
atlas_image.index,
texture_atlas_handle.id(),
)
});
(
atlas_rect,
texture_atlas.size,
texture_atlas.texture.clone(),
)
} else {
// Atlas not present in assets resource (should this warn the user?)
continue;
};
// Skip loading images
if !images.contains(&image) {
continue;
}
let scale = uinode.size() / atlas_rect.size();
atlas_rect.min *= scale;
atlas_rect.max *= scale;
atlas_size *= scale;
extracted_uinodes.uinodes.push(ExtractedUiNode {
stack_index,
transform: transform.compute_matrix(),
color: color.0,
rect: atlas_rect,
clip: clip.map(|clip| clip.clip),
image,
atlas_size: Some(atlas_size),
flip_x: atlas_image.flip_x,
flip_y: atlas_image.flip_y,
});
}
}
}
fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
match value { match value {
Val::Auto => 0., Val::Auto => 0.,
@ -288,14 +367,17 @@ pub fn extract_uinodes(
images: Extract<Res<Assets<Image>>>, images: Extract<Res<Assets<Image>>>,
ui_stack: Extract<Res<UiStack>>, ui_stack: Extract<Res<UiStack>>,
uinode_query: Extract< uinode_query: Extract<
Query<( Query<
&Node, (
&GlobalTransform, &Node,
&BackgroundColor, &GlobalTransform,
Option<&UiImage>, &BackgroundColor,
&ComputedVisibility, Option<&UiImage>,
Option<&CalculatedClip>, &ComputedVisibility,
)>, Option<&CalculatedClip>,
),
Without<UiTextureAtlasImage>,
>,
>, >,
) { ) {
extracted_uinodes.uinodes.clear(); extracted_uinodes.uinodes.clear();
@ -327,13 +409,13 @@ pub fn extract_uinodes(
min: Vec2::ZERO, min: Vec2::ZERO,
max: uinode.calculated_size, max: uinode.calculated_size,
}, },
clip: clip.map(|clip| clip.clip),
image, image,
atlas_size: None, atlas_size: None,
clip: clip.map(|clip| clip.clip),
flip_x, flip_x,
flip_y, flip_y,
}); });
} };
} }
} }

View file

@ -1563,6 +1563,18 @@ impl From<Color> for BackgroundColor {
} }
} }
/// The atlas sprite to be used in a UI Texture Atlas Node
#[derive(Component, Clone, Debug, Reflect, FromReflect, Default)]
#[reflect(Component, Default)]
pub struct UiTextureAtlasImage {
/// Texture index in the TextureAtlas
pub index: usize,
/// Whether to flip the sprite in the X axis
pub flip_x: bool,
/// Whether to flip the sprite in the Y axis
pub flip_y: bool,
}
/// The border color of the UI node. /// The border color of the UI node.
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)] #[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
#[reflect(FromReflect, Component, Default)] #[reflect(FromReflect, Component, Default)]

View file

@ -1,5 +1,7 @@
use crate::{measurement::AvailableSpace, ContentSize, Measure, Node, UiImage}; use crate::{
use bevy_asset::Assets; measurement::AvailableSpace, ContentSize, Measure, Node, UiImage, UiTextureAtlasImage,
};
use bevy_asset::{Assets, Handle};
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_ecs::query::Without; use bevy_ecs::query::Without;
use bevy_ecs::{ use bevy_ecs::{
@ -11,6 +13,7 @@ use bevy_ecs::{
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect}; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect};
use bevy_render::texture::Image; use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_text::Text; use bevy_text::Text;
@ -89,3 +92,41 @@ pub fn update_image_content_size_system(
} }
} }
} }
/// Updates content size of the node based on the texture atlas sprite
pub fn update_atlas_content_size_system(
atlases: Res<Assets<TextureAtlas>>,
#[cfg(feature = "bevy_text")] mut atlas_query: Query<
(
&mut ContentSize,
&Handle<TextureAtlas>,
&UiTextureAtlasImage,
&mut UiImageSize,
),
(With<Node>, Without<Text>, Without<UiImage>),
>,
#[cfg(not(feature = "bevy_text"))] mut atlas_query: Query<
(
&mut ContentSize,
&Handle<TextureAtlas>,
&UiTextureAtlasImage,
&mut UiImageSize,
),
(With<Node>, Without<UiImage>),
>,
) {
for (mut content_size, atlas, atlas_image, mut image_size) in &mut atlas_query {
if let Some(atlas) = atlases.get(atlas) {
let texture_rect = atlas.textures[atlas_image.index];
let size = Vec2::new(
texture_rect.max.x - texture_rect.min.x,
texture_rect.max.y - texture_rect.min.y,
);
// Update only if size has changed to avoid needless layout calculations
if size != image_size.size {
image_size.size = size;
content_size.set(ImageMeasure { size });
}
}
}
}

View file

@ -351,6 +351,7 @@ Example | Description
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI [UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI [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 Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [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 [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.

View file

@ -0,0 +1,86 @@
//! This example illustrates how to use `TextureAtlases` within ui
use bevy::{prelude::*, winit::WinitSettings};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(
// This sets image filtering to nearest
// This is done to prevent textures with low resolution (e.g. pixel art) from being blurred
// by linear filtering.
ImagePlugin::default_nearest(),
))
// 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, increment_atlas_index)
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
// Camera
commands.spawn(Camera2dBundle::default());
let text_style = TextStyle {
color: Color::ANTIQUE_WHITE,
font_size: 20.,
..default()
};
let texture_handle = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
let texture_atlas =
TextureAtlas::from_grid(texture_handle, Vec2::new(24.0, 24.0), 7, 1, None, None);
let texture_atlas_handle = texture_atlases.add(texture_atlas);
// root node
commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
row_gap: Val::Px(text_style.font_size * 2.),
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn((AtlasImageBundle {
style: Style {
width: Val::Px(256.),
height: Val::Px(256.),
..default()
},
texture_atlas: texture_atlas_handle,
texture_atlas_image: UiTextureAtlasImage::default(),
..default()
},));
parent.spawn(TextBundle::from_sections([
TextSection::new("press ".to_string(), text_style.clone()),
TextSection::new(
"space".to_string(),
TextStyle {
color: Color::YELLOW,
..text_style.clone()
},
),
TextSection::new(" to advance frames".to_string(), text_style),
]));
});
}
fn increment_atlas_index(
mut atlas_images: Query<&mut UiTextureAtlasImage>,
keyboard: Res<Input<KeyCode>>,
) {
if keyboard.just_pressed(KeyCode::Space) {
for mut atlas_image in &mut atlas_images {
atlas_image.index = (atlas_image.index + 1) % 6;
}
}
}