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::<SpriteMeta>()
.init_resource::<ExtractedSprites>()
.init_resource::<ExtractedSlices>()
.init_resource::<SpriteAssetEvents>()
.add_render_command::<Transparent2d, DrawSprite>()
.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 main_entity: Entity,
pub render_entity: Entity,
pub transform: GlobalTransform,
pub color: LinearRgba,
/// Select an area of the texture
pub rect: Option<Rect>,
/// Change the on-screen size of the sprite
pub custom_size: Option<Vec2>,
/// 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)
pub image_handle_id: AssetId<Image>,
pub flip_x: bool,
pub flip_y: bool,
pub anchor: Vec2,
/// 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 scaling_mode: Option<ScalingMode>,
pub render_entity: Entity,
pub kind: ExtractedSpriteKind,
}
pub enum ExtractedSpriteKind {
/// A single sprite with custom sizing and scaling options
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)]
@ -349,6 +362,11 @@ pub struct ExtractedSprites {
pub sprites: Vec<ExtractedSprite>,
}
#[derive(Resource, Default)]
pub struct ExtractedSlices {
pub slices: Vec<ExtractedSlice>,
}
#[derive(Resource, Default)]
pub struct SpriteAssetEvents {
pub images: Vec<AssetEvent<Image>>,
@ -367,8 +385,8 @@ pub fn extract_sprite_events(
}
pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>,
mut extracted_slices: ResMut<ExtractedSlices>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
sprite_query: Extract<
Query<(
@ -382,19 +400,32 @@ pub fn extract_sprites(
>,
) {
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() {
continue;
}
if let Some(slices) = slices {
extracted_sprites.sprites.extend(slices.extract_sprites(
&mut commands,
transform,
original_entity,
sprite,
));
let start = extracted_slices.slices.len();
extracted_slices
.slices
.extend(slices.extract_slices(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 {
let atlas_rect = sprite
.texture_atlas
@ -407,25 +438,26 @@ pub fn extract_sprites(
(Some(atlas_rect), Some(mut sprite_rect)) => {
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;
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
extracted_sprites.sprites.push(ExtractedSprite {
render_entity: entity,
main_entity,
render_entity,
color: sprite.color.into(),
transform: *transform,
rect,
// Pass the custom size
custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: sprite.image.id(),
anchor: sprite.anchor.as_vec(),
original_entity,
scaling_mode: sprite.image_mode.scale(),
kind: ExtractedSpriteKind::Single {
anchor: sprite.anchor.as_vec(),
rect,
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());
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) {
continue;
@ -569,7 +601,7 @@ pub fn queue_sprites(
pipeline,
entity: (
extracted_sprite.render_entity,
extracted_sprite.original_entity.into(),
extracted_sprite.main_entity.into(),
),
sort_key,
// `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>,
gpu_images: Res<RenderAssets<GpuImage>>,
extracted_sprites: Res<ExtractedSprites>,
extracted_slices: Res<ExtractedSlices>,
mut phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
events: Res<SpriteAssetEvents>,
mut batches: ResMut<SpriteBatches>,
@ -702,112 +735,170 @@ pub fn prepare_sprite_image_bind_groups(
},
));
}
// By default, the size of the quad is the size of the texture
let mut quad_size = batch_image_size;
// Texture size is the size of the image
let mut texture_size = batch_image_size;
// 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 rect_size = rect.size();
quad_size = rect_size;
// Update texture size to the rect size
// It will help scale properly only portion of the image
texture_size = rect_size;
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,
)
} else {
Vec4::new(0.0, 1.0, 1.0, -1.0)
};
// Override the size if a custom one is specified
if let Some(custom_size) = extracted_sprite.custom_size {
quad_size = custom_size;
}
// Used for translation of the quad if `TextureScale::Fit...` is specified.
let mut quad_translation = Vec2::ZERO;
// Scales the texture based on the `texture_scale` field.
if let Some(scaling_mode) = extracted_sprite.scaling_mode {
apply_scaling(
match extracted_sprite.kind {
ExtractedSpriteKind::Single {
anchor,
rect,
scaling_mode,
texture_size,
&mut quad_size,
&mut quad_translation,
&mut uv_offset_scale,
);
custom_size,
} => {
// By default, the size of the quad is the size of the texture
let mut quad_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
let mut uv_offset_scale = if let Some(rect) = rect {
let rect_size = rect.size();
quad_size = rect_size;
// Update texture size to the rect size
// It will help scale properly only portion of the image
texture_size = rect_size;
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,
)
} else {
Vec4::new(0.0, 1.0, 1.0, -1.0)
};
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;
}
// 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.
let mut quad_translation = Vec2::ZERO;
// Scales the texture based on the `texture_scale` field.
if let Some(scaling_mode) = scaling_mode {
apply_scaling(
scaling_mode,
texture_size,
&mut quad_size,
&mut quad_translation,
&mut uv_offset_scale,
);
}
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(
quad_size.extend(1.0),
Quat::IDENTITY,
((quad_size + quad_translation) * (-anchor - Vec2::splat(0.5)))
.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;
}
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;
}
}
}
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(
quad_size.extend(1.0),
Quat::IDENTITY,
((quad_size + quad_translation)
* (-extracted_sprite.anchor - Vec2::splat(0.5)))
.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,
));
transparent_phase.items[batch_item_index]
.batch_range_mut()
.end += 1;
current_batch.as_mut().unwrap().get_mut().range.end += 1;
index += 1;
}
sprite_meta
.sprite_instance_buffer
.write_buffer(&render_device, &render_queue);
if sprite_meta.sprite_index_buffer.len() != 6 {
sprite_meta.sprite_index_buffer.clear();
// NOTE: This code is creating 6 indices pointing to 4 vertices.
// The vertices form the corners of a quad based on their two least significant bits.
// 10 11
//
// 00 01
// The sprite shader can then use the two least significant bits as the vertex index.
// The rest of the properties to transform the vertex positions and UVs (which are
// implicit) are baked into the instance transform, and UV offset and scale.
// See bevy_sprite/src/render/sprite.wgsl for the details.
sprite_meta.sprite_index_buffer.push(2);
sprite_meta.sprite_index_buffer.push(0);
sprite_meta.sprite_index_buffer.push(1);
sprite_meta.sprite_index_buffer.push(1);
sprite_meta.sprite_index_buffer.push(3);
sprite_meta.sprite_index_buffer.push(2);
sprite_meta
.sprite_index_buffer
.write_buffer(&render_device, &render_queue);
}
}
sprite_meta
.sprite_instance_buffer
.write_buffer(&render_device, &render_queue);
if sprite_meta.sprite_index_buffer.len() != 6 {
sprite_meta.sprite_index_buffer.clear();
// NOTE: This code is creating 6 indices pointing to 4 vertices.
// The vertices form the corners of a quad based on their two least significant bits.
// 10 11
//
// 00 01
// The sprite shader can then use the two least significant bits as the vertex index.
// The rest of the properties to transform the vertex positions and UVs (which are
// implicit) are baked into the instance transform, and UV offset and scale.
// See bevy_sprite/src/render/sprite.wgsl for the details.
sprite_meta.sprite_index_buffer.push(2);
sprite_meta.sprite_index_buffer.push(0);
sprite_meta.sprite_index_buffer.push(1);
sprite_meta.sprite_index_buffer.push(1);
sprite_meta.sprite_index_buffer.push(3);
sprite_meta.sprite_index_buffer.push(2);
sprite_meta
.sprite_index_buffer
.write_buffer(&render_device, &render_queue);
}
}
/// [`RenderCommand`] for sprite rendering.
pub type DrawSprite = (
SetItemPipeline,

View File

@ -1,4 +1,4 @@
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets};
@ -6,8 +6,6 @@ use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{Rect, Vec2};
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
///
@ -16,61 +14,33 @@ use bevy_transform::prelude::*;
pub struct ComputedTextureSlices(Vec<TextureSlice>);
impl ComputedTextureSlices {
/// Computes [`ExtractedSprite`] iterator from the sprite slices
/// Computes [`ExtractedSlice`] 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_sprites<'a, 'w, 's>(
pub(crate) fn extract_slices<'a>(
&'a self,
commands: &'a mut Commands<'w, 's>,
transform: &'a GlobalTransform,
original_entity: Entity,
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_x, mut flip_y] = [false; 2];
if sprite.flip_x {
flip.x *= -1.0;
flip_x = true;
}
if sprite.flip_y {
flip.y *= -1.0;
flip_y = true;
}
self.0.iter().map(move |slice| {
let offset = (slice.offset * flip).extend(0.0);
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(),
}
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,
})
}
fn redepend_anchor_from_sprite_to_slice(sprite: &Sprite, slice: &TextureSlice) -> Vec2 {
let sprite_size = sprite
.custom_size
.unwrap_or(sprite.rect.unwrap_or_default().size());
if sprite_size == Vec2::ZERO {
sprite.anchor.as_vec()
} else {
sprite.anchor.as_vec() * sprite_size / slice.draw_size
}
}
}
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices

View File

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