mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 06:00:20 +00:00
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:
parent
2f22f5ca21
commit
85487707ef
13 changed files with 383 additions and 235 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)>);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) = ¤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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue