mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
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:
parent
7fc6db32ce
commit
8b5bf42c28
8 changed files with 298 additions and 15 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1943,6 +1943,16 @@ description = "Illustrates how to scale the UI"
|
|||
category = "UI (User Interface)"
|
||||
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]]
|
||||
name = "viewport_debug"
|
||||
path = "examples/ui/viewport_debug.rs"
|
||||
|
|
|
@ -157,8 +157,12 @@ impl Plugin for UiPlugin {
|
|||
.ambiguous_with(widget::text_system);
|
||||
|
||||
system
|
||||
})
|
||||
.add_systems(
|
||||
});
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
widget::update_atlas_content_size_system.before(UiSystem::Layout),
|
||||
);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
ui_layout_system
|
||||
|
|
|
@ -3,13 +3,15 @@
|
|||
use crate::{
|
||||
widget::{Button, TextFlags, UiImageSize},
|
||||
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
|
||||
ZIndex,
|
||||
UiTextureAtlasImage, ZIndex,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_ecs::bundle::Bundle;
|
||||
use bevy_render::{
|
||||
prelude::{Color, ComputedVisibility},
|
||||
view::Visibility,
|
||||
};
|
||||
use bevy_sprite::TextureAtlas;
|
||||
#[cfg(feature = "bevy_text")]
|
||||
use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle};
|
||||
use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||
|
@ -109,6 +111,51 @@ pub struct ImageBundle {
|
|||
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")]
|
||||
/// A UI node that is text
|
||||
#[derive(Bundle, Debug)]
|
||||
|
|
|
@ -9,6 +9,7 @@ use bevy_window::{PrimaryWindow, Window};
|
|||
pub use pipeline::*;
|
||||
pub use render_pass::*;
|
||||
|
||||
use crate::UiTextureAtlasImage;
|
||||
use crate::{
|
||||
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::<Camera3d>,
|
||||
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
|
||||
extract_atlas_uinodes.after(RenderUiSystem::ExtractNode),
|
||||
extract_uinode_borders.after(RenderUiSystem::ExtractNode),
|
||||
#[cfg(feature = "bevy_text")]
|
||||
extract_text_uinodes.after(RenderUiSystem::ExtractNode),
|
||||
|
@ -166,6 +168,83 @@ pub struct ExtractedUiNodes {
|
|||
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 {
|
||||
match value {
|
||||
Val::Auto => 0.,
|
||||
|
@ -288,14 +367,17 @@ pub fn extract_uinodes(
|
|||
images: Extract<Res<Assets<Image>>>,
|
||||
ui_stack: Extract<Res<UiStack>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
&Node,
|
||||
&GlobalTransform,
|
||||
&BackgroundColor,
|
||||
Option<&UiImage>,
|
||||
&ComputedVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
)>,
|
||||
Query<
|
||||
(
|
||||
&Node,
|
||||
&GlobalTransform,
|
||||
&BackgroundColor,
|
||||
Option<&UiImage>,
|
||||
&ComputedVisibility,
|
||||
Option<&CalculatedClip>,
|
||||
),
|
||||
Without<UiTextureAtlasImage>,
|
||||
>,
|
||||
>,
|
||||
) {
|
||||
extracted_uinodes.uinodes.clear();
|
||||
|
@ -327,13 +409,13 @@ pub fn extract_uinodes(
|
|||
min: Vec2::ZERO,
|
||||
max: uinode.calculated_size,
|
||||
},
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
image,
|
||||
atlas_size: None,
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
flip_x,
|
||||
flip_y,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
|
||||
#[reflect(FromReflect, Component, Default)]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::{measurement::AvailableSpace, ContentSize, Measure, Node, UiImage};
|
||||
use bevy_asset::Assets;
|
||||
use crate::{
|
||||
measurement::AvailableSpace, ContentSize, Measure, Node, UiImage, UiTextureAtlasImage,
|
||||
};
|
||||
use bevy_asset::{Assets, Handle};
|
||||
#[cfg(feature = "bevy_text")]
|
||||
use bevy_ecs::query::Without;
|
||||
use bevy_ecs::{
|
||||
|
@ -11,6 +13,7 @@ use bevy_ecs::{
|
|||
use bevy_math::Vec2;
|
||||
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect};
|
||||
use bevy_render::texture::Image;
|
||||
use bevy_sprite::TextureAtlas;
|
||||
#[cfg(feature = "bevy_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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -351,6 +351,7 @@ Example | Description
|
|||
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for 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 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
|
||||
[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.
|
||||
|
|
86
examples/ui/ui_texture_atlas.rs
Normal file
86
examples/ui/ui_texture_atlas.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue