diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index a7be1a4ab983b..f1646d1c9f187 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -48,6 +48,8 @@ pub const SPRITE_SHADER_HANDLE: HandleUntyped = #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum SpriteSystem { ExtractSprites, + PrepareSprites, + QueueSprites, } impl Plugin for SpritePlugin { @@ -75,7 +77,14 @@ impl Plugin for SpritePlugin { render::extract_sprites.label(SpriteSystem::ExtractSprites), ) .add_system_to_stage(RenderStage::Extract, render::extract_sprite_events) - .add_system_to_stage(RenderStage::Queue, queue_sprites); + .add_system_to_stage( + RenderStage::Prepare, + render::prepare_sprites.label(SpriteSystem::PrepareSprites), + ) + .add_system_to_stage( + RenderStage::Queue, + render::queue_sprites.label(SpriteSystem::QueueSprites), + ); }; } } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index ead7f71895f8f..90002a235fc0a 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,4 +1,5 @@ use std::cmp::Ordering; +use std::ops::Range; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, @@ -170,9 +171,11 @@ impl SpecializedRenderPipeline for SpritePipeline { } } -#[derive(Component, Clone, Copy)] +/// Render world sprite data pub struct ExtractedSprite { + /// Sprite global translation, rotation and scale pub transform: GlobalTransform, + /// Sprite color tint pub color: Color, /// Select an area of the texture pub rect: Option, @@ -181,14 +184,21 @@ pub struct ExtractedSprite { /// Handle to the `Image` of this sprite /// PERF: storing a `HandleId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) pub image_handle_id: HandleId, + /// Is the texture flipped horizontally pub flip_x: bool, + /// Is the texture flipped vertically pub flip_y: bool, + /// Sprite anchor offset pub anchor: Vec2, } +/// Container for all [`ExtractedSprite`] #[derive(Default)] pub struct ExtractedSprites { + /// White extracted sprites pub sprites: Vec, + /// Color tinted extracted sprites + pub colored_sprites: Vec, } #[derive(Default)] @@ -233,12 +243,13 @@ pub fn extract_sprites( >, ) { extracted_sprites.sprites.clear(); + extracted_sprites.colored_sprites.clear(); for (visibility, sprite, transform, handle) in sprite_query.iter() { if !visibility.is_visible { continue; } // 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.alloc().init(ExtractedSprite { + let sprite = ExtractedSprite { color: sprite.color, transform: *transform, // Use the full texture @@ -249,7 +260,12 @@ pub fn extract_sprites( flip_y: sprite.flip_y, image_handle_id: handle.id, anchor: sprite.anchor.as_vec(), - }); + }; + if sprite.color == Color::WHITE { + extracted_sprites.sprites.alloc().init(sprite); + } else { + extracted_sprites.colored_sprites.alloc().init(sprite); + } } for (visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { if !visibility.is_visible { @@ -257,7 +273,7 @@ pub fn extract_sprites( } if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { let rect = Some(texture_atlas.textures[atlas_sprite.index as usize]); - extracted_sprites.sprites.alloc().init(ExtractedSprite { + let sprite = ExtractedSprite { color: atlas_sprite.color, transform: *transform, // Select the area in the texture atlas @@ -268,29 +284,45 @@ pub fn extract_sprites( flip_y: atlas_sprite.flip_y, image_handle_id: texture_atlas.texture.id, anchor: atlas_sprite.anchor.as_vec(), - }); + }; + if sprite.color == Color::WHITE { + extracted_sprites.sprites.alloc().init(sprite); + } else { + extracted_sprites.colored_sprites.alloc().init(sprite); + } } } } +/// Single sprite vertex data #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct SpriteVertex { + /// 3D vertex position pub position: [f32; 3], + /// vertex UV coordinates pub uv: [f32; 2], } +/// Single sprite colored vertex data #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct ColoredSpriteVertex { + /// 3D vertex position pub position: [f32; 3], + /// Vertex UV coordinates pub uv: [f32; 2], + /// Vertex color as linear RGBA pub color: [f32; 4], } +/// Sprite rendering information, stored as a resource pub struct SpriteMeta { + /// Non colored vertices buffer vertices: BufferVec, + /// Colored vertices buffer colored_vertices: BufferVec, + /// The associated pipeline's bind group view_bind_group: Option, } @@ -304,26 +336,180 @@ impl Default for SpriteMeta { } } -const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +/// Sprite quad triangles vertex indices +const QUAD_INDICES: [usize; 6] = [ + 0, 2, 3, // Bottom left triangle + 0, 1, 2, // Top right triangle +]; +/// Base Quad vertices 2D positions const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + // Top left Vec2::new(-0.5, -0.5), + // Top right Vec2::new(0.5, -0.5), + // Bottom right Vec2::new(0.5, 0.5), + // Bottom left Vec2::new(-0.5, 0.5), ]; +/// Base Quad vertices UV coordinates const QUAD_UVS: [Vec2; 4] = [ + // Top left Vec2::new(0., 1.), + // Top right Vec2::new(1., 1.), + // Bottom right Vec2::new(1., 0.), + // Bottom left Vec2::new(0., 0.), ]; -#[derive(Component, Eq, PartialEq, Copy, Clone)] +/// Component defining a batch of sprites for the render world +#[derive(Component)] pub struct SpriteBatch { + /// The [`SpriteMeta`] vertex data indices + range: Range, + /// The texture handle id image_handle_id: HandleId, + /// Defines if the `range` targets [`SpriteMeta::vertices`] or [`SpriteMeta::colored_vertices`] colored: bool, + /// Sort key of the batch + z_order: f32, +} + +pub fn prepare_sprites( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut sprite_meta: ResMut, + gpu_images: Res>, + mut extracted_sprites: ResMut, +) { + // Impossible handle id value + const NULL_HANDLE: HandleId = HandleId::Id(Uuid::nil(), u64::MAX); + + // Clear the vertex buffers + sprite_meta.vertices.clear(); + sprite_meta.colored_vertices.clear(); + + // Sort sprites by z for correct transparency and then by handle to improve batching + let sort = |a: &ExtractedSprite, b: &ExtractedSprite| match a + .transform + .translation + .z + .partial_cmp(&b.transform.translation.z) + { + Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), + Some(other) => other, + }; + extracted_sprites.sprites.sort_unstable_by(sort); + extracted_sprites.colored_sprites.sort_unstable_by(sort); + + for (colored, sprites) in [ + (false, &extracted_sprites.sprites), + (true, &extracted_sprites.colored_sprites), + ] { + // Impossible starting values that will be replaced on the first iteration + let mut current_batch_handle = NULL_HANDLE; + let mut current_image_size = Vec2::ZERO; + let mut z_order = 0.0; + + // Vertex buffer indices + let mut start = 0; + let mut end = 0; + + for extracted_sprite in sprites { + if extracted_sprite.image_handle_id != current_batch_handle { + if let Some(gpu_image) = + gpu_images.get(&Handle::weak(extracted_sprite.image_handle_id)) + { + current_image_size = gpu_image.size; + current_batch_handle = extracted_sprite.image_handle_id; + if start != end { + commands.spawn().insert(SpriteBatch { + range: start..end, + image_handle_id: current_batch_handle, + colored, + z_order, + }); + start = end; + } + } else { + // Skip this item if the texture is not ready + continue; + } + } + // Calculate vertex data for this item + let mut uvs = QUAD_UVS; + if extracted_sprite.flip_x { + uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; + } + if extracted_sprite.flip_y { + uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; + } + + // By default, the size of the quad is the size of the texture + let mut quad_size = current_image_size; + + // If a rect is specified, adjust UVs and the size of the quad + if let Some(rect) = extracted_sprite.rect { + let rect_size = rect.size(); + for uv in &mut uvs { + *uv = (rect.min + *uv * rect_size) / current_image_size; + } + quad_size = rect_size; + } + + // Override the size if a custom one is specified + if let Some(custom_size) = extracted_sprite.custom_size { + quad_size = custom_size; + } + + // Apply size and global transform + let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { + extracted_sprite + .transform + .mul_vec3(((quad_pos - extracted_sprite.anchor) * quad_size).extend(0.)) + .into() + }); + if colored { + for i in QUAD_INDICES { + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: positions[i], + uv: uvs[i].into(), + color: extracted_sprite.color.as_linear_rgba_f32(), + }); + } + } else { + for i in QUAD_INDICES { + sprite_meta.vertices.push(SpriteVertex { + position: positions[i], + uv: uvs[i].into(), + }); + } + } + end += QUAD_INDICES.len() as u32; + z_order = extracted_sprite.transform.translation.z; + } + + // if start != end, there is one remaining batch to process + if start != end { + commands.spawn().insert(SpriteBatch { + range: start..end, + image_handle_id: current_batch_handle, + colored, + z_order, + }); + } + } + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); + sprite_meta + .colored_vertices + .write_buffer(&render_device, &render_queue); } #[derive(Default)] @@ -333,10 +519,8 @@ pub struct ImageBindGroups { #[allow(clippy::too_many_arguments)] pub fn queue_sprites( - mut commands: Commands, draw_functions: Res>, render_device: Res, - render_queue: Res, mut sprite_meta: ResMut, view_uniforms: Res, sprite_pipeline: Res, @@ -345,7 +529,7 @@ pub fn queue_sprites( mut image_bind_groups: ResMut, gpu_images: Res>, msaa: Res, - mut extracted_sprites: ResMut, + sprite_batches: Query<(Entity, &SpriteBatch)>, mut views: Query<&mut RenderPhase>, events: Res, ) { @@ -360,12 +544,6 @@ pub fn queue_sprites( } if let Some(view_binding) = view_uniforms.uniforms.binding() { - let sprite_meta = &mut sprite_meta; - - // Clear the vertex buffers - sprite_meta.vertices.clear(); - sprite_meta.colored_vertices.clear(); - sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { binding: 0, @@ -384,168 +562,56 @@ pub fn queue_sprites( key | SpritePipelineKey::COLORED, ); - // Vertex buffer indices - let mut index = 0; - let mut colored_index = 0; - // FIXME: VisibleEntities is ignored for mut transparent_phase in &mut views { - let extracted_sprites = &mut extracted_sprites.sprites; let image_bind_groups = &mut *image_bind_groups; - - transparent_phase.items.reserve(extracted_sprites.len()); - - // Sort sprites by z for correct transparency and then by handle to improve batching - extracted_sprites.sort_unstable_by(|a, b| { - match a - .transform - .translation - .z - .partial_cmp(&b.transform.translation.z) - { - Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), - Some(other) => other, - } - }); - - // Impossible starting values that will be replaced on the first iteration - let mut current_batch = SpriteBatch { - image_handle_id: HandleId::Id(Uuid::nil(), u64::MAX), - colored: false, - }; - let mut current_batch_entity = Entity::from_raw(u32::MAX); - let mut current_image_size = Vec2::ZERO; - // Add a phase item for each sprite, and detect when succesive items can be batched. - // Spawn an entity with a `SpriteBatch` component for each possible batch. - // Compatible items share the same entity. - // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted - // by any other phase item (and they can interrupt other items from batching). - for extracted_sprite in extracted_sprites { - let new_batch = SpriteBatch { - image_handle_id: extracted_sprite.image_handle_id, - colored: extracted_sprite.color != Color::WHITE, - }; - if new_batch != current_batch { - // Set-up a new possible batch - if let Some(gpu_image) = - gpu_images.get(&Handle::weak(new_batch.image_handle_id)) - { - current_batch = new_batch; - current_image_size = Vec2::new(gpu_image.size.x, gpu_image.size.y); - current_batch_entity = commands.spawn_bundle((current_batch,)).id(); - - image_bind_groups - .values - .entry(Handle::weak(current_batch.image_handle_id)) - .or_insert_with(|| { - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView( - &gpu_image.texture_view, - ), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: Some("sprite_material_bind_group"), - layout: &sprite_pipeline.material_layout, - }) - }); - } else { - // Skip this item if the texture is not ready - continue; - } - } - - // Calculate vertex data for this item - - let mut uvs = QUAD_UVS; - if extracted_sprite.flip_x { - uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; - } - if extracted_sprite.flip_y { - uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; - } - - // By default, the size of the quad is the size of the texture - let mut quad_size = current_image_size; - - // If a rect is specified, adjust UVs and the size of the quad - if let Some(rect) = extracted_sprite.rect { - let rect_size = rect.size(); - for uv in &mut uvs { - *uv = (rect.min + *uv * rect_size) / current_image_size; - } - quad_size = rect_size; - } - - // Override the size if a custom one is specified - if let Some(custom_size) = extracted_sprite.custom_size { - quad_size = custom_size; - } - - // Apply size and global transform - let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { - extracted_sprite - .transform - .mul_vec3(((quad_pos - extracted_sprite.anchor) * quad_size).extend(0.)) - .into() - }); + for (entity, sprite_batch) in sprite_batches.iter() { + image_bind_groups + .values + .entry(Handle::weak(sprite_batch.image_handle_id)) + .or_insert_with(|| { + let gpu_image = gpu_images + .get(&Handle::weak(sprite_batch.image_handle_id)) + .unwrap(); + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&gpu_image.texture_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("sprite_material_bind_group"), + layout: &sprite_pipeline.material_layout, + }) + }); // These items will be sorted by depth with other phase items - let sort_key = FloatOrd(extracted_sprite.transform.translation.z); + let sort_key = FloatOrd(sprite_batch.z_order); // Store the vertex data and add the item to the render phase - if current_batch.colored { - for i in QUAD_INDICES { - sprite_meta.colored_vertices.push(ColoredSpriteVertex { - position: positions[i], - uv: uvs[i].into(), - color: extracted_sprite.color.as_linear_rgba_f32(), - }); - } - let item_start = colored_index; - colored_index += QUAD_INDICES.len() as u32; - let item_end = colored_index; - + if sprite_batch.colored { transparent_phase.add(Transparent2d { draw_function: draw_sprite_function, pipeline: colored_pipeline, - entity: current_batch_entity, + entity, sort_key, - batch_range: Some(item_start..item_end), + batch_range: Some(sprite_batch.range.clone()), }); } else { - for i in QUAD_INDICES { - sprite_meta.vertices.push(SpriteVertex { - position: positions[i], - uv: uvs[i].into(), - }); - } - let item_start = index; - index += QUAD_INDICES.len() as u32; - let item_end = index; - transparent_phase.add(Transparent2d { draw_function: draw_sprite_function, pipeline, - entity: current_batch_entity, + entity, sort_key, - batch_range: Some(item_start..item_end), + batch_range: Some(sprite_batch.range.clone()), }); } } } - sprite_meta - .vertices - .write_buffer(&render_device, &render_queue); - sprite_meta - .colored_vertices - .write_buffer(&render_device, &render_queue); } }