Skip to content

Commit

Permalink
UI texture slice texture flipping reimplementation (#15034)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
3 people authored Sep 4, 2024
1 parent 739007f commit 8ac745a
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 27 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 56 additions & 27 deletions crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
},
);
}
Expand Down Expand Up @@ -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(),
[
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand All @@ -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 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);

[slices, border, [rx, ry, cx, cy]]
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]);

// 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,
[
repeat_side_x,
repeat_side_y,
repeat_center_x,
repeat_center_y,
],
]
}
ImageScaleMode::Tiled {
tile_x,
Expand All @@ -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
}
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
78 changes: 78 additions & 0 deletions examples/ui/ui_texture_slice_flip_and_tile.rs
Original file line number Diff line number Diff line change
@@ -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()),
));
}
});
}

0 comments on commit 8ac745a

Please sign in to comment.