Skip to content

Commit

Permalink
UI Texture 9 slice (#11600)
Browse files Browse the repository at this point in the history
> Follow up to #10588 
> Closes #11749 (Supersedes #11756)

Enable Texture slicing for the following UI nodes:
- `ImageBundle`
- `ButtonBundle`

<img width="739" alt="Screenshot 2024-01-29 at 13 57 43"
src="https://github.com/bevyengine/bevy/assets/26703856/37675681-74eb-4689-ab42-024310cf3134">

I also added a collection of `fantazy-ui-borders` from
[Kenney's](www.kenney.nl) assets, with the appropriate license (CC).
If it's a problem I can use the same textures as the `sprite_slice`
example

# Work done

Added the `ImageScaleMode` component to the targetted bundles, most of
the logic is directly reused from `bevy_sprite`.
The only additional internal component is the UI specific
`ComputedSlices`, which does the same thing as its spritee equivalent
but adapted to UI code.

Again the slicing is not compatible with `TextureAtlas`, it's something
I need to tackle more deeply in the future

# Fixes

* [x] I noticed that `TextureSlicer::compute_slices` could infinitely
loop if the border was larger that the image half extents, now an error
is triggered and the texture will fallback to being stretched
* [x] I noticed that when using small textures with very small *tiling*
options we could generate hundred of thousands of slices. Now I set a
minimum size of 1 pixel per slice, which is already ridiculously small,
and a warning will be sent at runtime when slice count goes above 1000
* [x] Sprite slicing with `flip_x` or `flip_y` would give incorrect
results, correct flipping is now supported to both sprites and ui image
nodes thanks to @odecay observation

# GPU Alternative

I create a separate branch attempting to implementing 9 slicing and
tiling directly through the `ui.wgsl` fragment shader. It works but
requires sending more data to the GPU:
- slice border
- tiling factors

And more importantly, the actual quad *scale* which is hard to put in
the shader with the current code, so that would be for a later iteration
  • Loading branch information
ManevilleF authored Feb 7, 2024
1 parent ff77adc commit ab16f5e
Show file tree
Hide file tree
Showing 17 changed files with 414 additions and 30 deletions.
1 change: 1 addition & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* Ground tile from [Kenney's Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit) (CC0 1.0 Universal)
* Game icons from [Kenney's Game Icons](https://www.kenney.nl/assets/game-icons) (CC0 1.0 Universal)
* Space ships from [Kenny's Simple Space Kit](https://www.kenney.nl/assets/simple-space) (CC0 1.0 Universal)
* UI borders from [Kenny's Fantasy UI Borders Kit](https://kenney.nl/assets/fantasy-ui-borders) (CC0 1.0 Universal)
* glTF animated fox from [glTF Sample Models][fox]
* Low poly fox [by PixelMannen] (CC0 1.0 Universal)
* Rigging and animation [by @tomkranis on Sketchfab] ([CC-BY 4.0])
Expand Down
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,17 @@ description = "Illustrates how to use TextureAtlases in UI"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "ui_texture_slice"
path = "examples/ui/ui_texture_slice.rs"
doc-scrape-examples = true

[package.metadata.example.ui_texture_slice]
name = "UI Texture Slice"
description = "Illustrates how to use 9 Slicing in UI"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "viewport_debug"
path = "examples/ui/viewport_debug.rs"
Expand Down
30 changes: 30 additions & 0 deletions assets/textures/fantasy_ui_borders/License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@


Fantasy UI Borders (1.0)

Created/distributed by Kenney (www.kenney.nl)
Creation date: 03-12-2023

For the sample image the font 'Aoboshi One' was used, OPL (Open Font License)

------------------------------

License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/

You can use this content for personal, educational, and commercial purposes.

Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)

------------------------------

• Website : www.kenney.nl
• Donate : www.kenney.nl/donate

• Patreon : patreon.com/kenney

Follow on social media for updates:

• Twitter: twitter.com/KenneyNL
• Instagram: instagram.com/kenney_nl
• Mastodon: mastodon.gamedev.place/@kenney
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,21 @@ pub struct SpriteBundle {
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Bundle, Clone, Default)]
pub struct SpriteSheetBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
pub global_transform: GlobalTransform,
/// The sprite sheet base texture
pub texture: Handle<Image>,
/// The sprite sheet texture atlas, allowing to draw a custom section of `texture`.
pub atlas: TextureAtlas,
/// User indication of whether an entity is visible
pub visibility: Visibility,
/// Inherited visibility of an entity.
pub inherited_visibility: InheritedVisibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
pub view_visibility: ViewVisibility,
Expand Down
5 changes: 2 additions & 3 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub mod prelude {
bundle::{SpriteBundle, SpriteSheetBundle},
sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasLayout},
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
};
}
Expand Down Expand Up @@ -124,7 +124,6 @@ impl Plugin for SpritePlugin {
/// System calculating and inserting an [`Aabb`] component to entities with either:
/// - a `Mesh2dHandle` component,
/// - a `Sprite` and `Handle<Image>` components,
/// - a `TextureAtlasSprite` and `Handle<TextureAtlas>` components,
/// and without a [`NoFrustumCulling`] component.
///
/// Used in system set [`VisibilitySystems::CalculateBounds`].
Expand All @@ -137,7 +136,7 @@ pub fn calculate_bounds_2d(
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
Or<(Without<Aabb>, Changed<Sprite>, Changed<TextureAtlas>)>,
Without<NoFrustumCulling>,
),
>,
Expand Down
18 changes: 14 additions & 4 deletions crates/bevy_sprite/src/texture_slice/computed_slices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,27 @@ impl ComputedTextureSlices {
sprite: &'a Sprite,
handle: &'a Handle<Image>,
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a {
let mut flip = Vec2::ONE;
let [mut flip_x, mut flip_y] = [false; 2];
if sprite.flip_x {
flip.x *= -1.0;
flip_x = true;
}
if sprite.flip_y {
flip.y *= -1.0;
flip_y = true;
}
self.0.iter().map(move |slice| {
let transform =
transform.mul_transform(Transform::from_translation(slice.offset.extend(0.0)));
let offset = (slice.offset * flip).extend(0.0);
let transform = transform.mul_transform(Transform::from_translation(offset));
ExtractedSprite {
original_entity: Some(original_entity),
color: sprite.color,
transform,
rect: Some(slice.texture_rect),
custom_size: Some(slice.draw_size),
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
flip_x,
flip_y,
image_handle_id: handle.id(),
anchor: sprite.anchor.as_vec(),
}
Expand Down
15 changes: 11 additions & 4 deletions crates/bevy_sprite/src/texture_slice/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ pub(crate) use computed_slices::{
compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices,
};

/// Single texture slice, representing a texture rect to draw in a given area
#[derive(Debug, Clone)]
pub(crate) struct TextureSlice {
pub struct TextureSlice {
/// texture area to draw
pub texture_rect: Rect,
/// slice draw size
Expand Down Expand Up @@ -39,16 +40,19 @@ impl TextureSlice {
// Each tile expected size
let expected_size = Vec2::new(
if tile_x {
rect_size.x * stretch_value
// No slice should be less than 1 pixel wide
(rect_size.x * stretch_value).max(1.0)
} else {
self.draw_size.x
},
if tile_y {
rect_size.y * stretch_value
// No slice should be less than 1 pixel high
(rect_size.y * stretch_value).max(1.0)
} else {
self.draw_size.y
},
);
)
.min(self.draw_size);
let mut slices = Vec::new();
let base_offset = Vec2::new(
-self.draw_size.x / 2.0,
Expand Down Expand Up @@ -81,6 +85,9 @@ impl TextureSlice {
offset.y -= size_y / 2.0;
remaining_columns -= size_y;
}
if slices.len() > 1_000 {
bevy_log::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len());
}
slices
}
}
21 changes: 16 additions & 5 deletions crates/bevy_sprite/src/texture_slice/slicer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,23 @@ 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.
#[must_use]
pub(crate) fn compute_slices(
&self,
rect: Rect,
render_size: Option<Vec2>,
) -> Vec<TextureSlice> {
pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {
let render_size = render_size.unwrap_or_else(|| rect.size());
let rect_size = rect.size() / 2.0;
if self.border.left >= rect_size.x
|| self.border.right >= rect_size.x
|| self.border.top >= rect_size.y
|| self.border.bottom >= rect_size.y
{
bevy_log::error!(
"TextureSlicer::border has out of bounds values. No slicing will be applied"
);
return vec![TextureSlice {
texture_rect: rect,
draw_size: render_size,
offset: Vec2::ZERO,
}];
}
let mut slices = Vec::with_capacity(9);
// Corners
let corners = self.corner_slices(rect, render_size);
Expand Down
19 changes: 15 additions & 4 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod geometry;
mod layout;
mod render;
mod stack;
mod texture_slice;
mod ui_node;

pub use focus::*;
Expand All @@ -39,6 +40,9 @@ pub mod prelude {
geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button, widget::Label,
Interaction, UiMaterialPlugin, UiScale,
};
// `bevy_sprite` re-exports for texture slicing
#[doc(hidden)]
pub use bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer};
}

use bevy_app::prelude::*;
Expand Down Expand Up @@ -162,10 +166,17 @@ impl Plugin for UiPlugin {
// They run independently since `widget::image_node_system` will only ever observe
// its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout`
// will never modify a pre-existing `Image` asset.
widget::update_image_content_size_system
.before(UiSystem::Layout)
.in_set(AmbiguousWithTextSystem)
.in_set(AmbiguousWithUpdateText2DLayout),
(
widget::update_image_content_size_system
.before(UiSystem::Layout)
.in_set(AmbiguousWithTextSystem)
.in_set(AmbiguousWithUpdateText2DLayout),
(
texture_slice::compute_slices_on_asset_event,
texture_slice::compute_slices_on_image_change,
),
)
.chain(),
),
);

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 @@ -13,7 +13,7 @@ use bevy_render::{
prelude::Color,
view::{InheritedVisibility, ViewVisibility, Visibility},
};
use bevy_sprite::TextureAtlas;
use bevy_sprite::{ImageScaleMode, TextureAtlas};
#[cfg(feature = "bevy_text")]
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform};
Expand Down Expand Up @@ -95,6 +95,8 @@ pub struct ImageBundle {
///
/// This component is set automatically
pub image_size: UiImageSize,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The transform of the node
Expand Down Expand Up @@ -307,6 +309,8 @@ pub struct ButtonBundle {
pub border_color: BorderColor,
/// The image of the node
pub image: UiImage,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
Expand Down Expand Up @@ -343,6 +347,7 @@ impl Default for ButtonBundle {
inherited_visibility: Default::default(),
view_visibility: Default::default(),
z_index: Default::default(),
scale_mode: ImageScaleMode::default(),
}
}
}
Expand Down
36 changes: 28 additions & 8 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ pub use ui_material_pipeline::*;

use crate::graph::{LabelsUi, SubGraphUi};
use crate::{
BackgroundColor, BorderColor, CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline,
Style, TargetCamera, UiImage, UiScale, Val,
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip,
ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val,
};

use bevy_app::prelude::*;
Expand Down Expand Up @@ -62,7 +62,6 @@ pub const UI_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(130128470471
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum RenderUiSystem {
ExtractNode,
ExtractAtlasNode,
}

pub fn build_ui_render(app: &mut App) {
Expand All @@ -86,10 +85,10 @@ 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_uinode_borders.after(RenderUiSystem::ExtractAtlasNode),
extract_uinode_borders,
#[cfg(feature = "bevy_text")]
extract_text_uinodes.after(RenderUiSystem::ExtractAtlasNode),
extract_uinode_outlines.after(RenderUiSystem::ExtractAtlasNode),
extract_text_uinodes,
extract_uinode_outlines,
),
)
.add_systems(
Expand Down Expand Up @@ -377,6 +376,7 @@ pub fn extract_uinode_outlines(
}

pub fn extract_uinodes(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
default_ui_camera: Extract<DefaultUiCamera>,
Expand All @@ -391,11 +391,22 @@ pub fn extract_uinodes(
Option<&CalculatedClip>,
Option<&TextureAtlas>,
Option<&TargetCamera>,
Option<&ComputedTextureSlices>,
)>,
>,
) {
for (entity, uinode, transform, color, maybe_image, view_visibility, clip, atlas, camera) in
uinode_query.iter()
for (
entity,
uinode,
transform,
color,
maybe_image,
view_visibility,
clip,
atlas,
camera,
slices,
) in uinode_query.iter()
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
Expand All @@ -406,6 +417,15 @@ pub fn extract_uinodes(
continue;
}

if let Some((image, slices)) = maybe_image.zip(slices) {
extracted_uinodes.uinodes.extend(
slices
.extract_ui_nodes(transform, uinode, color, image, clip, camera_entity)
.map(|e| (commands.spawn_empty().id(), e)),
);
continue;
}

let (image, flip_x, flip_y) = if let Some(image) = maybe_image {
(image.texture.id(), image.flip_x, image.flip_y)
} else {
Expand Down
Loading

0 comments on commit ab16f5e

Please sign in to comment.