Skip to content

Commit 8b5bf42

Browse files
mwbryantickshonpe
andauthored
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 <[email protected]>
1 parent 7fc6db3 commit 8b5bf42

File tree

8 files changed

+298
-15
lines changed

8 files changed

+298
-15
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,16 @@ description = "Illustrates how to scale the UI"
19431943
category = "UI (User Interface)"
19441944
wasm = true
19451945

1946+
[[example]]
1947+
name = "ui_texture_atlas"
1948+
path = "examples/ui/ui_texture_atlas.rs"
1949+
1950+
[package.metadata.example.ui_texture_atlas]
1951+
name = "UI Texture Atlas"
1952+
description = "Illustrates how to use TextureAtlases in UI"
1953+
category = "UI (User Interface)"
1954+
wasm = true
1955+
19461956
[[example]]
19471957
name = "viewport_debug"
19481958
path = "examples/ui/viewport_debug.rs"

crates/bevy_ui/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ impl Plugin for UiPlugin {
157157
.ambiguous_with(widget::text_system);
158158

159159
system
160-
})
161-
.add_systems(
160+
});
161+
app.add_systems(
162+
PostUpdate,
163+
widget::update_atlas_content_size_system.before(UiSystem::Layout),
164+
);
165+
app.add_systems(
162166
PostUpdate,
163167
(
164168
ui_layout_system

crates/bevy_ui/src/node_bundles.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
use crate::{
44
widget::{Button, TextFlags, UiImageSize},
55
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
6-
ZIndex,
6+
UiTextureAtlasImage, ZIndex,
77
};
8+
use bevy_asset::Handle;
89
use bevy_ecs::bundle::Bundle;
910
use bevy_render::{
1011
prelude::{Color, ComputedVisibility},
1112
view::Visibility,
1213
};
14+
use bevy_sprite::TextureAtlas;
1315
#[cfg(feature = "bevy_text")]
1416
use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle};
1517
use bevy_transform::prelude::{GlobalTransform, Transform};
@@ -109,6 +111,51 @@ pub struct ImageBundle {
109111
pub z_index: ZIndex,
110112
}
111113

114+
/// A UI node that is a texture atlas sprite
115+
#[derive(Bundle, Debug, Default)]
116+
pub struct AtlasImageBundle {
117+
/// Describes the logical size of the node
118+
///
119+
/// This field is automatically managed by the UI layout system.
120+
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
121+
pub node: Node,
122+
/// Styles which control the layout (size and position) of the node and it's children
123+
/// In some cases these styles also affect how the node drawn/painted.
124+
pub style: Style,
125+
/// The calculated size based on the given image
126+
pub calculated_size: ContentSize,
127+
/// The background color, which serves as a "fill" for this node
128+
///
129+
/// Combines with `UiImage` to tint the provided image.
130+
pub background_color: BackgroundColor,
131+
/// A handle to the texture atlas to use for this Ui Node
132+
pub texture_atlas: Handle<TextureAtlas>,
133+
/// The descriptor for which sprite to use from the given texture atlas
134+
pub texture_atlas_image: UiTextureAtlasImage,
135+
/// Whether this node should block interaction with lower nodes
136+
pub focus_policy: FocusPolicy,
137+
/// The size of the image in pixels
138+
///
139+
/// This field is set automatically
140+
pub image_size: UiImageSize,
141+
/// The transform of the node
142+
///
143+
/// This field is automatically managed by the UI layout system.
144+
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
145+
pub transform: Transform,
146+
/// The global transform of the node
147+
///
148+
/// This field is automatically managed by the UI layout system.
149+
/// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
150+
pub global_transform: GlobalTransform,
151+
/// Describes the visibility properties of the node
152+
pub visibility: Visibility,
153+
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
154+
pub computed_visibility: ComputedVisibility,
155+
/// Indicates the depth at which the node should appear in the UI
156+
pub z_index: ZIndex,
157+
}
158+
112159
#[cfg(feature = "bevy_text")]
113160
/// A UI node that is text
114161
#[derive(Bundle, Debug)]

crates/bevy_ui/src/render/mod.rs

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bevy_window::{PrimaryWindow, Window};
99
pub use pipeline::*;
1010
pub use render_pass::*;
1111

12+
use crate::UiTextureAtlasImage;
1213
use crate::{
1314
prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack,
1415
};
@@ -82,6 +83,7 @@ pub fn build_ui_render(app: &mut App) {
8283
extract_default_ui_camera_view::<Camera2d>,
8384
extract_default_ui_camera_view::<Camera3d>,
8485
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
86+
extract_atlas_uinodes.after(RenderUiSystem::ExtractNode),
8587
extract_uinode_borders.after(RenderUiSystem::ExtractNode),
8688
#[cfg(feature = "bevy_text")]
8789
extract_text_uinodes.after(RenderUiSystem::ExtractNode),
@@ -166,6 +168,83 @@ pub struct ExtractedUiNodes {
166168
pub uinodes: Vec<ExtractedUiNode>,
167169
}
168170

171+
pub fn extract_atlas_uinodes(
172+
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
173+
images: Extract<Res<Assets<Image>>>,
174+
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
175+
176+
ui_stack: Extract<Res<UiStack>>,
177+
uinode_query: Extract<
178+
Query<
179+
(
180+
&Node,
181+
&GlobalTransform,
182+
&BackgroundColor,
183+
&ComputedVisibility,
184+
Option<&CalculatedClip>,
185+
&Handle<TextureAtlas>,
186+
&UiTextureAtlasImage,
187+
),
188+
Without<UiImage>,
189+
>,
190+
>,
191+
) {
192+
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
193+
if let Ok((uinode, transform, color, visibility, clip, texture_atlas_handle, atlas_image)) =
194+
uinode_query.get(*entity)
195+
{
196+
// Skip invisible and completely transparent nodes
197+
if !visibility.is_visible() || color.0.a() == 0.0 {
198+
continue;
199+
}
200+
201+
let (mut atlas_rect, mut atlas_size, image) =
202+
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
203+
let atlas_rect = *texture_atlas
204+
.textures
205+
.get(atlas_image.index)
206+
.unwrap_or_else(|| {
207+
panic!(
208+
"Atlas index {:?} does not exist for texture atlas handle {:?}.",
209+
atlas_image.index,
210+
texture_atlas_handle.id(),
211+
)
212+
});
213+
(
214+
atlas_rect,
215+
texture_atlas.size,
216+
texture_atlas.texture.clone(),
217+
)
218+
} else {
219+
// Atlas not present in assets resource (should this warn the user?)
220+
continue;
221+
};
222+
223+
// Skip loading images
224+
if !images.contains(&image) {
225+
continue;
226+
}
227+
228+
let scale = uinode.size() / atlas_rect.size();
229+
atlas_rect.min *= scale;
230+
atlas_rect.max *= scale;
231+
atlas_size *= scale;
232+
233+
extracted_uinodes.uinodes.push(ExtractedUiNode {
234+
stack_index,
235+
transform: transform.compute_matrix(),
236+
color: color.0,
237+
rect: atlas_rect,
238+
clip: clip.map(|clip| clip.clip),
239+
image,
240+
atlas_size: Some(atlas_size),
241+
flip_x: atlas_image.flip_x,
242+
flip_y: atlas_image.flip_y,
243+
});
244+
}
245+
}
246+
}
247+
169248
fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
170249
match value {
171250
Val::Auto => 0.,
@@ -288,14 +367,17 @@ pub fn extract_uinodes(
288367
images: Extract<Res<Assets<Image>>>,
289368
ui_stack: Extract<Res<UiStack>>,
290369
uinode_query: Extract<
291-
Query<(
292-
&Node,
293-
&GlobalTransform,
294-
&BackgroundColor,
295-
Option<&UiImage>,
296-
&ComputedVisibility,
297-
Option<&CalculatedClip>,
298-
)>,
370+
Query<
371+
(
372+
&Node,
373+
&GlobalTransform,
374+
&BackgroundColor,
375+
Option<&UiImage>,
376+
&ComputedVisibility,
377+
Option<&CalculatedClip>,
378+
),
379+
Without<UiTextureAtlasImage>,
380+
>,
299381
>,
300382
) {
301383
extracted_uinodes.uinodes.clear();
@@ -327,13 +409,13 @@ pub fn extract_uinodes(
327409
min: Vec2::ZERO,
328410
max: uinode.calculated_size,
329411
},
412+
clip: clip.map(|clip| clip.clip),
330413
image,
331414
atlas_size: None,
332-
clip: clip.map(|clip| clip.clip),
333415
flip_x,
334416
flip_y,
335417
});
336-
}
418+
};
337419
}
338420
}
339421

crates/bevy_ui/src/ui_node.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,18 @@ impl From<Color> for BackgroundColor {
15631563
}
15641564
}
15651565

1566+
/// The atlas sprite to be used in a UI Texture Atlas Node
1567+
#[derive(Component, Clone, Debug, Reflect, FromReflect, Default)]
1568+
#[reflect(Component, Default)]
1569+
pub struct UiTextureAtlasImage {
1570+
/// Texture index in the TextureAtlas
1571+
pub index: usize,
1572+
/// Whether to flip the sprite in the X axis
1573+
pub flip_x: bool,
1574+
/// Whether to flip the sprite in the Y axis
1575+
pub flip_y: bool,
1576+
}
1577+
15661578
/// The border color of the UI node.
15671579
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
15681580
#[reflect(FromReflect, Component, Default)]

crates/bevy_ui/src/widget/image.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use crate::{measurement::AvailableSpace, ContentSize, Measure, Node, UiImage};
2-
use bevy_asset::Assets;
1+
use crate::{
2+
measurement::AvailableSpace, ContentSize, Measure, Node, UiImage, UiTextureAtlasImage,
3+
};
4+
use bevy_asset::{Assets, Handle};
35
#[cfg(feature = "bevy_text")]
46
use bevy_ecs::query::Without;
57
use bevy_ecs::{
@@ -11,6 +13,7 @@ use bevy_ecs::{
1113
use bevy_math::Vec2;
1214
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect};
1315
use bevy_render::texture::Image;
16+
use bevy_sprite::TextureAtlas;
1417
#[cfg(feature = "bevy_text")]
1518
use bevy_text::Text;
1619

@@ -89,3 +92,41 @@ pub fn update_image_content_size_system(
8992
}
9093
}
9194
}
95+
96+
/// Updates content size of the node based on the texture atlas sprite
97+
pub fn update_atlas_content_size_system(
98+
atlases: Res<Assets<TextureAtlas>>,
99+
#[cfg(feature = "bevy_text")] mut atlas_query: Query<
100+
(
101+
&mut ContentSize,
102+
&Handle<TextureAtlas>,
103+
&UiTextureAtlasImage,
104+
&mut UiImageSize,
105+
),
106+
(With<Node>, Without<Text>, Without<UiImage>),
107+
>,
108+
#[cfg(not(feature = "bevy_text"))] mut atlas_query: Query<
109+
(
110+
&mut ContentSize,
111+
&Handle<TextureAtlas>,
112+
&UiTextureAtlasImage,
113+
&mut UiImageSize,
114+
),
115+
(With<Node>, Without<UiImage>),
116+
>,
117+
) {
118+
for (mut content_size, atlas, atlas_image, mut image_size) in &mut atlas_query {
119+
if let Some(atlas) = atlases.get(atlas) {
120+
let texture_rect = atlas.textures[atlas_image.index];
121+
let size = Vec2::new(
122+
texture_rect.max.x - texture_rect.min.x,
123+
texture_rect.max.y - texture_rect.min.y,
124+
);
125+
// Update only if size has changed to avoid needless layout calculations
126+
if size != image_size.size {
127+
image_size.size = size;
128+
content_size.set(ImageMeasure { size });
129+
}
130+
}
131+
}
132+
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ Example | Description
351351
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
352352
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
353353
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
354+
[UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI
354355
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
355356
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
356357
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.

0 commit comments

Comments
 (0)