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

UI Texture 9 slice #11600

Merged
merged 16 commits into from
Feb 7, 2024
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:/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>)>,
mockersf marked this conversation as resolved.
Show resolved Hide resolved
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
Loading