diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index e7e60ff7a2..37d4d2d6e4 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -118,6 +118,7 @@ impl Plugin for SpritePlugin { .init_resource::>() .init_resource::() .init_resource::() + .init_resource::() .init_resource::() .add_render_command::() .add_systems( diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 50c117040b..5879812d7d 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -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, /// Change the on-screen size of the sprite - pub custom_size: Option, /// Asset ID of the [`Image`] of this sprite /// PERF: storing an `AssetId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) pub image_handle_id: AssetId, 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, - pub render_entity: Entity, + pub kind: ExtractedSpriteKind, +} + +pub enum ExtractedSpriteKind { + /// A single sprite with custom sizing and scaling options + Single { + anchor: Vec2, + rect: Option, + scaling_mode: Option, + custom_size: Option, + }, + /// 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 }, } #[derive(Resource, Default)] @@ -348,6 +361,11 @@ pub struct ExtractedSprites { pub sprites: Vec, } +#[derive(Resource, Default)] +pub struct ExtractedSlices { + pub slices: Vec, +} + #[derive(Resource, Default)] pub struct SpriteAssetEvents { pub images: Vec>, @@ -366,8 +384,8 @@ pub fn extract_sprite_events( } pub fn extract_sprites( - mut commands: Commands, mut extracted_sprites: ResMut, + mut extracted_slices: ResMut, texture_atlases: Extract>>, sprite_query: Extract< Query<( @@ -381,19 +399,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 @@ -406,25 +437,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, + }, }); } } @@ -553,7 +585,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; @@ -568,7 +600,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` @@ -622,6 +654,7 @@ pub fn prepare_sprite_image_bind_groups( mut image_bind_groups: ResMut, gpu_images: Res>, extracted_sprites: Res, + extracted_slices: Res, mut phases: ResMut>, events: Res, mut batches: ResMut, @@ -701,112 +734,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, diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index 78b84d2977..f14d6978cc 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -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); 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 + 'a + use<'a, 'w, 's> { + ) -> impl ExactSizeIterator + '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 diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 28eac9cef8..ef70a4b9c3 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -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, + mut extracted_slices: ResMut, texture_atlases: Extract>>, windows: Extract>>, text2d_query: Extract< @@ -149,8 +152,11 @@ pub fn extract_text2d_sprite( &GlobalTransform, )>, >, - text_styles: Extract>, + text_colors: Extract>, ) { + 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; } } }