From 85487707ef0074611b506d639b9f7ad5ef17c387 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 4 Nov 2021 20:28:53 +0000 Subject: [PATCH] Sprite Batching (#3060) This implements the following: * **Sprite Batching**: Collects sprites in a vertex buffer to draw many sprites with a single draw call. Sprites are batched by their `Handle` within a specific z-level. When possible, sprites are opportunistically batched _across_ z-levels (when no sprites with a different texture exist between two sprites with the same texture on different z levels). With these changes, I can now get ~130,000 sprites at 60fps on the `bevymark_pipelined` example. * **Sprite Color Tints**: The `Sprite` type now has a `color` field. Non-white color tints result in a specialized render pipeline that passes the color in as a vertex attribute. I chose to specialize this because passing vertex colors has a measurable price (without colors I get ~130,000 sprites on bevymark, with colors I get ~100,000 sprites). "Colored" sprites cannot be batched with "uncolored" sprites, but I think this is fine because the chance of a "colored" sprite needing to batch with other "colored" sprites is generally probably way higher than an "uncolored" sprite needing to batch with a "colored" sprite. * **Sprite Flipping**: Sprites can be flipped on their x or y axis using `Sprite::flip_x` and `Sprite::flip_y`. This is also true for `TextureAtlasSprite`. * **Simpler BufferVec/UniformVec/DynamicUniformVec Clearing**: improved the clearing interface by removing the need to know the size of the final buffer at the initial clear. ![image](https://user-images.githubusercontent.com/2694663/140001821-99be0d96-025d-489e-9bfa-ba19c1dc9548.png) Note that this moves sprites away from entity-driven rendering and back to extracted lists. We _could_ use entities here, but it necessitates that an intermediate list is allocated / populated to collect and sort extracted sprites. This redundant copy, combined with the normal overhead of spawning extracted sprite entities, brings bevymark down to ~80,000 sprites at 60fps. I think making sprites a bit more fixed (by default) is worth it. I view this as acceptable because batching makes normal entity-driven rendering pretty useless anyway (and we would want to batch most custom materials too). We can still support custom shaders with custom bindings, we'll just need to define a specific interface for it. --- examples/tools/bevymark_pipelined.rs | 21 +- pipelined/bevy_core_pipeline/src/lib.rs | 9 +- pipelined/bevy_pbr2/src/render/light.rs | 9 +- .../bevy_render2/src/color/colorspace.rs | 4 + pipelined/bevy_render2/src/color/mod.rs | 1 + .../bevy_render2/src/render_component.rs | 9 +- .../src/render_resource/buffer_vec.rs | 34 +- .../src/render_resource/uniform_vec.rs | 38 +- pipelined/bevy_render2/src/view/mod.rs | 10 +- pipelined/bevy_sprite2/src/lib.rs | 9 +- pipelined/bevy_sprite2/src/render/mod.rs | 454 +++++++++++------- pipelined/bevy_sprite2/src/render/sprite.wgsl | 17 +- pipelined/bevy_sprite2/src/sprite.rs | 3 + 13 files changed, 383 insertions(+), 235 deletions(-) diff --git a/examples/tools/bevymark_pipelined.rs b/examples/tools/bevymark_pipelined.rs index f42969bc17..912f62aca6 100644 --- a/examples/tools/bevymark_pipelined.rs +++ b/examples/tools/bevymark_pipelined.rs @@ -4,13 +4,13 @@ use bevy::{ ecs::prelude::*, input::Input, math::Vec3, - prelude::{App, AssetServer, Handle, MouseButton, Transform}, + prelude::{info, App, AssetServer, Handle, MouseButton, Transform}, render2::{camera::OrthographicCameraBundle, color::Color, texture::Image}, - sprite2::PipelinedSpriteBundle, + sprite2::{PipelinedSpriteBundle, Sprite}, window::WindowDescriptor, PipelinedDefaultPlugins, }; -use rand::Rng; +use rand::{random, Rng}; const BIRDS_PER_SECOND: u32 = 10000; const _BASE_COLOR: Color = Color::rgb(5.0, 5.0, 5.0); @@ -21,6 +21,7 @@ const HALF_BIRD_SIZE: f32 = 256. * BIRD_SCALE * 0.5; struct BevyCounter { pub count: u128, + pub color: Color, } struct Bird { @@ -52,7 +53,10 @@ fn main() { .add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(LogDiagnosticsPlugin::default()) // .add_plugin(WgpuResourceDiagnosticsPlugin::default()) - .insert_resource(BevyCounter { count: 0 }) + .insert_resource(BevyCounter { + count: 0, + color: Color::WHITE, + }) // .init_resource::() .add_startup_system(setup) .add_system(mouse_handler) @@ -161,6 +165,9 @@ fn mouse_handler( // texture: Some(texture_handle), // }); // } + if mouse_button_input.just_released(MouseButton::Left) { + counter.color = Color::rgb(random(), random(), random()); + } if mouse_button_input.pressed(MouseButton::Left) { let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_seconds_f64()) as u128; @@ -194,6 +201,10 @@ fn spawn_birds( scale: Vec3::splat(BIRD_SCALE), ..Default::default() }, + sprite: Sprite { + color: counter.color, + ..Default::default() + }, ..Default::default() }) .insert(Bird { @@ -255,7 +266,7 @@ fn counter_system( counter: Res, ) { if timer.timer.tick(time.delta()).finished() { - println!("counter: {}", counter.count); + info!("counter: {}", counter.count); } } diff --git a/pipelined/bevy_core_pipeline/src/lib.rs b/pipelined/bevy_core_pipeline/src/lib.rs index 1ce7ab334e..bb38fab3bb 100644 --- a/pipelined/bevy_core_pipeline/src/lib.rs +++ b/pipelined/bevy_core_pipeline/src/lib.rs @@ -7,7 +7,6 @@ pub use main_pass_3d::*; pub use main_pass_driver::*; use bevy_app::{App, Plugin}; -use bevy_asset::Handle; use bevy_core::FloatOrd; use bevy_ecs::{ prelude::*, @@ -23,7 +22,7 @@ use bevy_render2::{ }, render_resource::*, renderer::RenderDevice, - texture::{Image, TextureCache}, + texture::TextureCache, view::{ExtractedView, Msaa, ViewDepthTexture}, RenderApp, RenderStage, RenderWorld, }; @@ -131,18 +130,18 @@ impl Plugin for CorePipelinePlugin { } pub struct Transparent2d { - pub sort_key: Handle, + pub sort_key: FloatOrd, pub entity: Entity, pub pipeline: CachedPipelineId, pub draw_function: DrawFunctionId, } impl PhaseItem for Transparent2d { - type SortKey = Handle; + type SortKey = FloatOrd; #[inline] fn sort_key(&self) -> Self::SortKey { - self.sort_key.clone_weak() + self.sort_key } #[inline] diff --git a/pipelined/bevy_pbr2/src/render/light.rs b/pipelined/bevy_pbr2/src/render/light.rs index bd1a91c453..1dddd127bb 100644 --- a/pipelined/bevy_pbr2/src/render/light.rs +++ b/pipelined/bevy_pbr2/src/render/light.rs @@ -383,10 +383,7 @@ pub fn prepare_lights( point_lights: Query<&ExtractedPointLight>, directional_lights: Query<&ExtractedDirectionalLight>, ) { - // PERF: view.iter().count() could be views.iter().len() if we implemented ExactSizeIterator for archetype-only filters - light_meta - .view_gpu_lights - .reserve_and_clear(views.iter().count(), &render_device); + light_meta.view_gpu_lights.clear(); let ambient_color = ambient_light.color.as_rgba_linear() * ambient_light.brightness; // set up light data for each view @@ -605,7 +602,9 @@ pub fn prepare_lights( }); } - light_meta.view_gpu_lights.write_buffer(&render_queue); + light_meta + .view_gpu_lights + .write_buffer(&render_device, &render_queue); } pub fn queue_shadow_view_bind_group( diff --git a/pipelined/bevy_render2/src/color/colorspace.rs b/pipelined/bevy_render2/src/color/colorspace.rs index 677b115276..43d3f7986f 100644 --- a/pipelined/bevy_render2/src/color/colorspace.rs +++ b/pipelined/bevy_render2/src/color/colorspace.rs @@ -5,6 +5,7 @@ pub trait SrgbColorSpace { // source: https://entropymine.com/imageworsener/srgbformula/ impl SrgbColorSpace for f32 { + #[inline] fn linear_to_nonlinear_srgb(self) -> f32 { if self <= 0.0 { return self; @@ -17,6 +18,7 @@ impl SrgbColorSpace for f32 { } } + #[inline] fn nonlinear_to_linear_srgb(self) -> f32 { if self <= 0.0 { return self; @@ -32,6 +34,7 @@ impl SrgbColorSpace for f32 { pub struct HslRepresentation; impl HslRepresentation { /// converts a color in HLS space to sRGB space + #[inline] pub fn hsl_to_nonlinear_srgb(hue: f32, saturation: f32, lightness: f32) -> [f32; 3] { // https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; @@ -60,6 +63,7 @@ impl HslRepresentation { } /// converts a color in sRGB space to HLS space + #[inline] pub fn nonlinear_srgb_to_hsl([red, green, blue]: [f32; 3]) -> (f32, f32, f32) { // https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB let x_max = red.max(green.max(blue)); diff --git a/pipelined/bevy_render2/src/color/mod.rs b/pipelined/bevy_render2/src/color/mod.rs index 0a6ab6e3a8..8f4e666352 100644 --- a/pipelined/bevy_render2/src/color/mod.rs +++ b/pipelined/bevy_render2/src/color/mod.rs @@ -416,6 +416,7 @@ impl Color { } /// Converts a `Color` to a `[f32; 4]` from linear RBG colorspace + #[inline] pub fn as_linear_rgba_f32(self: Color) -> [f32; 4] { match self { Color::Rgba { diff --git a/pipelined/bevy_render2/src/render_component.rs b/pipelined/bevy_render2/src/render_component.rs index 1a81cb2fa1..f123db86c3 100644 --- a/pipelined/bevy_render2/src/render_component.rs +++ b/pipelined/bevy_render2/src/render_component.rs @@ -92,10 +92,7 @@ fn prepare_uniform_components( ) where C: AsStd140 + Clone, { - let len = components.iter().len(); - component_uniforms - .uniforms - .reserve_and_clear(len, &render_device); + component_uniforms.uniforms.clear(); for (entity, component) in components.iter() { commands .get_or_spawn(entity) @@ -105,7 +102,9 @@ fn prepare_uniform_components( }); } - component_uniforms.uniforms.write_buffer(&render_queue); + component_uniforms + .uniforms + .write_buffer(&render_device, &render_queue); } pub struct ExtractComponentPlugin(PhantomData (C, F)>); diff --git a/pipelined/bevy_render2/src/render_resource/buffer_vec.rs b/pipelined/bevy_render2/src/render_resource/buffer_vec.rs index 2094d749df..ccdadb119c 100644 --- a/pipelined/bevy_render2/src/render_resource/buffer_vec.rs +++ b/pipelined/bevy_render2/src/render_resource/buffer_vec.rs @@ -43,17 +43,20 @@ impl BufferVec { self.capacity } + #[inline] + pub fn len(&self) -> usize { + self.values.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + pub fn push(&mut self, value: T) -> usize { - let len = self.values.len(); - if len < self.capacity { - self.values.push(value); - len - } else { - panic!( - "Cannot push value because capacity of {} has been reached", - self.capacity - ); - } + let index = self.values.len(); + self.values.push(value); + index } pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { @@ -69,12 +72,11 @@ impl BufferVec { } } - pub fn reserve_and_clear(&mut self, capacity: usize, device: &RenderDevice) { - self.clear(); - self.reserve(capacity, device); - } - - pub fn write_buffer(&mut self, queue: &RenderQueue) { + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); if let Some(buffer) = &self.buffer { let range = 0..self.item_size * self.values.len(); let bytes: &[u8] = cast_slice(&self.values); diff --git a/pipelined/bevy_render2/src/render_resource/uniform_vec.rs b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs index 569db08f07..bfc5025520 100644 --- a/pipelined/bevy_render2/src/render_resource/uniform_vec.rs +++ b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs @@ -58,19 +58,12 @@ impl UniformVec { } pub fn push(&mut self, value: T) -> usize { - let len = self.values.len(); - if len < self.capacity { - self.values.push(value); - len - } else { - panic!( - "Cannot push value because capacity of {} has been reached", - self.capacity - ); - } + let index = self.values.len(); + self.values.push(value); + index } - pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) -> bool { if capacity > self.capacity { self.capacity = capacity; let size = self.item_size * capacity; @@ -81,15 +74,17 @@ impl UniformVec { usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, mapped_at_creation: false, })); + true + } else { + false } } - pub fn reserve_and_clear(&mut self, capacity: usize, device: &RenderDevice) { - self.clear(); - self.reserve(capacity, device); - } - - pub fn write_buffer(&mut self, queue: &RenderQueue) { + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); if let Some(uniform_buffer) = &self.uniform_buffer { let range = 0..self.item_size * self.values.len(); let mut writer = std140::Writer::new(&mut self.scratch[range.clone()]); @@ -152,13 +147,8 @@ impl DynamicUniformVec { } #[inline] - pub fn reserve_and_clear(&mut self, capacity: usize, device: &RenderDevice) { - self.uniform_vec.reserve_and_clear(capacity, device); - } - - #[inline] - pub fn write_buffer(&mut self, queue: &RenderQueue) { - self.uniform_vec.write_buffer(queue); + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + self.uniform_vec.write_buffer(device, queue); } #[inline] diff --git a/pipelined/bevy_render2/src/view/mod.rs b/pipelined/bevy_render2/src/view/mod.rs index c350e65198..17ea7704be 100644 --- a/pipelined/bevy_render2/src/view/mod.rs +++ b/pipelined/bevy_render2/src/view/mod.rs @@ -90,11 +90,9 @@ fn prepare_view_uniforms( render_device: Res, render_queue: Res, mut view_uniforms: ResMut, - mut views: Query<(Entity, &ExtractedView)>, + views: Query<(Entity, &ExtractedView)>, ) { - view_uniforms - .uniforms - .reserve_and_clear(views.iter_mut().len(), &render_device); + view_uniforms.uniforms.clear(); for (entity, camera) in views.iter() { let projection = camera.projection; let view_uniforms = ViewUniformOffset { @@ -108,7 +106,9 @@ fn prepare_view_uniforms( commands.entity(entity).insert(view_uniforms); } - view_uniforms.uniforms.write_buffer(&render_queue); + view_uniforms + .uniforms + .write_buffer(&render_device, &render_queue); } fn prepare_view_targets( diff --git a/pipelined/bevy_sprite2/src/lib.rs b/pipelined/bevy_sprite2/src/lib.rs index e38da1e3b9..27d36548c8 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -18,7 +18,11 @@ use bevy_app::prelude::*; use bevy_asset::{AddAsset, Assets, HandleUntyped}; use bevy_core_pipeline::Transparent2d; use bevy_reflect::TypeUuid; -use bevy_render2::{render_phase::DrawFunctions, render_resource::Shader, RenderApp, RenderStage}; +use bevy_render2::{ + render_phase::DrawFunctions, + render_resource::{Shader, SpecializedPipelines}, + RenderApp, RenderStage, +}; #[derive(Default)] pub struct SpritePlugin; @@ -36,8 +40,9 @@ impl Plugin for SpritePlugin { render_app .init_resource::() .init_resource::() + .init_resource::>() .init_resource::() - .add_system_to_stage(RenderStage::Extract, render::extract_atlases) + .init_resource::() .add_system_to_stage(RenderStage::Extract, render::extract_sprites) .add_system_to_stage(RenderStage::Prepare, render::prepare_sprites) .add_system_to_stage(RenderStage::Queue, queue_sprites); diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index d44db571ff..1b6e4bd5cf 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -1,22 +1,26 @@ +use std::{cmp::Ordering, ops::Range}; + use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, Rect, Sprite, SPRITE_SHADER_HANDLE, }; use bevy_asset::{Assets, Handle}; +use bevy_core::FloatOrd; use bevy_core_pipeline::Transparent2d; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemState}, }; -use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4Swizzles}; use bevy_render2::{ - mesh::{shape::Quad, Indices, Mesh, VertexAttributeValues}, + color::Color, render_asset::RenderAssets, render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass}, render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, Image}, view::{ViewUniformOffset, ViewUniforms}, + RenderWorld, }; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; @@ -25,14 +29,12 @@ use bytemuck::{Pod, Zeroable}; pub struct SpritePipeline { view_layout: BindGroupLayout, material_layout: BindGroupLayout, - pipeline: CachedPipelineId, } impl FromWorld for SpritePipeline { fn from_world(world: &mut World) -> Self { let world = world.cell(); let render_device = world.get_resource::().unwrap(); - let mut pipeline_cache = world.get_resource_mut::().unwrap(); let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { @@ -75,31 +77,59 @@ impl FromWorld for SpritePipeline { label: Some("sprite_material_layout"), }); - let descriptor = RenderPipelineDescriptor { + SpritePipeline { + view_layout, + material_layout, + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct SpritePipelineKey { + colored: bool, +} + +impl SpecializedPipeline for SpritePipeline { + type Key = SpritePipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut vertex_buffer_layout = VertexBufferLayout { + array_stride: 20, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: VertexFormat::Float32x2, + offset: 12, + shader_location: 1, + }, + ], + }; + let mut shader_defs = Vec::new(); + if key.colored { + shader_defs.push("COLORED".to_string()); + vertex_buffer_layout.attributes.push(VertexAttribute { + format: VertexFormat::Uint32, + offset: 20, + shader_location: 2, + }); + vertex_buffer_layout.array_stride += 4; + } + + RenderPipelineDescriptor { vertex: VertexState { shader: SPRITE_SHADER_HANDLE.typed::(), entry_point: "vertex".into(), - shader_defs: vec![], - buffers: vec![VertexBufferLayout { - array_stride: 20, - step_mode: VertexStepMode::Vertex, - attributes: vec![ - VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 0, - }, - VertexAttribute { - format: VertexFormat::Float32x2, - offset: 12, - shader_location: 1, - }, - ], - }], + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], }, fragment: Some(FragmentState { shader: SPRITE_SHADER_HANDLE.typed::(), - shader_defs: vec![], + shader_defs, entry_point: "fragment".into(), targets: vec![ColorTargetState { format: TextureFormat::bevy_default(), @@ -118,7 +148,7 @@ impl FromWorld for SpritePipeline { write_mask: ColorWrites::ALL, }], }), - layout: Some(vec![view_layout.clone(), material_layout.clone()]), + layout: Some(vec![self.view_layout.clone(), self.material_layout.clone()]), primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: None, @@ -135,81 +165,68 @@ impl FromWorld for SpritePipeline { alpha_to_coverage_enabled: false, }, label: Some("sprite_pipeline".into()), - }; - - SpritePipeline { - pipeline: pipeline_cache.queue(descriptor), - view_layout, - material_layout, } } } pub struct ExtractedSprite { transform: Mat4, + color: Color, rect: Rect, handle: Handle, atlas_size: Option, - vertex_index: usize, + flip_x: bool, + flip_y: bool, } -pub fn extract_atlases( - mut commands: Commands, - texture_atlases: Res>, - atlas_query: Query<( - Entity, - &TextureAtlasSprite, - &GlobalTransform, - &Handle, - )>, -) { - let mut sprites = Vec::new(); - for (entity, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { - if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - let rect = texture_atlas.textures[atlas_sprite.index]; - sprites.push(( - entity, - (ExtractedSprite { - atlas_size: Some(texture_atlas.size), - transform: transform.compute_matrix(), - rect, - handle: texture_atlas.texture.clone_weak(), - vertex_index: 0, - },), - )); - } - } - commands.insert_or_spawn_batch(sprites); +#[derive(Default)] +pub struct ExtractedSprites { + sprites: Vec, } pub fn extract_sprites( - mut commands: Commands, + mut render_world: ResMut, images: Res>, - sprite_query: Query<(Entity, &Sprite, &GlobalTransform, &Handle)>, + texture_atlases: Res>, + sprite_query: Query<(&Sprite, &GlobalTransform, &Handle)>, + atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, ) { - let mut sprites = Vec::new(); - for (entity, sprite, transform, handle) in sprite_query.iter() { + let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + extracted_sprites.sprites.clear(); + for (sprite, transform, handle) in sprite_query.iter() { if let Some(image) = images.get(handle) { let size = image.texture_descriptor.size; - sprites.push(( - entity, - (ExtractedSprite { - atlas_size: None, - transform: transform.compute_matrix(), - rect: Rect { - min: Vec2::ZERO, - max: sprite - .custom_size - .unwrap_or_else(|| Vec2::new(size.width as f32, size.height as f32)), - }, - handle: handle.clone_weak(), - vertex_index: 0, - },), - )); + extracted_sprites.sprites.push(ExtractedSprite { + atlas_size: None, + color: sprite.color, + transform: transform.compute_matrix(), + rect: Rect { + min: Vec2::ZERO, + max: sprite + .custom_size + .unwrap_or_else(|| Vec2::new(size.width as f32, size.height as f32)), + }, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + handle: handle.clone_weak(), + }); }; } - commands.insert_or_spawn_batch(sprites); + for (atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let rect = texture_atlas.textures[atlas_sprite.index as usize]; + extracted_sprites.sprites.push(ExtractedSprite { + atlas_size: Some(texture_atlas.size), + color: atlas_sprite.color, + transform: transform.compute_matrix(), + rect, + flip_x: atlas_sprite.flip_x, + flip_y: atlas_sprite.flip_y, + handle: texture_atlas.texture.clone_weak(), + }); + } + } } #[repr(C)] @@ -219,10 +236,17 @@ struct SpriteVertex { pub uv: [f32; 2], } +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct ColoredSpriteVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub color: u32, +} + pub struct SpriteMeta { vertices: BufferVec, - indices: BufferVec, - quad: Mesh, + colored_vertices: BufferVec, view_bind_group: Option, } @@ -230,88 +254,180 @@ impl Default for SpriteMeta { fn default() -> Self { Self { vertices: BufferVec::new(BufferUsages::VERTEX), - indices: BufferVec::new(BufferUsages::INDEX), + colored_vertices: BufferVec::new(BufferUsages::VERTEX), view_bind_group: None, - quad: Quad { - size: Vec2::new(1.0, 1.0), - ..Default::default() - } - .into(), } } } +const QUAD_VERTEX_POSITIONS: &[Vec3] = &[ + const_vec3!([-0.5, -0.5, 0.0]), + const_vec3!([0.5, 0.5, 0.0]), + const_vec3!([-0.5, 0.5, 0.0]), + const_vec3!([-0.5, -0.5, 0.0]), + const_vec3!([0.5, -0.5, 0.0]), + const_vec3!([0.5, 0.5, 0.0]), +]; + +pub struct SpriteBatch { + range: Range, + handle: Handle, + z: f32, + colored: bool, +} + pub fn prepare_sprites( + mut commands: Commands, render_device: Res, render_queue: Res, mut sprite_meta: ResMut, - mut extracted_sprites: Query<&mut ExtractedSprite>, + mut extracted_sprites: ResMut, ) { - let extracted_sprite_len = extracted_sprites.iter_mut().len(); - // dont create buffers when there are no sprites - if extracted_sprite_len == 0 { - return; - } + sprite_meta.vertices.clear(); + sprite_meta.colored_vertices.clear(); - let quad_vertex_positions = if let VertexAttributeValues::Float32x3(vertex_positions) = - sprite_meta - .quad - .attribute(Mesh::ATTRIBUTE_POSITION) - .unwrap() - .clone() - { - vertex_positions - } else { - panic!("expected vec3"); - }; + // sort first by z and then by handle. this ensures that, when possible, batches span multiple z layers + // batches won't span z-layers if there is another batch between them + extracted_sprites.sprites.sort_by(|a, b| { + match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.w_axis[2])) { + Ordering::Equal => a.handle.cmp(&b.handle), + other => other, + } + }); - let quad_indices = if let Indices::U32(indices) = sprite_meta.quad.indices().unwrap() { - indices.clone() - } else { - panic!("expected u32 indices"); - }; - - sprite_meta.vertices.reserve_and_clear( - extracted_sprite_len * quad_vertex_positions.len(), - &render_device, - ); - sprite_meta - .indices - .reserve_and_clear(extracted_sprite_len * quad_indices.len(), &render_device); - - for (i, mut extracted_sprite) in extracted_sprites.iter_mut().enumerate() { + let mut start = 0; + let mut end = 0; + let mut colored_start = 0; + let mut colored_end = 0; + let mut current_batch_handle: Option> = None; + let mut current_batch_colored = false; + let mut last_z = 0.0; + for extracted_sprite in extracted_sprites.sprites.iter() { + let colored = extracted_sprite.color != Color::WHITE; + if let Some(current_batch_handle) = ¤t_batch_handle { + if *current_batch_handle != extracted_sprite.handle || current_batch_colored != colored + { + if current_batch_colored { + commands.spawn_bundle((SpriteBatch { + range: colored_start..colored_end, + handle: current_batch_handle.clone_weak(), + z: last_z, + colored: true, + },)); + colored_start = colored_end; + } else { + commands.spawn_bundle((SpriteBatch { + range: start..end, + handle: current_batch_handle.clone_weak(), + z: last_z, + colored: false, + },)); + start = end; + } + } + } + current_batch_handle = Some(extracted_sprite.handle.clone_weak()); + current_batch_colored = colored; let sprite_rect = extracted_sprite.rect; // Specify the corners of the sprite - let bottom_left = Vec2::new(sprite_rect.min.x, sprite_rect.max.y); - let top_left = sprite_rect.min; - let top_right = Vec2::new(sprite_rect.max.x, sprite_rect.min.y); - let bottom_right = sprite_rect.max; + let mut bottom_left = Vec2::new(sprite_rect.min.x, sprite_rect.max.y); + let mut top_left = sprite_rect.min; + let mut top_right = Vec2::new(sprite_rect.max.x, sprite_rect.min.y); + let mut bottom_right = sprite_rect.max; - let atlas_positions: [Vec2; 4] = [bottom_left, top_left, top_right, bottom_right]; - - extracted_sprite.vertex_index = i; - for (index, vertex_position) in quad_vertex_positions.iter().enumerate() { - let mut final_position = - Vec3::from(*vertex_position) * extracted_sprite.rect.size().extend(1.0); - final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz(); - sprite_meta.vertices.push(SpriteVertex { - position: final_position.into(), - uv: (atlas_positions[index] - / extracted_sprite.atlas_size.unwrap_or(sprite_rect.max)) - .into(), - }); + if extracted_sprite.flip_x { + bottom_left.x = sprite_rect.max.x; + top_left.x = sprite_rect.max.x; + bottom_right.x = sprite_rect.min.x; + top_right.x = sprite_rect.min.x; } - for index in quad_indices.iter() { - sprite_meta - .indices - .push((i * quad_vertex_positions.len()) as u32 + *index); + if extracted_sprite.flip_y { + bottom_left.y = sprite_rect.min.y; + bottom_right.y = sprite_rect.min.y; + top_left.y = sprite_rect.max.y; + top_right.y = sprite_rect.max.y; + } + + let atlas_extent = extracted_sprite.atlas_size.unwrap_or(sprite_rect.max); + bottom_left /= atlas_extent; + bottom_right /= atlas_extent; + top_left /= atlas_extent; + top_right /= atlas_extent; + + let uvs: [[f32; 2]; 6] = [ + bottom_left.into(), + top_right.into(), + top_left.into(), + bottom_left.into(), + bottom_right.into(), + top_right.into(), + ]; + + let rect_size = extracted_sprite.rect.size().extend(1.0); + if current_batch_colored { + let color = extracted_sprite.color.as_linear_rgba_f32(); + // encode color as a single u32 to save space + let color = (color[0] * 255.0) as u32 + | ((color[1] * 255.0) as u32) << 8 + | ((color[2] * 255.0) as u32) << 16 + | ((color[3] * 255.0) as u32) << 24; + for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() { + let mut final_position = *vertex_position * rect_size; + final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz(); + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: final_position.into(), + uv: uvs[index], + color, + }); + } + } else { + for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() { + let mut final_position = *vertex_position * rect_size; + final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz(); + sprite_meta.vertices.push(SpriteVertex { + position: final_position.into(), + uv: uvs[index], + }); + } + } + + last_z = extracted_sprite.transform.w_axis[2]; + if current_batch_colored { + colored_end += QUAD_VERTEX_POSITIONS.len() as u32; + } else { + end += QUAD_VERTEX_POSITIONS.len() as u32; } } - sprite_meta.vertices.write_buffer(&render_queue); - sprite_meta.indices.write_buffer(&render_queue); + // if start != end, there is one last batch to process + if start != end { + if let Some(current_batch_handle) = current_batch_handle { + commands.spawn_bundle((SpriteBatch { + range: start..end, + handle: current_batch_handle, + colored: false, + z: last_z, + },)); + } + } else if colored_start != colored_end { + if let Some(current_batch_handle) = current_batch_handle { + commands.spawn_bundle((SpriteBatch { + range: colored_start..colored_end, + handle: current_batch_handle, + colored: true, + z: last_z, + },)); + } + } + + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); + sprite_meta + .colored_vertices + .write_buffer(&render_device, &render_queue); } #[derive(Default)] @@ -326,9 +442,11 @@ pub fn queue_sprites( mut sprite_meta: ResMut, view_uniforms: Res, sprite_pipeline: Res, + mut pipelines: ResMut>, + mut pipeline_cache: ResMut, mut image_bind_groups: ResMut, gpu_images: Res>, - mut extracted_sprites: Query<(Entity, &ExtractedSprite)>, + mut sprite_batches: Query<(Entity, &SpriteBatch)>, mut views: Query<&mut RenderPhase>, ) { if let Some(view_binding) = view_uniforms.uniforms.binding() { @@ -341,13 +459,23 @@ pub fn queue_sprites( layout: &sprite_pipeline.view_layout, })); let draw_sprite_function = draw_functions.read().get_id::().unwrap(); + let pipeline = pipelines.specialize( + &mut pipeline_cache, + &sprite_pipeline, + SpritePipelineKey { colored: false }, + ); + let colored_pipeline = pipelines.specialize( + &mut pipeline_cache, + &sprite_pipeline, + SpritePipelineKey { colored: true }, + ); for mut transparent_phase in views.iter_mut() { - for (entity, sprite) in extracted_sprites.iter_mut() { + for (entity, batch) in sprite_batches.iter_mut() { image_bind_groups .values - .entry(sprite.handle.clone_weak()) + .entry(batch.handle.clone_weak()) .or_insert_with(|| { - let gpu_image = gpu_images.get(&sprite.handle).unwrap(); + let gpu_image = gpu_images.get(&batch.handle).unwrap(); render_device.create_bind_group(&BindGroupDescriptor { entries: &[ BindGroupEntry { @@ -365,9 +493,13 @@ pub fn queue_sprites( }); transparent_phase.add(Transparent2d { draw_function: draw_sprite_function, - pipeline: sprite_pipeline.pipeline, + pipeline: if batch.colored { + colored_pipeline + } else { + pipeline + }, entity, - sort_key: sprite.handle.clone_weak(), + sort_key: FloatOrd(batch.z), }); } } @@ -380,7 +512,7 @@ pub struct DrawSprite { SRes, SRes, SQuery>, - SQuery>, + SQuery>, )>, } @@ -400,20 +532,18 @@ impl Draw for DrawSprite { view: Entity, item: &Transparent2d, ) { - const INDICES: usize = 6; let (sprite_meta, image_bind_groups, pipelines, views, sprites) = self.params.get(world); let view_uniform = views.get(view).unwrap(); let sprite_meta = sprite_meta.into_inner(); let image_bind_groups = image_bind_groups.into_inner(); - let extracted_sprite = sprites.get(item.entity).unwrap(); + let sprite_batch = sprites.get(item.entity).unwrap(); if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) { pass.set_render_pipeline(pipeline); - pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); - pass.set_index_buffer( - sprite_meta.indices.buffer().unwrap().slice(..), - 0, - IndexFormat::Uint32, - ); + if sprite_batch.colored { + pass.set_vertex_buffer(0, sprite_meta.colored_vertices.buffer().unwrap().slice(..)); + } else { + pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); + } pass.set_bind_group( 0, sprite_meta.view_bind_group.as_ref().unwrap(), @@ -421,19 +551,11 @@ impl Draw for DrawSprite { ); pass.set_bind_group( 1, - image_bind_groups - .values - .get(&extracted_sprite.handle) - .unwrap(), + image_bind_groups.values.get(&sprite_batch.handle).unwrap(), &[], ); - pass.draw_indexed( - (extracted_sprite.vertex_index * INDICES) as u32 - ..(extracted_sprite.vertex_index * INDICES + INDICES) as u32, - 0, - 0..1, - ); + pass.draw(sprite_batch.range.clone(), 0..1); } } } diff --git a/pipelined/bevy_sprite2/src/render/sprite.wgsl b/pipelined/bevy_sprite2/src/render/sprite.wgsl index 29362a6fd2..e331817950 100644 --- a/pipelined/bevy_sprite2/src/render/sprite.wgsl +++ b/pipelined/bevy_sprite2/src/render/sprite.wgsl @@ -8,17 +8,26 @@ var view: View; struct VertexOutput { [[location(0)]] uv: vec2; +#ifdef COLORED + [[location(1)]] color: vec4; +#endif [[builtin(position)]] position: vec4; }; [[stage(vertex)]] fn vertex( [[location(0)]] vertex_position: vec3, - [[location(1)]] vertex_uv: vec2 + [[location(1)]] vertex_uv: vec2, +#ifdef COLORED + [[location(2)]] vertex_color: u32, +#endif ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; out.position = view.view_proj * vec4(vertex_position, 1.0); +#ifdef COLORED + out.color = vec4((vec4(vertex_color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; +#endif return out; } @@ -29,5 +38,9 @@ var sprite_sampler: sampler; [[stage(fragment)]] fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { - return textureSample(sprite_texture, sprite_sampler, in.uv); + var color = textureSample(sprite_texture, sprite_sampler, in.uv); +#ifdef COLORED + color = in.color * color; +#endif + return color; } \ No newline at end of file diff --git a/pipelined/bevy_sprite2/src/sprite.rs b/pipelined/bevy_sprite2/src/sprite.rs index 06ae90094d..655cd21100 100644 --- a/pipelined/bevy_sprite2/src/sprite.rs +++ b/pipelined/bevy_sprite2/src/sprite.rs @@ -1,10 +1,13 @@ use bevy_math::Vec2; use bevy_reflect::{Reflect, TypeUuid}; +use bevy_render2::color::Color; #[derive(Debug, Default, Clone, TypeUuid, Reflect)] #[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"] #[repr(C)] pub struct Sprite { + /// The sprite's color tint + pub color: Color, /// Flip the sprite along the X axis pub flip_x: bool, /// Flip the sprite along the Y axis