ExtractedSprites slice buffer (#17041)

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>
This commit is contained in:
ickshonpe 2025-03-25 03:51:50 +00:00 committed by François Mockers
parent ca3a5c0c6e
commit da305da07b
4 changed files with 280 additions and 192 deletions

View File

@ -118,6 +118,7 @@ impl Plugin for SpritePlugin {
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>() .init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
.init_resource::<SpriteMeta>() .init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>() .init_resource::<ExtractedSprites>()
.init_resource::<ExtractedSlices>()
.init_resource::<SpriteAssetEvents>() .init_resource::<SpriteAssetEvents>()
.add_render_command::<Transparent2d, DrawSprite>() .add_render_command::<Transparent2d, DrawSprite>()
.add_systems( .add_systems(

View File

@ -323,24 +323,37 @@ impl SpecializedRenderPipeline for SpritePipeline {
} }
} }
pub struct ExtractedSlice {
pub offset: Vec2,
pub rect: Rect,
pub size: Vec2,
}
pub struct ExtractedSprite { pub struct ExtractedSprite {
pub main_entity: Entity,
pub render_entity: Entity,
pub transform: GlobalTransform, pub transform: GlobalTransform,
pub color: LinearRgba, pub color: LinearRgba,
/// Select an area of the texture
pub rect: Option<Rect>,
/// Change the on-screen size of the sprite /// Change the on-screen size of the sprite
pub custom_size: Option<Vec2>,
/// Asset ID of the [`Image`] of this sprite /// Asset ID of the [`Image`] of this sprite
/// PERF: storing an `AssetId` instead of `Handle<Image>` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) /// PERF: storing an `AssetId` instead of `Handle<Image>` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped)
pub image_handle_id: AssetId<Image>, pub image_handle_id: AssetId<Image>,
pub flip_x: bool, pub flip_x: bool,
pub flip_y: bool, pub flip_y: bool,
pub anchor: Vec2, pub kind: ExtractedSpriteKind,
/// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the }
/// entity that caused that creation for use in determining visibility.
pub original_entity: Entity, pub enum ExtractedSpriteKind {
pub scaling_mode: Option<ScalingMode>, /// A single sprite with custom sizing and scaling options
pub render_entity: Entity, Single {
anchor: Vec2,
rect: Option<Rect>,
scaling_mode: Option<ScalingMode>,
custom_size: Option<Vec2>,
},
/// Indexes into the list of [`ExtractedSlice`]s stored in the [`ExtractedSlices`] resource
/// Used for elements composed from multiple sprites such as text or nine-patched borders
Slices { indices: Range<usize> },
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
@ -349,6 +362,11 @@ pub struct ExtractedSprites {
pub sprites: Vec<ExtractedSprite>, pub sprites: Vec<ExtractedSprite>,
} }
#[derive(Resource, Default)]
pub struct ExtractedSlices {
pub slices: Vec<ExtractedSlice>,
}
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct SpriteAssetEvents { pub struct SpriteAssetEvents {
pub images: Vec<AssetEvent<Image>>, pub images: Vec<AssetEvent<Image>>,
@ -367,8 +385,8 @@ pub fn extract_sprite_events(
} }
pub fn extract_sprites( pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>, mut extracted_sprites: ResMut<ExtractedSprites>,
mut extracted_slices: ResMut<ExtractedSlices>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>, texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
sprite_query: Extract< sprite_query: Extract<
Query<( Query<(
@ -382,19 +400,32 @@ pub fn extract_sprites(
>, >,
) { ) {
extracted_sprites.sprites.clear(); extracted_sprites.sprites.clear();
for (original_entity, entity, view_visibility, sprite, transform, slices) in sprite_query.iter() extracted_slices.slices.clear();
for (main_entity, render_entity, view_visibility, sprite, transform, slices) in
sprite_query.iter()
{ {
if !view_visibility.get() { if !view_visibility.get() {
continue; continue;
} }
if let Some(slices) = slices { if let Some(slices) = slices {
extracted_sprites.sprites.extend(slices.extract_sprites( let start = extracted_slices.slices.len();
&mut commands, extracted_slices
transform, .slices
original_entity, .extend(slices.extract_slices(sprite));
sprite, let end = extracted_slices.slices.len();
)); extracted_sprites.sprites.push(ExtractedSprite {
main_entity,
render_entity,
color: sprite.color.into(),
transform: *transform,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: sprite.image.id(),
kind: ExtractedSpriteKind::Slices {
indices: start..end,
},
});
} else { } else {
let atlas_rect = sprite let atlas_rect = sprite
.texture_atlas .texture_atlas
@ -407,25 +438,26 @@ pub fn extract_sprites(
(Some(atlas_rect), Some(mut sprite_rect)) => { (Some(atlas_rect), Some(mut sprite_rect)) => {
sprite_rect.min += atlas_rect.min; sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min; sprite_rect.max += atlas_rect.min;
Some(sprite_rect) Some(sprite_rect)
} }
}; };
// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive
extracted_sprites.sprites.push(ExtractedSprite { extracted_sprites.sprites.push(ExtractedSprite {
render_entity: entity, main_entity,
render_entity,
color: sprite.color.into(), color: sprite.color.into(),
transform: *transform, transform: *transform,
rect,
// Pass the custom size
custom_size: sprite.custom_size,
flip_x: sprite.flip_x, flip_x: sprite.flip_x,
flip_y: sprite.flip_y, flip_y: sprite.flip_y,
image_handle_id: sprite.image.id(), image_handle_id: sprite.image.id(),
kind: ExtractedSpriteKind::Single {
anchor: sprite.anchor.as_vec(), anchor: sprite.anchor.as_vec(),
original_entity, rect,
scaling_mode: sprite.image_mode.scale(), scaling_mode: sprite.image_mode.scale(),
// Pass the custom size
custom_size: sprite.custom_size,
},
}); });
} }
} }
@ -554,7 +586,7 @@ pub fn queue_sprites(
.reserve(extracted_sprites.sprites.len()); .reserve(extracted_sprites.sprites.len());
for (index, extracted_sprite) in extracted_sprites.sprites.iter().enumerate() { for (index, extracted_sprite) in extracted_sprites.sprites.iter().enumerate() {
let view_index = extracted_sprite.original_entity.index(); let view_index = extracted_sprite.main_entity.index();
if !view_entities.contains(view_index as usize) { if !view_entities.contains(view_index as usize) {
continue; continue;
@ -569,7 +601,7 @@ pub fn queue_sprites(
pipeline, pipeline,
entity: ( entity: (
extracted_sprite.render_entity, extracted_sprite.render_entity,
extracted_sprite.original_entity.into(), extracted_sprite.main_entity.into(),
), ),
sort_key, sort_key,
// `batch_range` is calculated in `prepare_sprite_image_bind_groups` // `batch_range` is calculated in `prepare_sprite_image_bind_groups`
@ -623,6 +655,7 @@ pub fn prepare_sprite_image_bind_groups(
mut image_bind_groups: ResMut<ImageBindGroups>, mut image_bind_groups: ResMut<ImageBindGroups>,
gpu_images: Res<RenderAssets<GpuImage>>, gpu_images: Res<RenderAssets<GpuImage>>,
extracted_sprites: Res<ExtractedSprites>, extracted_sprites: Res<ExtractedSprites>,
extracted_slices: Res<ExtractedSlices>,
mut phases: ResMut<ViewSortedRenderPhases<Transparent2d>>, mut phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
events: Res<SpriteAssetEvents>, events: Res<SpriteAssetEvents>,
mut batches: ResMut<SpriteBatches>, mut batches: ResMut<SpriteBatches>,
@ -702,15 +735,20 @@ pub fn prepare_sprite_image_bind_groups(
}, },
)); ));
} }
match extracted_sprite.kind {
ExtractedSpriteKind::Single {
anchor,
rect,
scaling_mode,
custom_size,
} => {
// By default, the size of the quad is the size of the texture // By default, the size of the quad is the size of the texture
let mut quad_size = batch_image_size; let mut quad_size = batch_image_size;
// Texture size is the size of the image
let mut texture_size = batch_image_size; let mut texture_size = batch_image_size;
// Calculate vertex data for this item
// If a rect is specified, adjust UVs and the size of the quad // If a rect is specified, adjust UVs and the size of the quad
let mut uv_offset_scale = if let Some(rect) = extracted_sprite.rect { let mut uv_offset_scale = if let Some(rect) = rect {
let rect_size = rect.size(); let rect_size = rect.size();
quad_size = rect_size; quad_size = rect_size;
// Update texture size to the rect size // Update texture size to the rect size
@ -726,16 +764,23 @@ pub fn prepare_sprite_image_bind_groups(
Vec4::new(0.0, 1.0, 1.0, -1.0) Vec4::new(0.0, 1.0, 1.0, -1.0)
}; };
// Override the size if a custom one is specified if extracted_sprite.flip_x {
if let Some(custom_size) = extracted_sprite.custom_size { uv_offset_scale.x += uv_offset_scale.z;
quad_size = custom_size; uv_offset_scale.z *= -1.0;
} }
if extracted_sprite.flip_y {
uv_offset_scale.y += uv_offset_scale.w;
uv_offset_scale.w *= -1.0;
}
// Override the size if a custom one is specified
quad_size = custom_size.unwrap_or(quad_size);
// Used for translation of the quad if `TextureScale::Fit...` is specified. // Used for translation of the quad if `TextureScale::Fit...` is specified.
let mut quad_translation = Vec2::ZERO; let mut quad_translation = Vec2::ZERO;
// Scales the texture based on the `texture_scale` field. // Scales the texture based on the `texture_scale` field.
if let Some(scaling_mode) = extracted_sprite.scaling_mode { if let Some(scaling_mode) = scaling_mode {
apply_scaling( apply_scaling(
scaling_mode, scaling_mode,
texture_size, texture_size,
@ -758,8 +803,7 @@ pub fn prepare_sprite_image_bind_groups(
* Affine3A::from_scale_rotation_translation( * Affine3A::from_scale_rotation_translation(
quad_size.extend(1.0), quad_size.extend(1.0),
Quat::IDENTITY, Quat::IDENTITY,
((quad_size + quad_translation) ((quad_size + quad_translation) * (-anchor - Vec2::splat(0.5)))
* (-extracted_sprite.anchor - Vec2::splat(0.5)))
.extend(0.0), .extend(0.0),
); );
@ -772,12 +816,59 @@ pub fn prepare_sprite_image_bind_groups(
&uv_offset_scale, &uv_offset_scale,
)); ));
transparent_phase.items[batch_item_index]
.batch_range_mut()
.end += 1;
current_batch.as_mut().unwrap().get_mut().range.end += 1; current_batch.as_mut().unwrap().get_mut().range.end += 1;
index += 1; index += 1;
} }
ExtractedSpriteKind::Slices { ref indices } => {
for i in indices.clone() {
let slice = &extracted_slices.slices[i];
let rect = slice.rect;
let rect_size = rect.size();
// Calculate vertex data for this item
let mut uv_offset_scale: Vec4;
// If a rect is specified, adjust UVs and the size of the quad
uv_offset_scale = Vec4::new(
rect.min.x / batch_image_size.x,
rect.max.y / batch_image_size.y,
rect_size.x / batch_image_size.x,
-rect_size.y / batch_image_size.y,
);
if extracted_sprite.flip_x {
uv_offset_scale.x += uv_offset_scale.z;
uv_offset_scale.z *= -1.0;
}
if extracted_sprite.flip_y {
uv_offset_scale.y += uv_offset_scale.w;
uv_offset_scale.w *= -1.0;
}
let transform = extracted_sprite.transform.affine()
* Affine3A::from_scale_rotation_translation(
slice.size.extend(1.0),
Quat::IDENTITY,
(slice.size * -Vec2::splat(0.5) + slice.offset).extend(0.0),
);
// Store the vertex data and add the item to the render phase
sprite_meta
.sprite_instance_buffer
.push(SpriteInstance::from(
&transform,
&extracted_sprite.color,
&uv_offset_scale,
));
current_batch.as_mut().unwrap().get_mut().range.end += 1;
index += 1;
}
}
}
transparent_phase.items[batch_item_index]
.batch_range_mut()
.end += 1;
} }
sprite_meta sprite_meta
.sprite_instance_buffer .sprite_instance_buffer
@ -806,8 +897,8 @@ pub fn prepare_sprite_image_bind_groups(
.sprite_index_buffer .sprite_index_buffer
.write_buffer(&render_device, &render_queue); .write_buffer(&render_device, &render_queue);
} }
}
} }
/// [`RenderCommand`] for sprite rendering. /// [`RenderCommand`] for sprite rendering.
pub type DrawSprite = ( pub type DrawSprite = (
SetItemPipeline, SetItemPipeline,

View File

@ -1,4 +1,4 @@
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout}; use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice; use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets}; use bevy_asset::{AssetEvent, Assets};
@ -6,8 +6,6 @@ use bevy_ecs::prelude::*;
use bevy_image::Image; use bevy_image::Image;
use bevy_math::{Rect, Vec2}; use bevy_math::{Rect, Vec2};
use bevy_platform_support::collections::HashSet; use bevy_platform_support::collections::HashSet;
use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_transform::prelude::*;
/// Component storing texture slices for tiled or sliced sprite entities /// Component storing texture slices for tiled or sliced sprite entities
/// ///
@ -16,60 +14,32 @@ use bevy_transform::prelude::*;
pub struct ComputedTextureSlices(Vec<TextureSlice>); pub struct ComputedTextureSlices(Vec<TextureSlice>);
impl ComputedTextureSlices { impl ComputedTextureSlices {
/// Computes [`ExtractedSprite`] iterator from the sprite slices /// Computes [`ExtractedSlice`] iterator from the sprite slices
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `transform` - the sprite entity global transform
/// * `original_entity` - the sprite entity
/// * `sprite` - The sprite component /// * `sprite` - The sprite component
/// * `handle` - The sprite texture handle
#[must_use] #[must_use]
pub(crate) fn extract_sprites<'a, 'w, 's>( pub(crate) fn extract_slices<'a>(
&'a self, &'a self,
commands: &'a mut Commands<'w, 's>,
transform: &'a GlobalTransform,
original_entity: Entity,
sprite: &'a Sprite, sprite: &'a Sprite,
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a + use<'a, 'w, 's> { ) -> impl ExactSizeIterator<Item = ExtractedSlice> + 'a {
let mut flip = Vec2::ONE; let mut flip = Vec2::ONE;
let [mut flip_x, mut flip_y] = [false; 2];
if sprite.flip_x { if sprite.flip_x {
flip.x *= -1.0; flip.x *= -1.0;
flip_x = true;
} }
if sprite.flip_y { if sprite.flip_y {
flip.y *= -1.0; flip.y *= -1.0;
flip_y = true;
} }
self.0.iter().map(move |slice| { let anchor = sprite.anchor.as_vec()
let offset = (slice.offset * flip).extend(0.0); * sprite
let transform = transform.mul_transform(Transform::from_translation(offset));
ExtractedSprite {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
original_entity,
color: sprite.color.into(),
transform,
rect: Some(slice.texture_rect),
custom_size: Some(slice.draw_size),
flip_x,
flip_y,
image_handle_id: sprite.image.id(),
anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice),
scaling_mode: sprite.image_mode.scale(),
}
})
}
fn redepend_anchor_from_sprite_to_slice(sprite: &Sprite, slice: &TextureSlice) -> Vec2 {
let sprite_size = sprite
.custom_size .custom_size
.unwrap_or(sprite.rect.unwrap_or_default().size()); .unwrap_or(sprite.rect.unwrap_or_default().size());
if sprite_size == Vec2::ZERO { self.0.iter().map(move |slice| ExtractedSlice {
sprite.anchor.as_vec() offset: slice.offset * flip - anchor,
} else { rect: slice.texture_rect,
sprite.anchor.as_vec() * sprite_size / slice.draw_size size: slice.draw_size,
} })
} }
} }

View File

@ -26,7 +26,9 @@ use bevy_render::{
view::{NoFrustumCulling, ViewVisibility}, view::{NoFrustumCulling, ViewVisibility},
Extract, Extract,
}; };
use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, Sprite}; use bevy_sprite::{
Anchor, ExtractedSlice, ExtractedSlices, ExtractedSprite, ExtractedSprites, Sprite,
};
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_transform::prelude::GlobalTransform; use bevy_transform::prelude::GlobalTransform;
use bevy_window::{PrimaryWindow, Window}; use bevy_window::{PrimaryWindow, Window};
@ -136,6 +138,7 @@ pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>;
pub fn extract_text2d_sprite( pub fn extract_text2d_sprite(
mut commands: Commands, mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>, mut extracted_sprites: ResMut<ExtractedSprites>,
mut extracted_slices: ResMut<ExtractedSlices>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>, texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
windows: Extract<Query<&Window, With<PrimaryWindow>>>, windows: Extract<Query<&Window, With<PrimaryWindow>>>,
text2d_query: Extract< text2d_query: Extract<
@ -149,8 +152,11 @@ pub fn extract_text2d_sprite(
&GlobalTransform, &GlobalTransform,
)>, )>,
>, >,
text_styles: Extract<Query<(&TextFont, &TextColor)>>, text_colors: Extract<Query<&TextColor>>,
) { ) {
let mut start = extracted_slices.slices.len();
let mut end = start + 1;
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows let scale_factor = windows
.single() .single()
@ -159,7 +165,7 @@ pub fn extract_text2d_sprite(
let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.));
for ( for (
original_entity, main_entity,
view_visibility, view_visibility,
computed_block, computed_block,
text_layout_info, text_layout_info,
@ -182,15 +188,19 @@ pub fn extract_text2d_sprite(
*global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling; *global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling;
let mut color = LinearRgba::WHITE; let mut color = LinearRgba::WHITE;
let mut current_span = usize::MAX; let mut current_span = usize::MAX;
for PositionedGlyph {
for (
i,
PositionedGlyph {
position, position,
atlas_info, atlas_info,
span_index, span_index,
.. ..
} in &text_layout_info.glyphs },
) in text_layout_info.glyphs.iter().enumerate()
{ {
if *span_index != current_span { if *span_index != current_span {
color = text_styles color = text_colors
.get( .get(
computed_block computed_block
.entities() .entities()
@ -198,25 +208,41 @@ pub fn extract_text2d_sprite(
.map(|t| t.entity) .map(|t| t.entity)
.unwrap_or(Entity::PLACEHOLDER), .unwrap_or(Entity::PLACEHOLDER),
) )
.map(|(_, text_color)| LinearRgba::from(text_color.0)) .map(|text_color| LinearRgba::from(text_color.0))
.unwrap_or_default(); .unwrap_or_default();
current_span = *span_index; current_span = *span_index;
} }
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); let rect = texture_atlases
.get(&atlas_info.texture_atlas)
.unwrap()
.textures[atlas_info.location.glyph_index]
.as_rect();
extracted_slices.slices.push(ExtractedSlice {
offset: *position,
rect,
size: rect.size(),
});
if text_layout_info.glyphs.get(i + 1).is_none_or(|info| {
info.span_index != current_span || info.atlas_info.texture != atlas_info.texture
}) {
let render_entity = commands.spawn(TemporaryRenderEntity).id();
extracted_sprites.sprites.push(ExtractedSprite { extracted_sprites.sprites.push(ExtractedSprite {
render_entity: commands.spawn(TemporaryRenderEntity).id(), main_entity,
transform: transform * GlobalTransform::from_translation(position.extend(0.)), render_entity,
transform,
color, color,
rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()),
custom_size: None,
image_handle_id: atlas_info.texture.id(), image_handle_id: atlas_info.texture.id(),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
anchor: Anchor::Center.as_vec(), kind: bevy_sprite::ExtractedSpriteKind::Slices {
original_entity, indices: start..end,
scaling_mode: None, },
}); });
start = end;
}
end += 1;
} }
} }
} }