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
|
|
|
// This module is mostly copied and pasted from `bevy_sprite::texture_slice`
|
|
|
|
//
|
|
|
|
// A more centralized solution should be investigated in the future
|
|
|
|
|
|
|
|
use bevy_asset::{AssetEvent, Assets};
|
|
|
|
use bevy_ecs::prelude::*;
|
|
|
|
use bevy_math::{Rect, Vec2};
|
|
|
|
use bevy_render::texture::Image;
|
|
|
|
use bevy_sprite::{ImageScaleMode, TextureSlice};
|
|
|
|
use bevy_transform::prelude::*;
|
|
|
|
use bevy_utils::HashSet;
|
|
|
|
|
|
|
|
use crate::{widget::UiImageSize, BackgroundColor, CalculatedClip, ExtractedUiNode, Node, UiImage};
|
|
|
|
|
|
|
|
/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`]
|
|
|
|
///
|
|
|
|
/// This component is automatically inserted and updated
|
|
|
|
#[derive(Debug, Clone, Component)]
|
|
|
|
pub struct ComputedTextureSlices {
|
|
|
|
slices: Vec<TextureSlice>,
|
|
|
|
image_size: Vec2,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ComputedTextureSlices {
|
|
|
|
/// Computes [`ExtractedUiNode`] iterator from the sprite slices
|
|
|
|
///
|
|
|
|
/// # Arguments
|
|
|
|
///
|
|
|
|
/// * `transform` - the sprite entity global transform
|
|
|
|
/// * `original_entity` - the sprite entity
|
|
|
|
/// * `sprite` - The sprite component
|
|
|
|
/// * `handle` - The sprite texture handle
|
|
|
|
#[must_use]
|
|
|
|
pub(crate) fn extract_ui_nodes<'a>(
|
|
|
|
&'a self,
|
|
|
|
transform: &'a GlobalTransform,
|
|
|
|
node: &'a Node,
|
|
|
|
background_color: &'a BackgroundColor,
|
|
|
|
image: &'a UiImage,
|
|
|
|
clip: Option<&'a CalculatedClip>,
|
|
|
|
camera_entity: Entity,
|
|
|
|
) -> impl ExactSizeIterator<Item = ExtractedUiNode> + 'a {
|
|
|
|
let mut flip = Vec2::new(1.0, -1.0);
|
|
|
|
let [mut flip_x, mut flip_y] = [false; 2];
|
|
|
|
if image.flip_x {
|
|
|
|
flip.x *= -1.0;
|
|
|
|
flip_x = true;
|
|
|
|
}
|
|
|
|
if image.flip_y {
|
|
|
|
flip.y *= -1.0;
|
|
|
|
flip_y = true;
|
|
|
|
}
|
|
|
|
self.slices.iter().map(move |slice| {
|
|
|
|
let offset = (slice.offset * flip).extend(0.0);
|
|
|
|
let transform = transform.mul_transform(Transform::from_translation(offset));
|
|
|
|
let scale = slice.draw_size / slice.texture_rect.size();
|
|
|
|
let mut rect = slice.texture_rect;
|
|
|
|
rect.min *= scale;
|
|
|
|
rect.max *= scale;
|
|
|
|
let atlas_size = Some(self.image_size * scale);
|
|
|
|
ExtractedUiNode {
|
|
|
|
stack_index: node.stack_index,
|
|
|
|
color: background_color.0,
|
|
|
|
transform: transform.compute_matrix(),
|
|
|
|
rect,
|
|
|
|
flip_x,
|
|
|
|
flip_y,
|
|
|
|
image: image.texture.id(),
|
|
|
|
atlas_size,
|
|
|
|
clip: clip.map(|clip| clip.clip),
|
|
|
|
camera_entity,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
|
|
|
|
/// will be computed according to the `image_handle` dimensions or the sprite rect.
|
|
|
|
///
|
2024-02-09 20:36:32 +00:00
|
|
|
/// Returns `None` if the image asset is not loaded
|
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
|
|
|
#[must_use]
|
|
|
|
fn compute_texture_slices(
|
|
|
|
draw_area: Vec2,
|
|
|
|
scale_mode: &ImageScaleMode,
|
|
|
|
image_handle: &UiImage,
|
|
|
|
images: &Assets<Image>,
|
|
|
|
) -> Option<ComputedTextureSlices> {
|
|
|
|
let image_size = images.get(&image_handle.texture).map(|i| {
|
|
|
|
Vec2::new(
|
|
|
|
i.texture_descriptor.size.width as f32,
|
|
|
|
i.texture_descriptor.size.height as f32,
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
let texture_rect = Rect {
|
|
|
|
min: Vec2::ZERO,
|
|
|
|
max: image_size,
|
|
|
|
};
|
|
|
|
let slices = match scale_mode {
|
|
|
|
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
|
|
|
|
ImageScaleMode::Tiled {
|
|
|
|
tile_x,
|
|
|
|
tile_y,
|
|
|
|
stretch_value,
|
|
|
|
} => {
|
|
|
|
let slice = TextureSlice {
|
|
|
|
texture_rect,
|
|
|
|
draw_size: draw_area,
|
|
|
|
offset: Vec2::ZERO,
|
|
|
|
};
|
|
|
|
slice.tiled(*stretch_value, (*tile_x, *tile_y))
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Some(ComputedTextureSlices { slices, image_size })
|
|
|
|
}
|
|
|
|
|
|
|
|
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
|
2024-02-09 20:36:32 +00:00
|
|
|
/// on matching sprite entities with a [`ImageScaleMode`] component
|
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
|
|
|
pub(crate) fn compute_slices_on_asset_event(
|
|
|
|
mut commands: Commands,
|
|
|
|
mut events: EventReader<AssetEvent<Image>>,
|
|
|
|
images: Res<Assets<Image>>,
|
|
|
|
ui_nodes: Query<(
|
|
|
|
Entity,
|
|
|
|
&ImageScaleMode,
|
|
|
|
&Node,
|
|
|
|
Option<&UiImageSize>,
|
|
|
|
&UiImage,
|
|
|
|
)>,
|
|
|
|
) {
|
|
|
|
// We store the asset ids of added/modified image assets
|
|
|
|
let added_handles: HashSet<_> = events
|
|
|
|
.read()
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
if added_handles.is_empty() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// We recompute the sprite slices for sprite entities with a matching asset handle id
|
|
|
|
for (entity, scale_mode, ui_node, size, image) in &ui_nodes {
|
|
|
|
if !added_handles.contains(&image.texture.id()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let size = size.map(|s| s.size()).unwrap_or(ui_node.size());
|
|
|
|
if let Some(slices) = compute_texture_slices(size, scale_mode, image, &images) {
|
|
|
|
commands.entity(entity).insert(slices);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
|
2024-02-09 20:36:32 +00:00
|
|
|
/// on matching sprite entities with a [`ImageScaleMode`] component
|
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
|
|
|
pub(crate) fn compute_slices_on_image_change(
|
|
|
|
mut commands: Commands,
|
|
|
|
images: Res<Assets<Image>>,
|
|
|
|
changed_nodes: Query<
|
|
|
|
(
|
|
|
|
Entity,
|
|
|
|
&ImageScaleMode,
|
|
|
|
&Node,
|
|
|
|
Option<&UiImageSize>,
|
|
|
|
&UiImage,
|
|
|
|
),
|
|
|
|
Or<(
|
|
|
|
Changed<ImageScaleMode>,
|
|
|
|
Changed<UiImage>,
|
|
|
|
Changed<UiImageSize>,
|
|
|
|
Changed<Node>,
|
|
|
|
)>,
|
|
|
|
>,
|
|
|
|
) {
|
|
|
|
for (entity, scale_mode, ui_node, size, image) in &changed_nodes {
|
|
|
|
let size = size.map(|s| s.size()).unwrap_or(ui_node.size());
|
|
|
|
if let Some(slices) = compute_texture_slices(size, scale_mode, image, &images) {
|
|
|
|
commands.entity(entity).insert(slices);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|