From 8ac745ab106b1bdb1158dbc2dfa1480587da0484 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 4 Sep 2024 20:31:41 +0100 Subject: [PATCH] UI texture slice texture flipping reimplementation (#15034) # Objective Fixes #15032 ## Solution Reimplement support for the `flip_x` and `flip_y` fields. This doesn't flip the border geometry, I'm not really sure whether that is desirable or not. Also fixes a bug that was causing the side and center slices to tile incorrectly. ### Testing ``` cargo run --example ui_texture_slice_flip_and_tile ``` ## Showcase nearest With tiling need to use nearest filtering to avoid bleeding between the slices. --------- Co-authored-by: Jan Hohenheim Co-authored-by: Alice Cecile --- Cargo.toml | 11 +++ .../fantasy_ui_borders/numbered_slices.png | Bin 0 -> 620 bytes .../src/render/ui_texture_slice_pipeline.rs | 79 ++++++++++++------ examples/README.md | 1 + examples/ui/ui_texture_slice_flip_and_tile.rs | 78 +++++++++++++++++ 5 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 assets/textures/fantasy_ui_borders/numbered_slices.png create mode 100644 examples/ui/ui_texture_slice_flip_and_tile.rs diff --git a/Cargo.toml b/Cargo.toml index 3708e1d45b..4112429b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2917,6 +2917,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_slice_flip_and_tile" +path = "examples/ui/ui_texture_slice_flip_and_tile.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_slice_flip_and_tile] +name = "UI Texture Slice Flipping and Tiling" +description = "Illustrates how to flip and tile images with 9 Slicing in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "ui_texture_atlas_slice" path = "examples/ui/ui_texture_atlas_slice.rs" diff --git a/assets/textures/fantasy_ui_borders/numbered_slices.png b/assets/textures/fantasy_ui_borders/numbered_slices.png new file mode 100644 index 0000000000000000000000000000000000000000..612c3120ac64722e33df7e7ffd14cc6e4b42ed49 GIT binary patch literal 620 zcmV-y0+aoTP)Px%CP_p=RA_V(+6Cs3p9=VL53nZ_18qN%v_LK4YgM2r zc~&N}5=By_1geZb9?Hb6T*q45J_c%3k^X{6ceM&s3?RGLq85UCVg^V [[f32; 4]; 3] { match image_scale_mode { ImageScaleMode::Sliced(TextureSlicer { - border, + border: border_rect, center_scale_mode, sides_scale_mode, max_corner_scale, @@ -701,31 +713,48 @@ fn compute_texture_slices( .min_element() .min(*max_corner_scale); + // calculate the normalized extents of the nine-patched image slices let slices = [ - border.left / image_size.x, - border.top / image_size.y, - 1. - border.right / image_size.x, - 1. - border.bottom / image_size.y, + border_rect.left / image_size.x, + border_rect.top / image_size.y, + 1. - border_rect.right / image_size.x, + 1. - border_rect.bottom / image_size.y, ]; + // calculate the normalized extents of the target slices let border = [ - (border.left / target_size.x) * min_coeff, - (border.top / target_size.y) * min_coeff, - 1. - (border.right / target_size.x) * min_coeff, - 1. - (border.bottom / target_size.y) * min_coeff, + (border_rect.left / target_size.x) * min_coeff, + (border_rect.top / target_size.y) * min_coeff, + 1. - (border_rect.right / target_size.x) * min_coeff, + 1. - (border_rect.bottom / target_size.y) * min_coeff, ]; - let isx = image_size.x * (1. - slices[0] - slices[2]); - let isy = image_size.y * (1. - slices[1] - slices[3]); - let tsx = target_size.x * (1. - border[0] - border[2]); - let tsy = target_size.y * (1. - border[1] - border[3]); + let image_side_width = image_size.x * (slices[2] - slices[0]); + let image_side_height = image_size.y * (slices[2] - slices[1]); + let target_side_height = target_size.x * (border[2] - border[0]); + let target_side_width = target_size.y * (border[3] - border[1]); - let rx = compute_tiled_subaxis(isx, tsx, sides_scale_mode); - let ry = compute_tiled_subaxis(isy, tsy, sides_scale_mode); - let cx = compute_tiled_subaxis(isx, tsx, center_scale_mode); - let cy = compute_tiled_subaxis(isy, tsy, center_scale_mode); + // compute the number of times to repeat the side and center slices when tiling along each axis + // if the returned value is `1.` the slice will be stretched to fill the axis. + let repeat_side_x = + compute_tiled_subaxis(image_side_width, target_side_height, sides_scale_mode); + let repeat_side_y = + compute_tiled_subaxis(image_side_height, target_side_width, sides_scale_mode); + let repeat_center_x = + compute_tiled_subaxis(image_side_width, target_side_height, center_scale_mode); + let repeat_center_y = + compute_tiled_subaxis(image_side_height, target_side_width, center_scale_mode); - [slices, border, [rx, ry, cx, cy]] + [ + slices, + border, + [ + repeat_side_x, + repeat_side_y, + repeat_center_x, + repeat_center_y, + ], + ] } ImageScaleMode::Tiled { tile_x, @@ -739,21 +768,21 @@ fn compute_texture_slices( } } -fn compute_tiled_axis(tile: bool, is: f32, ts: f32, stretch: f32) -> f32 { +fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 { if tile { - let s = is * stretch; - ts / s + let s = image_extent * stretch; + target_extent / s } else { 1. } } -fn compute_tiled_subaxis(is: f32, ts: f32, mode: &SliceScaleMode) -> f32 { +fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 { match mode { SliceScaleMode::Stretch => 1., SliceScaleMode::Tile { stretch_value } => { - let s = is * *stretch_value; - ts / s + let s = image_extent * *stretch_value; + target_extent / s } } } diff --git a/examples/README.md b/examples/README.md index cadf2f7a54..e2e3bbf426 100644 --- a/examples/README.md +++ b/examples/README.md @@ -491,6 +491,7 @@ Example | Description [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI +[UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing 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. diff --git a/examples/ui/ui_texture_slice_flip_and_tile.rs b/examples/ui/ui_texture_slice_flip_and_tile.rs new file mode 100644 index 0000000000..5d295ade57 --- /dev/null +++ b/examples/ui/ui_texture_slice_flip_and_tile.rs @@ -0,0 +1,78 @@ +//! This example illustrates how to how to flip and tile images with 9-slicing in the UI. + +use bevy::{prelude::*, winit::WinitSettings}; +use bevy_render::texture::{ImageLoaderSettings, ImageSampler}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(UiScale(2.)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use for UI-only apps. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let image = asset_server.load_with_settings( + "textures/fantasy_ui_borders/numbered_slices.png", + |settings: &mut ImageLoaderSettings| { + // Need to use nearest filtering to avoid bleeding between the slices with tiling + settings.sampler = ImageSampler::nearest(); + }, + ); + + let slicer = TextureSlicer { + // `numbered_slices.png` is 48 pixels square. `BorderRect::square(16.)` insets the slicing line from each edge by 16 pixels, resulting in nine slices that are each 16 pixels square. + border: BorderRect::square(16.), + // With `SliceScaleMode::Tile` the side and center slices are tiled to to fill the side and center sections of the target. + // And with a `stretch_value` of `1.` the tiles will have the same size as the corresponding slices in the source image. + center_scale_mode: SliceScaleMode::Tile { stretch_value: 1. }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 1. }, + ..default() + }; + + // ui camera + commands.spawn(Camera2dBundle::default()); + + commands + .spawn(NodeBundle { + style: Style { + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_content: AlignContent::Center, + flex_wrap: FlexWrap::Wrap, + column_gap: Val::Px(10.), + row_gap: Val::Px(10.), + ..default() + }, + ..default() + }) + .with_children(|parent| { + for ([width, height], flip_x, flip_y) in [ + ([160., 160.], false, false), + ([320., 160.], false, true), + ([320., 160.], true, false), + ([160., 160.], true, true), + ] { + parent.spawn(( + NodeBundle { + style: Style { + width: Val::Px(width), + height: Val::Px(height), + ..default() + }, + ..Default::default() + }, + UiImage { + texture: image.clone(), + flip_x, + flip_y, + ..Default::default() + }, + ImageScaleMode::Sliced(slicer.clone()), + )); + } + }); +}