
Instead of extracting an individual sprite per glyph of a text spawn or slice of a nine-patched sprite, add a buffer to store the extracted slice geometry. Fixes #16972 * New struct `ExtractedSlice` to hold sprite slice size, position and atlas info (for text each glyph is a slice). * New resource `ExtractedSlices` that wraps the `ExtractedSlice` buffer. This is a separate resource so it can be used without sprites (with a text material, for example). * New enum `ExtractedSpriteKind` with variants `Single` and `Slices`. `Single` represents a single sprite, `Slices` contains a range into the `ExtractedSlice` buffer. * Only queue a single `ExtractedSprite` for sets of glyphs or slices and push the geometry for each individual slice or glyph into the `ExtractedSlice` buffer. * Modify `ComputedTextureSlices` to return an `ExtractedSlice` iterator instead of `ExtractedSprites`. * Modify `extract_text2d_sprite` to only queue new `ExtractedSprite`s on font changes and otherwise push slices. I don't like the name `ExtractedSpriteKind` much, it's a bit redundant and too haskellish. But although it's exported, it's not something users will interact with most of the time so don't want to overthink it. yellow = this pr, red = main ```cargo run --example many_glyphs --release --features "trace_tracy" -- --no-ui``` <img width="454" alt="many-glyphs" src="https://github.com/user-attachments/assets/711b52c9-2d4d-43c7-b154-e81a69c94dce" /> ```cargo run --example many_text2d --release --features "trace_tracy"``` <img width="415" alt="many-text2d" src="https://github.com/user-attachments/assets/5ea2480a-52e0-4cd0-9f12-07405cf6b8fa" /> * `ExtractedSprite` has a new `kind: ExtractedSpriteKind` field with variants `Single` and `Slices`. - `Single` represents a single sprite. `ExtractedSprite`'s `anchor`, `rect`, `scaling_mode` and `custom_size` fields have been moved into `Single`. - `Slices` contains a range that indexes into a new resource `ExtractedSlices`. Slices are used to draw elements composed from multiple sprites such as text or nine-patched borders. * `ComputedTextureSlices::extract_sprites` has been renamed to `extract_slices`. Its `transform` and `original_entity` parameters have been removed. --------- Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
157 lines
5.2 KiB
Rust
157 lines
5.2 KiB
Rust
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
|
|
|
|
use super::TextureSlice;
|
|
use bevy_asset::{AssetEvent, Assets};
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_image::Image;
|
|
use bevy_math::{Rect, Vec2};
|
|
use bevy_platform_support::collections::HashSet;
|
|
|
|
/// Component storing texture slices for tiled or sliced sprite entities
|
|
///
|
|
/// This component is automatically inserted and updated
|
|
#[derive(Debug, Clone, Component)]
|
|
pub struct ComputedTextureSlices(Vec<TextureSlice>);
|
|
|
|
impl ComputedTextureSlices {
|
|
/// Computes [`ExtractedSlice`] iterator from the sprite slices
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `sprite` - The sprite component
|
|
#[must_use]
|
|
pub(crate) fn extract_slices<'a>(
|
|
&'a self,
|
|
sprite: &'a Sprite,
|
|
) -> impl ExactSizeIterator<Item = ExtractedSlice> + 'a {
|
|
let mut flip = Vec2::ONE;
|
|
if sprite.flip_x {
|
|
flip.x *= -1.0;
|
|
}
|
|
if sprite.flip_y {
|
|
flip.y *= -1.0;
|
|
}
|
|
let anchor = sprite.anchor.as_vec()
|
|
* sprite
|
|
.custom_size
|
|
.unwrap_or(sprite.rect.unwrap_or_default().size());
|
|
self.0.iter().map(move |slice| ExtractedSlice {
|
|
offset: slice.offset * flip - anchor,
|
|
rect: slice.texture_rect,
|
|
size: slice.draw_size,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
|
|
/// will be computed according to the `image_handle` dimensions or the sprite rect.
|
|
///
|
|
/// Returns `None` if the image asset is not loaded
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `sprite` - The sprite component with the image handle and image mode
|
|
/// * `images` - The image assets, use to retrieve the image dimensions
|
|
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
|
|
#[must_use]
|
|
fn compute_sprite_slices(
|
|
sprite: &Sprite,
|
|
images: &Assets<Image>,
|
|
atlas_layouts: &Assets<TextureAtlasLayout>,
|
|
) -> Option<ComputedTextureSlices> {
|
|
let (image_size, texture_rect) = match &sprite.texture_atlas {
|
|
Some(a) => {
|
|
let layout = atlas_layouts.get(&a.layout)?;
|
|
(
|
|
layout.size.as_vec2(),
|
|
layout.textures.get(a.index)?.as_rect(),
|
|
)
|
|
}
|
|
None => {
|
|
let image = images.get(&sprite.image)?;
|
|
let size = Vec2::new(
|
|
image.texture_descriptor.size.width as f32,
|
|
image.texture_descriptor.size.height as f32,
|
|
);
|
|
let rect = sprite.rect.unwrap_or(Rect {
|
|
min: Vec2::ZERO,
|
|
max: size,
|
|
});
|
|
(size, rect)
|
|
}
|
|
};
|
|
let slices = match &sprite.image_mode {
|
|
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
|
|
SpriteImageMode::Tiled {
|
|
tile_x,
|
|
tile_y,
|
|
stretch_value,
|
|
} => {
|
|
let slice = TextureSlice {
|
|
texture_rect,
|
|
draw_size: sprite.custom_size.unwrap_or(image_size),
|
|
offset: Vec2::ZERO,
|
|
};
|
|
slice.tiled(*stretch_value, (*tile_x, *tile_y))
|
|
}
|
|
SpriteImageMode::Auto => {
|
|
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
|
|
}
|
|
SpriteImageMode::Scale(_) => {
|
|
unreachable!("Slices should not be computed for SpriteImageMode::Scale")
|
|
}
|
|
};
|
|
Some(ComputedTextureSlices(slices))
|
|
}
|
|
|
|
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
|
|
/// on sprite entities with a matching [`SpriteImageMode`]
|
|
pub(crate) fn compute_slices_on_asset_event(
|
|
mut commands: Commands,
|
|
mut events: EventReader<AssetEvent<Image>>,
|
|
images: Res<Assets<Image>>,
|
|
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
|
sprites: Query<(Entity, &Sprite)>,
|
|
) {
|
|
// 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, sprite) in &sprites {
|
|
if !sprite.image_mode.uses_slices() {
|
|
continue;
|
|
}
|
|
if !added_handles.contains(&sprite.image.id()) {
|
|
continue;
|
|
}
|
|
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
|
|
commands.entity(entity).insert(slices);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
|
|
pub(crate) fn compute_slices_on_sprite_change(
|
|
mut commands: Commands,
|
|
images: Res<Assets<Image>>,
|
|
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
|
|
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
|
|
) {
|
|
for (entity, sprite) in &changed_sprites {
|
|
if !sprite.image_mode.uses_slices() {
|
|
continue;
|
|
}
|
|
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
|
|
commands.entity(entity).insert(slices);
|
|
}
|
|
}
|
|
}
|