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<Image>` 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.
This commit is contained in:
Carter Anderson 2021-11-04 20:28:53 +00:00
parent 2f22f5ca21
commit 85487707ef
13 changed files with 383 additions and 235 deletions

View file

@ -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::<BirdMaterial>()
.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<BevyCounter>,
) {
if timer.timer.tick(time.delta()).finished() {
println!("counter: {}", counter.count);
info!("counter: {}", counter.count);
}
}

View file

@ -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<Image>,
pub sort_key: FloatOrd,
pub entity: Entity,
pub pipeline: CachedPipelineId,
pub draw_function: DrawFunctionId,
}
impl PhaseItem for Transparent2d {
type SortKey = Handle<Image>;
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key.clone_weak()
self.sort_key
}
#[inline]

View file

@ -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(

View file

@ -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));

View file

@ -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 {

View file

@ -92,10 +92,7 @@ fn prepare_uniform_components<C: Component>(
) 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<C: Component>(
});
}
component_uniforms.uniforms.write_buffer(&render_queue);
component_uniforms
.uniforms
.write_buffer(&render_device, &render_queue);
}
pub struct ExtractComponentPlugin<C, F = ()>(PhantomData<fn() -> (C, F)>);

View file

@ -43,17 +43,20 @@ impl<T: Pod> BufferVec<T> {
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<T: Pod> BufferVec<T> {
}
}
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);

View file

@ -58,19 +58,12 @@ impl<T: AsStd140> UniformVec<T> {
}
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<T: AsStd140> UniformVec<T> {
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<T: AsStd140> DynamicUniformVec<T> {
}
#[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]

View file

@ -90,11 +90,9 @@ fn prepare_view_uniforms(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut view_uniforms: ResMut<ViewUniforms>,
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(

View file

@ -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::<ImageBindGroups>()
.init_resource::<SpritePipeline>()
.init_resource::<SpecializedPipelines<SpritePipeline>>()
.init_resource::<SpriteMeta>()
.add_system_to_stage(RenderStage::Extract, render::extract_atlases)
.init_resource::<ExtractedSprites>()
.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);

View file

@ -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::<RenderDevice>().unwrap();
let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().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::<Shader>(),
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>(),
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<Image>,
atlas_size: Option<Vec2>,
vertex_index: usize,
flip_x: bool,
flip_y: bool,
}
pub fn extract_atlases(
mut commands: Commands,
texture_atlases: Res<Assets<TextureAtlas>>,
atlas_query: Query<(
Entity,
&TextureAtlasSprite,
&GlobalTransform,
&Handle<TextureAtlas>,
)>,
) {
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<ExtractedSprite>,
}
pub fn extract_sprites(
mut commands: Commands,
mut render_world: ResMut<RenderWorld>,
images: Res<Assets<Image>>,
sprite_query: Query<(Entity, &Sprite, &GlobalTransform, &Handle<Image>)>,
texture_atlases: Res<Assets<TextureAtlas>>,
sprite_query: Query<(&Sprite, &GlobalTransform, &Handle<Image>)>,
atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle<TextureAtlas>)>,
) {
let mut sprites = Vec::new();
for (entity, sprite, transform, handle) in sprite_query.iter() {
let mut extracted_sprites = render_world.get_resource_mut::<ExtractedSprites>().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<SpriteVertex>,
indices: BufferVec<u32>,
quad: Mesh,
colored_vertices: BufferVec<ColoredSpriteVertex>,
view_bind_group: Option<BindGroup>,
}
@ -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<u32>,
handle: Handle<Image>,
z: f32,
colored: bool,
}
pub fn prepare_sprites(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut sprite_meta: ResMut<SpriteMeta>,
mut extracted_sprites: Query<&mut ExtractedSprite>,
mut extracted_sprites: ResMut<ExtractedSprites>,
) {
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<Handle<Image>> = 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) = &current_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<SpriteMeta>,
view_uniforms: Res<ViewUniforms>,
sprite_pipeline: Res<SpritePipeline>,
mut pipelines: ResMut<SpecializedPipelines<SpritePipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut image_bind_groups: ResMut<ImageBindGroups>,
gpu_images: Res<RenderAssets<Image>>,
mut extracted_sprites: Query<(Entity, &ExtractedSprite)>,
mut sprite_batches: Query<(Entity, &SpriteBatch)>,
mut views: Query<&mut RenderPhase<Transparent2d>>,
) {
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::<DrawSprite>().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<ImageBindGroups>,
SRes<RenderPipelineCache>,
SQuery<Read<ViewUniformOffset>>,
SQuery<Read<ExtractedSprite>>,
SQuery<Read<SpriteBatch>>,
)>,
}
@ -400,20 +532,18 @@ impl Draw<Transparent2d> 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<Transparent2d> 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);
}
}
}

View file

@ -8,17 +8,26 @@ var<uniform> view: View;
struct VertexOutput {
[[location(0)]] uv: vec2<f32>;
#ifdef COLORED
[[location(1)]] color: vec4<f32>;
#endif
[[builtin(position)]] position: vec4<f32>;
};
[[stage(vertex)]]
fn vertex(
[[location(0)]] vertex_position: vec3<f32>,
[[location(1)]] vertex_uv: vec2<f32>
[[location(1)]] vertex_uv: vec2<f32>,
#ifdef COLORED
[[location(2)]] vertex_color: u32,
#endif
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
#ifdef COLORED
out.color = vec4<f32>((vec4<u32>(vertex_color) >> vec4<u32>(0u, 8u, 16u, 24u)) & vec4<u32>(255u)) / 255.0;
#endif
return out;
}
@ -29,5 +38,9 @@ var sprite_sampler: sampler;
[[stage(fragment)]]
fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
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;
}

View file

@ -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