bevy/crates/bevy_sprite/src/texture_slice/slicer.rs
Félix Lescaudey de Maneville ab16f5ed6a
UI Texture 9 slice (#11600)
> 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
2024-02-07 20:07:53 +00:00

278 lines
10 KiB
Rust

use super::{BorderRect, TextureSlice};
use bevy_math::{vec2, Rect, Vec2};
use bevy_reflect::Reflect;
/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes
/// without needing to prepare multiple assets. The associated texture will be split into nine portions,
/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion.
///
/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect)]
pub struct TextureSlicer {
/// The sprite borders, defining the 9 sections of the image
pub border: BorderRect,
/// Defines how the center part of the 9 slices will scale
pub center_scale_mode: SliceScaleMode,
/// Defines how the 4 side parts of the 9 slices will scale
pub sides_scale_mode: SliceScaleMode,
/// Defines the maximum scale of the 4 corner slices (default to `1.0`)
pub max_corner_scale: f32,
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]
Stretch,
/// The slice will be tiled to fit the area
Tile {
/// The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above `stretch_value`.
///
/// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels.
/// `2.0` means it would repeat after 20 screen pixels.
///
/// Note: The value should be inferior or equal to `1.0` to avoid quality loss.
///
/// Note: the value will be clamped to `0.001` if lower
stretch_value: f32,
},
}
impl TextureSlicer {
/// Computes the 4 corner slices
#[must_use]
fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {
let coef = render_size / base_rect.size();
let BorderRect {
left,
right,
top,
bottom,
} = self.border;
let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);
[
// Top Left Corner
TextureSlice {
texture_rect: Rect {
min: base_rect.min,
max: base_rect.min + vec2(left, top),
},
draw_size: vec2(left, top) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Top Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.min.y),
max: vec2(base_rect.max.x, top),
},
draw_size: vec2(right, top) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Bottom Left
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.min.x, base_rect.max.y - bottom),
max: vec2(base_rect.min.x + left, base_rect.max.y),
},
draw_size: vec2(left, bottom) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
// Bottom Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),
max: base_rect.max,
},
draw_size: vec2(right, bottom) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
]
}
/// Computes the 2 horizontal side slices (left and right borders)
#[must_use]
fn horizontal_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// left
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(0.0, self.border.top),
max: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
},
draw_size: vec2(
bl_corner.draw_size.x,
render_size.y - bl_corner.draw_size.y - tl_corner.draw_size.y,
),
offset: vec2(-render_size.x + bl_corner.draw_size.x, 0.0) / 2.0,
},
// right
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.bottom,
),
max: vec2(base_rect.max.x, base_rect.max.y - self.border.top),
},
draw_size: vec2(
br_corner.draw_size.x,
render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y),
),
offset: vec2(render_size.x - br_corner.draw_size.x, 0.0) / 2.0,
},
]
}
/// Computes the 2 vertical side slices (bottom and top borders)
#[must_use]
fn vertical_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Bottom
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
max: vec2(base_rect.max.x - self.border.right, base_rect.max.y),
},
draw_size: vec2(
render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),
bl_corner.draw_size.y,
),
offset: vec2(0.0, bl_corner.offset.y),
},
// Top
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(self.border.left, 0.0),
max: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
},
draw_size: vec2(
render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),
tl_corner.draw_size.y,
),
offset: vec2(0.0, tl_corner.offset.y),
},
]
}
/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,
/// a bigger number of sections will be computed.
///
/// # Arguments
///
/// * `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 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);
// Sides
let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);
let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);
// Center
let center = TextureSlice {
texture_rect: Rect {
min: rect.min + vec2(self.border.left, self.border.bottom),
max: vec2(rect.max.x - self.border.right, rect.max.y - self.border.top),
},
draw_size: vec2(
render_size.x - (corners[2].draw_size.x + corners[3].draw_size.x),
render_size.y - (corners[2].draw_size.y + corners[0].draw_size.y),
),
offset: Vec2::ZERO,
};
slices.extend(corners);
match self.center_scale_mode {
SliceScaleMode::Stretch => {
slices.push(center);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(center.tiled(stretch_value, (true, true)));
}
}
match self.sides_scale_mode {
SliceScaleMode::Stretch => {
slices.extend(horizontal_sides);
slices.extend(vertical_sides);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(
horizontal_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (false, true))),
);
slices.extend(
vertical_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (true, false))),
);
}
}
slices
}
}
impl Default for TextureSlicer {
fn default() -> Self {
Self {
border: Default::default(),
center_scale_mode: Default::default(),
sides_scale_mode: Default::default(),
max_corner_scale: 1.0,
}
}
}