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
<img width="787" alt="nearest"
src="https://github.com/user-attachments/assets/bc044bae-1748-42ba-92b5-0500c87264f6">
With tiling need to use nearest filtering to avoid bleeding between the
slices.

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe 2024-09-04 20:31:41 +01:00 committed by GitHub
parent 739007f148
commit 8ac745ab10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 144 additions and 25 deletions

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

View file

@ -232,6 +232,8 @@ pub struct ExtractedUiTextureSlice {
pub camera_entity: Entity,
pub color: LinearRgba,
pub image_scale_mode: ImageScaleMode,
pub flip_x: bool,
pub flip_y: bool,
}
#[derive(Resource, Default)]
@ -294,6 +296,8 @@ pub fn extract_ui_texture_slices(
camera_entity,
image_scale_mode: image_scale_mode.clone(),
atlas_rect,
flip_x: image.flip_x,
flip_y: image.flip_y,
},
);
}
@ -546,7 +550,7 @@ pub fn prepare_ui_slices(
let color = texture_slices.color.to_f32_array();
let (image_size, atlas) = if let Some(atlas) = texture_slices.atlas_rect {
let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect {
(
atlas.size(),
[
@ -560,6 +564,14 @@ pub fn prepare_ui_slices(
(batch_image_size, [0., 0., 1., 1.])
};
if texture_slices.flip_x {
atlas.swap(0, 2);
}
if texture_slices.flip_y {
atlas.swap(1, 3);
}
let [slices, border, repeat] = compute_texture_slices(
image_size,
uinode_rect.size(),
@ -692,7 +704,7 @@ fn compute_texture_slices(
) -> [[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
}
}
}

View file

@ -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.

View file

@ -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<AssetServer>) {
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()),
));
}
});
}