diff --git a/Cargo.toml b/Cargo.toml index af4f416784..d0bcb7198d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,14 @@ path = "examples/2d/contributors.rs" name = "many_sprites" path = "examples/2d/many_sprites.rs" +[[example]] +name = "mesh2d" +path = "examples/2d/mesh2d.rs" + +[[example]] +name = "mesh2d_manual" +path = "examples/2d/mesh2d_manual.rs" + [[example]] name = "rect" path = "examples/2d/rect.rs" diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 709c456487..1ace533a02 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -15,6 +15,8 @@ pub use main_pass_2d::*; pub use main_pass_3d::*; pub use main_pass_driver::*; +use std::ops::Range; + use bevy_app::{App, Plugin}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; @@ -23,8 +25,8 @@ use bevy_render::{ color::Color, render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, render_phase::{ - sort_phase_system, CachedPipelinePhaseItem, DrawFunctionId, DrawFunctions, EntityPhaseItem, - PhaseItem, RenderPhase, + batch_phase_system, sort_phase_system, BatchedPhaseItem, CachedPipelinePhaseItem, + DrawFunctionId, DrawFunctions, EntityPhaseItem, PhaseItem, RenderPhase, }, render_resource::*, renderer::RenderDevice, @@ -84,6 +86,11 @@ pub mod clear_graph { #[derive(Default)] pub struct CorePipelinePlugin; +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum CorePipelineRenderSystems { + SortTransparent2d, +} + impl Plugin for CorePipelinePlugin { fn build(&self, app: &mut App) { app.init_resource::(); @@ -97,7 +104,16 @@ impl Plugin for CorePipelinePlugin { .add_system_to_stage(RenderStage::Extract, extract_clear_color) .add_system_to_stage(RenderStage::Extract, extract_core_pipeline_camera_phases) .add_system_to_stage(RenderStage::Prepare, prepare_core_views_system) - .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage( + RenderStage::PhaseSort, + sort_phase_system:: + .label(CorePipelineRenderSystems::SortTransparent2d), + ) + .add_system_to_stage( + RenderStage::PhaseSort, + batch_phase_system:: + .after(CorePipelineRenderSystems::SortTransparent2d), + ) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::); @@ -160,6 +176,8 @@ pub struct Transparent2d { pub entity: Entity, pub pipeline: CachedPipelineId, pub draw_function: DrawFunctionId, + /// Range in the vertex buffer of this item + pub batch_range: Option>, } impl PhaseItem for Transparent2d { @@ -176,6 +194,30 @@ impl PhaseItem for Transparent2d { } } +impl EntityPhaseItem for Transparent2d { + #[inline] + fn entity(&self) -> Entity { + self.entity + } +} + +impl CachedPipelinePhaseItem for Transparent2d { + #[inline] + fn cached_pipeline(&self) -> CachedPipelineId { + self.pipeline + } +} + +impl BatchedPhaseItem for Transparent2d { + fn batch_range(&self) -> &Option> { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Option> { + &mut self.batch_range + } +} + pub struct Opaque3d { pub distance: f32, pub pipeline: CachedPipelineId, diff --git a/crates/bevy_core_pipeline/src/main_pass_2d.rs b/crates/bevy_core_pipeline/src/main_pass_2d.rs index 1ca7a21b79..8195a64cec 100644 --- a/crates/bevy_core_pipeline/src/main_pass_2d.rs +++ b/crates/bevy_core_pipeline/src/main_pass_2d.rs @@ -3,7 +3,7 @@ use bevy_ecs::prelude::*; use bevy_render::{ render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, - render_resource::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor}, + render_resource::{LoadOp, Operations, RenderPassDescriptor}, renderer::RenderContext, view::{ExtractedView, ViewTarget}, }; @@ -46,14 +46,10 @@ impl Node for MainPass2dNode { let pass_descriptor = RenderPassDescriptor { label: Some("main_pass_2d"), - color_attachments: &[RenderPassColorAttachment { - view: &target.view, - resolve_target: None, - ops: Operations { - load: LoadOp::Load, - store: true, - }, - }], + color_attachments: &[target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + })], depth_stencil_attachment: None, }; diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index fdd3b9c585..ab796439fc 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::Mat4; +use bevy_math::{Mat4, Size}; use bevy_reflect::TypeUuid; use bevy_render::{ mesh::{GpuBufferInfo, Mesh}, @@ -328,6 +328,10 @@ impl FromWorld for MeshPipeline { texture, texture_view, sampler, + size: Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), } }; MeshPipeline { diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index a17665d7ec..d796fd8342 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -53,3 +53,4 @@ hex = "0.4.2" hexasphere = "6.0.0" parking_lot = "0.11.0" regex = "1.5" +copyless = "0.1.5" \ No newline at end of file diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 7c41569a72..0258ee6b4d 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ }; use bevy_utils::HashMap; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use std::{any::TypeId, fmt::Debug, hash::Hash}; +use std::{any::TypeId, fmt::Debug, hash::Hash, ops::Range}; /// A draw function which is used to draw a specific [`PhaseItem`]. /// @@ -166,6 +166,49 @@ pub trait CachedPipelinePhaseItem: PhaseItem { fn cached_pipeline(&self) -> CachedPipelineId; } +/// A [`PhaseItem`] that can be batched dynamically. +/// +/// Batching is an optimization that regroups multiple items in the same vertex buffer +/// to render them in a single draw call. +pub trait BatchedPhaseItem: EntityPhaseItem { + /// Range in the vertex buffer of this item + fn batch_range(&self) -> &Option>; + + /// Range in the vertex buffer of this item + fn batch_range_mut(&mut self) -> &mut Option>; + + /// Batches another item within this item if they are compatible. + /// Items can be batched together if they have the same entity, and consecutive ranges. + /// If batching is successful, the `other` item should be discarded from the render pass. + #[inline] + fn add_to_batch(&mut self, other: &Self) -> BatchResult { + let self_entity = self.entity(); + if let (Some(self_batch_range), Some(other_batch_range)) = ( + self.batch_range_mut().as_mut(), + other.batch_range().as_ref(), + ) { + // If the items are compatible, join their range into `self` + if self_entity == other.entity() { + if self_batch_range.end == other_batch_range.start { + self_batch_range.end = other_batch_range.end; + return BatchResult::Success; + } else if self_batch_range.start == other_batch_range.end { + self_batch_range.start = other_batch_range.start; + return BatchResult::Success; + } + } + } + BatchResult::IncompatibleItems + } +} + +pub enum BatchResult { + /// The `other` item was batched into `self` + Success, + /// `self` and `other` cannot be batched together + IncompatibleItems, +} + impl RenderCommand

for E { type Param = E::Param; diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 594b9bbfe0..76de6bf665 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -6,6 +6,8 @@ pub use draw_state::*; use bevy_ecs::prelude::{Component, Query}; +use copyless::VecHelper; + /// A resource to collect and sort draw requests for specific [`PhaseItems`](PhaseItem). #[derive(Component)] pub struct RenderPhase { @@ -22,7 +24,7 @@ impl RenderPhase { /// Adds a [`PhaseItem`] to this render phase. #[inline] pub fn add(&mut self, item: I) { - self.items.push(item); + self.items.alloc().init(item); } /// Sorts all of its [`PhaseItems`](PhaseItem). @@ -31,9 +33,166 @@ impl RenderPhase { } } +impl RenderPhase { + /// Batches the compatible [`BatchedPhaseItem`]s of this render phase + pub fn batch(&mut self) { + // TODO: this could be done in-place + let mut items = std::mem::take(&mut self.items); + let mut items = items.drain(..); + + self.items.reserve(items.len()); + + // Start the first batch from the first item + if let Some(mut current_batch) = items.next() { + // Batch following items until we find an incompatible item + for next_item in items { + if matches!( + current_batch.add_to_batch(&next_item), + BatchResult::IncompatibleItems + ) { + // Store the completed batch, and start a new one from the incompatible item + self.items.push(current_batch); + current_batch = next_item; + } + } + // Store the last batch + self.items.push(current_batch); + } + } +} + /// This system sorts all [`RenderPhases`](RenderPhase) for the [`PhaseItem`] type. pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { for mut phase in render_phases.iter_mut() { phase.sort(); } } + +/// This system batches the [`PhaseItem`]s of all [`RenderPhase`]s of this type. +pub fn batch_phase_system(mut render_phases: Query<&mut RenderPhase>) { + for mut phase in render_phases.iter_mut() { + phase.batch(); + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use bevy_ecs::entity::Entity; + + use super::*; + + #[test] + fn batching() { + #[derive(Debug, PartialEq)] + struct TestPhaseItem { + entity: Entity, + batch_range: Option>, + } + impl PhaseItem for TestPhaseItem { + type SortKey = (); + + fn sort_key(&self) -> Self::SortKey {} + + fn draw_function(&self) -> DrawFunctionId { + unimplemented!(); + } + } + impl EntityPhaseItem for TestPhaseItem { + fn entity(&self) -> bevy_ecs::entity::Entity { + self.entity + } + } + impl BatchedPhaseItem for TestPhaseItem { + fn batch_range(&self) -> &Option> { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Option> { + &mut self.batch_range + } + } + let mut render_phase = RenderPhase::::default(); + let items = [ + TestPhaseItem { + entity: Entity::from_raw(0), + batch_range: Some(0..5), + }, + // This item should be batched + TestPhaseItem { + entity: Entity::from_raw(0), + batch_range: Some(5..10), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(0..5), + }, + TestPhaseItem { + entity: Entity::from_raw(0), + batch_range: Some(10..15), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(5..10), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: None, + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(10..15), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(20..25), + }, + // This item should be batched + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(25..30), + }, + // This item should be batched + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(30..35), + }, + ]; + for item in items { + render_phase.add(item); + } + render_phase.batch(); + let items_batched = [ + TestPhaseItem { + entity: Entity::from_raw(0), + batch_range: Some(0..10), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(0..5), + }, + TestPhaseItem { + entity: Entity::from_raw(0), + batch_range: Some(10..15), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(5..10), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: None, + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(10..15), + }, + TestPhaseItem { + entity: Entity::from_raw(1), + batch_range: Some(20..35), + }, + ]; + assert_eq!(&*render_phase.items, items_batched); + } +} diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index ccdadb119c..ad8f5ff2ad 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -3,6 +3,7 @@ use crate::{ renderer::{RenderDevice, RenderQueue}, }; use bevy_core::{cast_slice, Pod}; +use copyless::VecHelper; use wgpu::BufferUsages; pub struct BufferVec { @@ -55,7 +56,7 @@ impl BufferVec { pub fn push(&mut self, value: T) -> usize { let index = self.values.len(); - self.values.push(value); + self.values.alloc().init(value); index } diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 4d8b1f47a1..29063905ae 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -7,6 +7,7 @@ use crate::{ }; use bevy_asset::HandleUntyped; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_math::Size; use bevy_reflect::TypeUuid; use thiserror::Error; use wgpu::{ @@ -373,12 +374,13 @@ impl TextureFormatPixelInfo for TextureFormat { } /// The GPU-representation of an [`Image`]. -/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`]. +/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's [`Size`]. #[derive(Debug, Clone)] pub struct GpuImage { pub texture: Texture, pub texture_view: TextureView, pub sampler: Sampler, + pub size: Size, } impl RenderAsset for Image { @@ -426,10 +428,15 @@ impl RenderAsset for Image { ); let texture_view = texture.create_view(&TextureViewDescriptor::default()); + let size = Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ); Ok(GpuImage { texture, texture_view, sampler, + size, }) } } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index d38e45231b..61da101d02 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -28,7 +28,7 @@ impl Default for Visibility { } /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering -#[derive(Component, Clone, Reflect)] +#[derive(Component, Clone, Reflect, Debug)] #[reflect(Component)] pub struct ComputedVisibility { pub is_visible: bool, diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 53b70abeb8..cbb792bde4 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -30,3 +30,5 @@ guillotiere = "0.6.0" thiserror = "1.0" rectangle-pack = "0.4" serde = { version = "1", features = ["derive"] } +bitflags = "1.2" +copyless = "0.1.5" \ No newline at end of file diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index d97e1857de..82fdcd9db6 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -6,7 +6,7 @@ use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render::{ texture::{Image, DEFAULT_IMAGE_HANDLE}, - view::{ComputedVisibility, Visibility}, + view::Visibility, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -18,8 +18,6 @@ pub struct SpriteBundle { pub texture: Handle, /// User indication of whether an entity is visible pub visibility: Visibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub computed_visibility: ComputedVisibility, } impl Default for SpriteBundle { @@ -30,7 +28,6 @@ impl Default for SpriteBundle { global_transform: Default::default(), texture: DEFAULT_IMAGE_HANDLE.typed(), visibility: Default::default(), - computed_visibility: Default::default(), } } } @@ -47,6 +44,4 @@ pub struct SpriteSheetBundle { pub global_transform: GlobalTransform, /// User indication of whether an entity is visible pub visibility: Visibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub computed_visibility: ComputedVisibility, } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 3a6f266ee8..bad4870208 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -1,5 +1,6 @@ mod bundle; mod dynamic_texture_atlas_builder; +mod mesh2d; mod rect; mod render; mod sprite; @@ -14,12 +15,13 @@ pub mod prelude { bundle::{SpriteBundle, SpriteSheetBundle}, sprite::Sprite, texture_atlas::{TextureAtlas, TextureAtlasSprite}, - TextureAtlasBuilder, + ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, }; } pub use bundle::*; pub use dynamic_texture_atlas_builder::*; +pub use mesh2d::*; pub use rect::*; pub use render::*; pub use sprite::*; @@ -32,7 +34,7 @@ use bevy_core_pipeline::Transparent2d; use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel}; use bevy_reflect::TypeUuid; use bevy_render::{ - render_phase::DrawFunctions, + render_phase::AddRenderCommand, render_resource::{Shader, SpecializedPipelines}, RenderApp, RenderStage, }; @@ -45,7 +47,7 @@ pub const SPRITE_SHADER_HANDLE: HandleUntyped = #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum SpriteSystem { - ExtractSprite, + ExtractSprites, } impl Plugin for SpritePlugin { @@ -53,7 +55,10 @@ impl Plugin for SpritePlugin { let mut shaders = app.world.get_resource_mut::>().unwrap(); let sprite_shader = Shader::from_wgsl(include_str!("render/sprite.wgsl")); shaders.set_untracked(SPRITE_SHADER_HANDLE, sprite_shader); - app.add_asset::().register_type::(); + app.add_asset::() + .register_type::() + .add_plugin(Mesh2dRenderPlugin) + .add_plugin(ColorMaterialPlugin); let render_app = app.sub_app_mut(RenderApp); render_app .init_resource::() @@ -62,20 +67,12 @@ impl Plugin for SpritePlugin { .init_resource::() .init_resource::() .init_resource::() + .add_render_command::() .add_system_to_stage( RenderStage::Extract, - render::extract_sprites.label(SpriteSystem::ExtractSprite), + render::extract_sprites.label(SpriteSystem::ExtractSprites), ) .add_system_to_stage(RenderStage::Extract, render::extract_sprite_events) - .add_system_to_stage(RenderStage::Prepare, render::prepare_sprites) .add_system_to_stage(RenderStage::Queue, queue_sprites); - - let draw_sprite = DrawSprite::new(&mut render_app.world); - render_app - .world - .get_resource::>() - .unwrap() - .write() - .add(draw_sprite); } } diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs new file mode 100644 index 0000000000..eed38c3f14 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -0,0 +1,235 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{AssetServer, Assets, Handle, HandleUntyped}; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_math::Vec4; +use bevy_reflect::TypeUuid; +use bevy_render::{ + color::Color, + prelude::Shader, + render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, + render_resource::{ + std140::{AsStd140, Std140}, + *, + }, + renderer::RenderDevice, + texture::Image, +}; + +use crate::{Material2d, Material2dPipeline, Material2dPlugin, MaterialMesh2dBundle}; + +pub const COLOR_MATERIAL_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 3253086872234592509); + +#[derive(Default)] +pub struct ColorMaterialPlugin; + +impl Plugin for ColorMaterialPlugin { + fn build(&self, app: &mut App) { + let mut shaders = app.world.get_resource_mut::>().unwrap(); + shaders.set_untracked( + COLOR_MATERIAL_SHADER_HANDLE, + Shader::from_wgsl(include_str!("color_material.wgsl")), + ); + + app.add_plugin(Material2dPlugin::::default()); + + app.world + .get_resource_mut::>() + .unwrap() + .set_untracked( + Handle::::default(), + ColorMaterial { + color: Color::rgb(1.0, 0.0, 1.0), + ..Default::default() + }, + ); + } +} + +/// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2dHandle) with a texture tinted by a uniform color +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "e228a544-e3ca-4e1e-bb9d-4d8bc1ad8c19"] +pub struct ColorMaterial { + pub color: Color, + pub texture: Option>, +} + +impl Default for ColorMaterial { + fn default() -> Self { + ColorMaterial { + color: Color::rgb(1.0, 0.0, 1.0), + texture: None, + } + } +} + +impl From for ColorMaterial { + fn from(color: Color) -> Self { + ColorMaterial { + color, + ..Default::default() + } + } +} + +impl From> for ColorMaterial { + fn from(texture: Handle) -> Self { + ColorMaterial { + texture: Some(texture), + color: Color::WHITE, + } + } +} + +// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/color_material.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + pub struct ColorMaterialFlags: u32 { + const TEXTURE = (1 << 0); + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +/// The GPU representation of the uniform data of a [`ColorMaterial`]. +#[derive(Clone, Default, AsStd140)] +pub struct ColorMaterialUniformData { + pub color: Vec4, + pub flags: u32, +} + +/// The GPU representation of a [`ColorMaterial`]. +#[derive(Debug, Clone)] +pub struct GpuColorMaterial { + /// A buffer containing the [`ColorMaterialUniformData`] of the material. + pub buffer: Buffer, + /// The bind group specifying how the [`ColorMaterialUniformData`] and + /// the texture of the material are bound. + pub bind_group: BindGroup, + pub flags: ColorMaterialFlags, + pub texture: Option>, +} + +impl RenderAsset for ColorMaterial { + type ExtractedAsset = ColorMaterial; + type PreparedAsset = GpuColorMaterial; + type Param = ( + SRes, + SRes>, + SRes>, + ); + + fn extract_asset(&self) -> Self::ExtractedAsset { + self.clone() + } + + fn prepare_asset( + material: Self::ExtractedAsset, + (render_device, color_pipeline, gpu_images): &mut SystemParamItem, + ) -> Result> { + let (texture_view, sampler) = if let Some(result) = color_pipeline + .mesh2d_pipeline + .get_image_texture(gpu_images, &material.texture) + { + result + } else { + return Err(PrepareAssetError::RetryNextUpdate(material)); + }; + + let mut flags = ColorMaterialFlags::NONE; + if material.texture.is_some() { + flags |= ColorMaterialFlags::TEXTURE; + } + + let value = ColorMaterialUniformData { + color: material.color.as_linear_rgba_f32().into(), + flags: flags.bits(), + }; + let value_std140 = value.as_std140(); + + let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("color_material_uniform_buffer"), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + contents: value_std140.as_bytes(), + }); + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(texture_view), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(sampler), + }, + ], + label: Some("color_material_bind_group"), + layout: &color_pipeline.material2d_layout, + }); + + Ok(GpuColorMaterial { + buffer, + bind_group, + flags, + texture: material.texture, + }) + } +} + +impl Material2d for ColorMaterial { + fn fragment_shader(_asset_server: &AssetServer) -> Option> { + Some(COLOR_MATERIAL_SHADER_HANDLE.typed()) + } + + #[inline] + fn bind_group(render_asset: &::PreparedAsset) -> &BindGroup { + &render_asset.bind_group + } + + fn bind_group_layout( + render_device: &RenderDevice, + ) -> bevy_render::render_resource::BindGroupLayout { + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: BufferSize::new( + ColorMaterialUniformData::std140_size_static() as u64, + ), + }, + count: None, + }, + // Texture + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + // Texture Sampler + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("color_material_layout"), + }) + } +} + +/// A component bundle for entities with a [`Mesh2dHandle`](crate::Mesh2dHandle) and a [`ColorMaterial`]. +pub type ColorMesh2dBundle = MaterialMesh2dBundle; diff --git a/crates/bevy_sprite/src/mesh2d/color_material.wgsl b/crates/bevy_sprite/src/mesh2d/color_material.wgsl new file mode 100644 index 0000000000..a11e48094f --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/color_material.wgsl @@ -0,0 +1,41 @@ +#import bevy_sprite::mesh2d_view_bind_group +#import bevy_sprite::mesh2d_struct + +struct ColorMaterial { + color: vec4; + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32; +}; +let COLOR_MATERIAL_FLAGS_TEXTURE_BIT: u32 = 1u; + +[[group(0), binding(0)]] +var view: View; + +[[group(1), binding(0)]] +var material: ColorMaterial; +[[group(1), binding(1)]] +var texture: texture_2d; +[[group(1), binding(2)]] +var texture_sampler: sampler; + +[[group(2), binding(0)]] +var mesh: Mesh2d; + +struct FragmentInput { + [[builtin(front_facing)]] is_front: bool; + [[location(0)]] world_position: vec4; + [[location(1)]] world_normal: vec3; + [[location(2)]] uv: vec2; +#ifdef VERTEX_TANGENTS + [[location(3)]] world_tangent: vec4; +#endif +}; + +[[stage(fragment)]] +fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { + var output_color: vec4 = material.color; + if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) { + output_color = output_color * textureSample(texture, texture_sampler, in.uv); + } + return output_color; +} \ No newline at end of file diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs new file mode 100644 index 0000000000..0e3ceaae55 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -0,0 +1,345 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{AddAsset, Asset, AssetServer, Handle}; +use bevy_core::FloatOrd; +use bevy_core_pipeline::Transparent2d; +use bevy_ecs::{ + entity::Entity, + prelude::{Bundle, World}, + system::{ + lifetimeless::{Read, SQuery, SRes}, + Query, Res, ResMut, SystemParamItem, + }, + world::FromWorld, +}; +use bevy_render::{ + mesh::Mesh, + render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets}, + render_component::ExtractComponentPlugin, + render_phase::{ + AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, + SetItemPipeline, TrackedRenderPass, + }, + render_resource::{ + BindGroup, BindGroupLayout, RenderPipelineCache, RenderPipelineDescriptor, Shader, + SpecializedPipeline, SpecializedPipelines, + }, + renderer::RenderDevice, + view::{ComputedVisibility, Msaa, Visibility, VisibleEntities}, + RenderApp, RenderStage, +}; +use bevy_transform::components::{GlobalTransform, Transform}; +use std::hash::Hash; +use std::marker::PhantomData; + +use crate::{ + DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dUniform, SetMesh2dBindGroup, + SetMesh2dViewBindGroup, +}; + +/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`] +/// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level +/// way to render [`Mesh2dHandle`] entities with custom shader logic. For materials that can specialize their [`RenderPipelineDescriptor`] +/// based on specific material values, see [`SpecializedMaterial2d`]. [`Material2d`] automatically implements [`SpecializedMaterial2d`] +/// and can be used anywhere that type is used (such as [`Material2dPlugin`]). +pub trait Material2d: Asset + RenderAsset { + /// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material2d::bind_group_layout`]. + fn bind_group(material: &::PreparedAsset) -> &BindGroup; + + /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`Material2d::bind_group`]. + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; + + /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. + /// Defaults to [`None`]. + #[allow(unused_variables)] + fn vertex_shader(asset_server: &AssetServer) -> Option> { + None + } + + /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. + /// Defaults to [`None`]. + #[allow(unused_variables)] + fn fragment_shader(asset_server: &AssetServer) -> Option> { + None + } + + /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. + /// Defaults to an empty array / no dynamic uniform indices. + #[allow(unused_variables)] + #[inline] + fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { + &[] + } +} + +impl SpecializedMaterial2d for M { + type Key = (); + + #[inline] + fn key(_material: &::PreparedAsset) -> Self::Key {} + + #[inline] + fn specialize(_key: Self::Key, _descriptor: &mut RenderPipelineDescriptor) {} + + #[inline] + fn bind_group(material: &::PreparedAsset) -> &BindGroup { + ::bind_group(material) + } + + #[inline] + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { + ::bind_group_layout(render_device) + } + + #[inline] + fn vertex_shader(asset_server: &AssetServer) -> Option> { + ::vertex_shader(asset_server) + } + + #[inline] + fn fragment_shader(asset_server: &AssetServer) -> Option> { + ::fragment_shader(asset_server) + } + + #[allow(unused_variables)] + #[inline] + fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { + ::dynamic_uniform_indices(material) + } +} + +/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`](crate::MaterialMesh2dBundle) +/// to spawn entities that are rendered with a specific [`SpecializedMaterial2d`] type. They serve as an easy to use high level +/// way to render [`Mesh2dHandle`] entities with custom shader logic. [`SpecializedMaterial2d`s](SpecializedMaterial2d) use their [`SpecializedMaterial2d::Key`] +/// to customize their [`RenderPipelineDescriptor`] based on specific material values. The slightly simpler [`Material2d`] trait +/// should be used for materials that do not need specialization. [`Material2d`] types automatically implement [`SpecializedMaterial2d`]. +pub trait SpecializedMaterial2d: Asset + RenderAsset { + /// The key used to specialize this material's [`RenderPipelineDescriptor`]. + type Key: PartialEq + Eq + Hash + Clone + Send + Sync; + + /// Extract the [`SpecializedMaterial2d::Key`] for the "prepared" version of this material. This key will be + /// passed in to the [`SpecializedMaterial2d::specialize`] function when compiling the [`RenderPipeline`](bevy_render::render_resource::RenderPipeline) + /// for a given entity's material. + fn key(material: &::PreparedAsset) -> Self::Key; + + /// Specializes the given `descriptor` according to the given `key`. + fn specialize(key: Self::Key, descriptor: &mut RenderPipelineDescriptor); + + /// Returns this material's [`BindGroup`]. This should match the layout returned by [`SpecializedMaterial2d::bind_group_layout`]. + fn bind_group(material: &::PreparedAsset) -> &BindGroup; + + /// Returns this material's [`BindGroupLayout`]. This should match the [`BindGroup`] returned by [`SpecializedMaterial2d::bind_group`]. + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout; + + /// Returns this material's vertex shader. If [`None`] is returned, the default mesh vertex shader will be used. + /// Defaults to [`None`]. + #[allow(unused_variables)] + fn vertex_shader(asset_server: &AssetServer) -> Option> { + None + } + + /// Returns this material's fragment shader. If [`None`] is returned, the default mesh fragment shader will be used. + /// Defaults to [`None`]. + #[allow(unused_variables)] + fn fragment_shader(asset_server: &AssetServer) -> Option> { + None + } + + /// The dynamic uniform indices to set for the given `material`'s [`BindGroup`]. + /// Defaults to an empty array / no dynamic uniform indices. + #[allow(unused_variables)] + #[inline] + fn dynamic_uniform_indices(material: &::PreparedAsset) -> &[u32] { + &[] + } +} + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`SpecializedMaterial2d`] +/// asset type (which includes [`Material2d`] types). +pub struct Material2dPlugin(PhantomData); + +impl Default for Material2dPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for Material2dPlugin { + fn build(&self, app: &mut App) { + app.add_asset::() + .add_plugin(ExtractComponentPlugin::>::default()) + .add_plugin(RenderAssetPlugin::::default()); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_render_command::>() + .init_resource::>() + .init_resource::>>() + .add_system_to_stage(RenderStage::Queue, queue_material2d_meshes::); + } + } +} + +pub struct Material2dPipeline { + pub mesh2d_pipeline: Mesh2dPipeline, + pub material2d_layout: BindGroupLayout, + pub vertex_shader: Option>, + pub fragment_shader: Option>, + marker: PhantomData, +} + +impl SpecializedPipeline for Material2dPipeline { + type Key = (Mesh2dPipelineKey, M::Key); + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut descriptor = self.mesh2d_pipeline.specialize(key.0); + if let Some(vertex_shader) = &self.vertex_shader { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = &self.fragment_shader { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + descriptor.layout = Some(vec![ + self.mesh2d_pipeline.view_layout.clone(), + self.material2d_layout.clone(), + self.mesh2d_pipeline.mesh_layout.clone(), + ]); + + M::specialize(key.1, &mut descriptor); + descriptor + } +} + +impl FromWorld for Material2dPipeline { + fn from_world(world: &mut World) -> Self { + let asset_server = world.get_resource::().unwrap(); + let render_device = world.get_resource::().unwrap(); + let material2d_layout = M::bind_group_layout(render_device); + + Material2dPipeline { + mesh2d_pipeline: world.get_resource::().unwrap().clone(), + material2d_layout, + vertex_shader: M::vertex_shader(asset_server), + fragment_shader: M::fragment_shader(asset_server), + marker: PhantomData, + } + } +} + +type DrawMaterial2d = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetMaterial2dBindGroup, + SetMesh2dBindGroup<2>, + DrawMesh2d, +); + +pub struct SetMaterial2dBindGroup(PhantomData); +impl EntityRenderCommand + for SetMaterial2dBindGroup +{ + type Param = (SRes>, SQuery>>); + fn render<'w>( + _view: Entity, + item: Entity, + (materials, query): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let material2d_handle = query.get(item).unwrap(); + let material2d = materials.into_inner().get(material2d_handle).unwrap(); + pass.set_bind_group( + I, + M::bind_group(material2d), + M::dynamic_uniform_indices(material2d), + ); + RenderCommandResult::Success + } +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_material2d_meshes( + transparent_draw_functions: Res>, + material2d_pipeline: Res>, + mut pipelines: ResMut>>, + mut pipeline_cache: ResMut, + msaa: Res, + render_meshes: Res>, + render_materials: Res>, + material2d_meshes: Query<(&Handle, &Mesh2dHandle, &Mesh2dUniform)>, + mut views: Query<(&VisibleEntities, &mut RenderPhase)>, +) { + if material2d_meshes.is_empty() { + return; + } + for (visible_entities, mut transparent_phase) in views.iter_mut() { + let draw_transparent_pbr = transparent_draw_functions + .read() + .get_id::>() + .unwrap(); + + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples); + + for visible_entity in &visible_entities.entities { + if let Ok((material2d_handle, mesh2d_handle, mesh2d_uniform)) = + material2d_meshes.get(*visible_entity) + { + if let Some(material2d) = render_materials.get(material2d_handle) { + let mut mesh2d_key = mesh_key; + if let Some(mesh) = render_meshes.get(&mesh2d_handle.0) { + if mesh.has_tangents { + mesh2d_key |= Mesh2dPipelineKey::VERTEX_TANGENTS; + } + mesh2d_key |= + Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); + } + + let specialized_key = M::key(material2d); + let pipeline_id = pipelines.specialize( + &mut pipeline_cache, + &material2d_pipeline, + (mesh2d_key, specialized_key), + ); + + let mesh_z = mesh2d_uniform.transform.w_axis.z; + transparent_phase.add(Transparent2d { + entity: *visible_entity, + draw_function: draw_transparent_pbr, + pipeline: pipeline_id, + // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the + // lowest sort key and getting closer should increase. As we have + // -z in front of the camera, the largest distance is -far with values increasing toward the + // camera. As such we can just use mesh_z as the distance + sort_key: FloatOrd(mesh_z), + // This material is not batched + batch_range: None, + }); + } + } + } + } +} + +/// A component bundle for entities with a [`Mesh2dHandle`] and a [`SpecializedMaterial2d`]. +#[derive(Bundle, Clone)] +pub struct MaterialMesh2dBundle { + pub mesh: Mesh2dHandle, + pub material: Handle, + pub transform: Transform, + pub global_transform: GlobalTransform, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, +} + +impl Default for MaterialMesh2dBundle { + fn default() -> Self { + Self { + mesh: Default::default(), + material: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), + } + } +} diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs new file mode 100644 index 0000000000..2b3177f705 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -0,0 +1,512 @@ +use bevy_app::Plugin; +use bevy_asset::{Assets, Handle, HandleUntyped}; +use bevy_ecs::{ + prelude::*, + system::{lifetimeless::*, SystemParamItem}, +}; +use bevy_math::{Mat4, Size}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + mesh::{GpuBufferInfo, Mesh}, + render_asset::RenderAssets, + render_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, + render_phase::{EntityRenderCommand, RenderCommandResult, TrackedRenderPass}, + render_resource::{std140::AsStd140, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo}, + view::{ComputedVisibility, ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms}, + RenderApp, RenderStage, +}; +use bevy_transform::components::GlobalTransform; + +/// Component for rendering with meshes in the 2d pipeline, usually with a [2d material](crate::Material2d) such as [`ColorMaterial`](crate::ColorMaterial). +/// +/// It wraps a [`Handle`] to differentiate from the 3d pipelines which use the handles directly as components +#[derive(Default, Clone, Component)] +pub struct Mesh2dHandle(pub Handle); + +impl From> for Mesh2dHandle { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +#[derive(Default)] +pub struct Mesh2dRenderPlugin; + +pub const MESH2D_VIEW_BIND_GROUP_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 6901431444735842434); +pub const MESH2D_STRUCT_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 8994673400261890424); +pub const MESH2D_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2971387252468633715); + +impl Plugin for Mesh2dRenderPlugin { + fn build(&self, app: &mut bevy_app::App) { + let mut shaders = app.world.get_resource_mut::>().unwrap(); + shaders.set_untracked( + MESH2D_SHADER_HANDLE, + Shader::from_wgsl(include_str!("mesh2d.wgsl")), + ); + shaders.set_untracked( + MESH2D_STRUCT_HANDLE, + Shader::from_wgsl(include_str!("mesh2d_struct.wgsl")) + .with_import_path("bevy_sprite::mesh2d_struct"), + ); + shaders.set_untracked( + MESH2D_VIEW_BIND_GROUP_HANDLE, + Shader::from_wgsl(include_str!("mesh2d_view_bind_group.wgsl")) + .with_import_path("bevy_sprite::mesh2d_view_bind_group"), + ); + + app.add_plugin(UniformComponentPlugin::::default()); + + app.sub_app_mut(RenderApp) + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Extract, extract_mesh2d) + .add_system_to_stage(RenderStage::Queue, queue_mesh2d_bind_group) + .add_system_to_stage(RenderStage::Queue, queue_mesh2d_view_bind_groups); + } +} + +#[derive(Component, AsStd140, Clone)] +pub struct Mesh2dUniform { + pub transform: Mat4, + pub inverse_transpose_model: Mat4, + pub flags: u32, +} + +// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + struct MeshFlags: u32 { + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +pub fn extract_mesh2d( + mut commands: Commands, + mut previous_len: Local, + query: Query<(Entity, &ComputedVisibility, &GlobalTransform, &Mesh2dHandle)>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, computed_visibility, transform, handle) in query.iter() { + if !computed_visibility.is_visible { + continue; + } + let transform = transform.compute_matrix(); + values.push(( + entity, + ( + Mesh2dHandle(handle.0.clone_weak()), + Mesh2dUniform { + flags: MeshFlags::empty().bits, + transform, + inverse_transpose_model: transform.inverse().transpose(), + }, + ), + )); + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + +#[derive(Clone)] +pub struct Mesh2dPipeline { + pub view_layout: BindGroupLayout, + pub mesh_layout: BindGroupLayout, + // This dummy white texture is to be used in place of optional textures + pub dummy_white_gpu_image: GpuImage, +} + +impl FromWorld for Mesh2dPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.get_resource::().unwrap(); + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + // View + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(ViewUniform::std140_size_static() as u64), + }, + count: None, + }, + ], + label: Some("mesh2d_view_layout"), + }); + + let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(Mesh2dUniform::std140_size_static() as u64), + }, + count: None, + }], + label: Some("mesh2d_layout"), + }); + // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures + let dummy_white_gpu_image = { + let image = Image::new_fill( + Extent3d::default(), + TextureDimension::D2, + &[255u8; 4], + TextureFormat::bevy_default(), + ); + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = render_device.create_sampler(&image.sampler_descriptor); + + let format_size = image.texture_descriptor.format.pixel_size(); + let render_queue = world.get_resource_mut::().unwrap(); + render_queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new( + image.texture_descriptor.size.width * format_size as u32, + ) + .unwrap(), + ), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + GpuImage { + texture, + texture_view, + sampler, + size: Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), + } + }; + Mesh2dPipeline { + view_layout, + mesh_layout, + dummy_white_gpu_image, + } + } +} + +impl Mesh2dPipeline { + pub fn get_image_texture<'a>( + &'a self, + gpu_images: &'a RenderAssets, + handle_option: &Option>, + ) -> Option<(&'a TextureView, &'a Sampler)> { + if let Some(handle) = handle_option { + let gpu_image = gpu_images.get(handle)?; + Some((&gpu_image.texture_view, &gpu_image.sampler)) + } else { + Some(( + &self.dummy_white_gpu_image.texture_view, + &self.dummy_white_gpu_image.sampler, + )) + } + } +} + +bitflags::bitflags! { + #[repr(transparent)] + // NOTE: Apparently quadro drivers support up to 64x MSAA. + // MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA. + // FIXME: make normals optional? + pub struct Mesh2dPipelineKey: u32 { + const NONE = 0; + const VERTEX_TANGENTS = (1 << 0); + const MSAA_RESERVED_BITS = Mesh2dPipelineKey::MSAA_MASK_BITS << Mesh2dPipelineKey::MSAA_SHIFT_BITS; + const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Mesh2dPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS << Mesh2dPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + } +} + +impl Mesh2dPipelineKey { + const MSAA_MASK_BITS: u32 = 0b111111; + const MSAA_SHIFT_BITS: u32 = 32 - 6; + const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; + const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3; + + pub fn from_msaa_samples(msaa_samples: u32) -> Self { + let msaa_bits = ((msaa_samples - 1) & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; + Mesh2dPipelineKey::from_bits(msaa_bits).unwrap() + } + + pub fn msaa_samples(&self) -> u32 { + ((self.bits >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + 1 + } + + pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self { + let primitive_topology_bits = ((primitive_topology as u32) + & Self::PRIMITIVE_TOPOLOGY_MASK_BITS) + << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + Mesh2dPipelineKey::from_bits(primitive_topology_bits).unwrap() + } + + pub fn primitive_topology(&self) -> PrimitiveTopology { + let primitive_topology_bits = + (self.bits >> Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS) & Self::PRIMITIVE_TOPOLOGY_MASK_BITS; + match primitive_topology_bits { + x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList, + x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList, + x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip, + x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList, + x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip, + _ => PrimitiveTopology::default(), + } + } +} + +impl SpecializedPipeline for Mesh2dPipeline { + type Key = Mesh2dPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let (vertex_array_stride, vertex_attributes) = + if key.contains(Mesh2dPipelineKey::VERTEX_TANGENTS) { + ( + 48, + vec![ + // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 12, + shader_location: 0, + }, + // Normal + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 1, + }, + // Uv (GOTCHA! uv is no longer third in the buffer due to how Mesh sorts attributes (alphabetically)) + VertexAttribute { + format: VertexFormat::Float32x2, + offset: 40, + shader_location: 2, + }, + // Tangent + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 24, + shader_location: 3, + }, + ], + ) + } else { + ( + 32, + vec![ + // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 12, + shader_location: 0, + }, + // Normal + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 1, + }, + // Uv + VertexAttribute { + format: VertexFormat::Float32x2, + offset: 24, + shader_location: 2, + }, + ], + ) + }; + let mut shader_defs = Vec::new(); + if key.contains(Mesh2dPipelineKey::VERTEX_TANGENTS) { + shader_defs.push(String::from("VERTEX_TANGENTS")); + } + + #[cfg(feature = "webgl")] + shader_defs.push(String::from("NO_ARRAY_TEXTURES_SUPPORT")); + + RenderPipelineDescriptor { + vertex: VertexState { + shader: MESH2D_SHADER_HANDLE.typed::(), + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![VertexBufferLayout { + array_stride: vertex_array_stride, + step_mode: VertexStepMode::Vertex, + attributes: vertex_attributes, + }], + }, + fragment: Some(FragmentState { + shader: MESH2D_SHADER_HANDLE.typed::(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + }], + }), + layout: Some(vec![self.view_layout.clone(), self.mesh_layout.clone()]), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: Some(Face::Back), + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: key.primitive_topology(), + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("transparent_mesh2d_pipeline".into()), + } + } +} + +pub struct Mesh2dBindGroup { + pub value: BindGroup, +} + +pub fn queue_mesh2d_bind_group( + mut commands: Commands, + mesh2d_pipeline: Res, + render_device: Res, + mesh2d_uniforms: Res>, +) { + if let Some(binding) = mesh2d_uniforms.uniforms().binding() { + commands.insert_resource(Mesh2dBindGroup { + value: render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: binding, + }], + label: Some("mesh2d_bind_group"), + layout: &mesh2d_pipeline.mesh_layout, + }), + }); + } +} + +#[derive(Component)] +pub struct Mesh2dViewBindGroup { + pub value: BindGroup, +} + +pub fn queue_mesh2d_view_bind_groups( + mut commands: Commands, + render_device: Res, + mesh2d_pipeline: Res, + view_uniforms: Res, + views: Query>, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + for entity in views.iter() { + let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding.clone(), + }], + label: Some("mesh2d_view_bind_group"), + layout: &mesh2d_pipeline.view_layout, + }); + + commands.entity(entity).insert(Mesh2dViewBindGroup { + value: view_bind_group, + }); + } + } +} + +pub struct SetMesh2dViewBindGroup; +impl EntityRenderCommand for SetMesh2dViewBindGroup { + type Param = SQuery<(Read, Read)>; + #[inline] + fn render<'w>( + view: Entity, + _item: Entity, + view_query: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let (view_uniform, mesh2d_view_bind_group) = view_query.get(view).unwrap(); + pass.set_bind_group(I, &mesh2d_view_bind_group.value, &[view_uniform.offset]); + + RenderCommandResult::Success + } +} + +pub struct SetMesh2dBindGroup; +impl EntityRenderCommand for SetMesh2dBindGroup { + type Param = ( + SRes, + SQuery>>, + ); + #[inline] + fn render<'w>( + _view: Entity, + item: Entity, + (mesh2d_bind_group, mesh2d_query): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let mesh2d_index = mesh2d_query.get(item).unwrap(); + pass.set_bind_group( + I, + &mesh2d_bind_group.into_inner().value, + &[mesh2d_index.index()], + ); + RenderCommandResult::Success + } +} + +pub struct DrawMesh2d; +impl EntityRenderCommand for DrawMesh2d { + type Param = (SRes>, SQuery>); + #[inline] + fn render<'w>( + _view: Entity, + item: Entity, + (meshes, mesh2d_query): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let mesh_handle = &mesh2d_query.get(item).unwrap().0; + if let Some(gpu_mesh) = meshes.into_inner().get(mesh_handle) { + pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + match &gpu_mesh.buffer_info { + GpuBufferInfo::Indexed { + buffer, + index_format, + count, + } => { + pass.set_index_buffer(buffer.slice(..), 0, *index_format); + pass.draw_indexed(0..*count, 0, 0..1); + } + GpuBufferInfo::NonIndexed { vertex_count } => { + pass.draw(0..*vertex_count, 0..1); + } + } + RenderCommandResult::Success + } else { + RenderCommandResult::Failure + } + } +} diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl new file mode 100644 index 0000000000..1e2b02eb64 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl @@ -0,0 +1,68 @@ +#import bevy_sprite::mesh2d_view_bind_group +#import bevy_sprite::mesh2d_struct + +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] normal: vec3; + [[location(2)]] uv: vec2; +#ifdef VERTEX_TANGENTS + [[location(3)]] tangent: vec4; +#endif +}; + +struct VertexOutput { + [[builtin(position)]] clip_position: vec4; + [[location(0)]] world_position: vec4; + [[location(1)]] world_normal: vec3; + [[location(2)]] uv: vec2; +#ifdef VERTEX_TANGENTS + [[location(3)]] world_tangent: vec4; +#endif +}; + +[[group(0), binding(0)]] +var view: View; + +[[group(2), binding(0)]] +var mesh: Mesh2d; + +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + let world_position = mesh.model * vec4(vertex.position, 1.0); + + var out: VertexOutput; + out.uv = vertex.uv; + out.world_position = world_position; + out.clip_position = view.view_proj * world_position; + out.world_normal = mat3x3( + mesh.inverse_transpose_model[0].xyz, + mesh.inverse_transpose_model[1].xyz, + mesh.inverse_transpose_model[2].xyz + ) * vertex.normal; +#ifdef VERTEX_TANGENTS + out.world_tangent = vec4( + mat3x3( + mesh.model[0].xyz, + mesh.model[1].xyz, + mesh.model[2].xyz + ) * vertex.tangent.xyz, + vertex.tangent.w + ); +#endif + return out; +} + +struct FragmentInput { + [[builtin(front_facing)]] is_front: bool; + [[location(0)]] world_position: vec4; + [[location(1)]] world_normal: vec3; + [[location(2)]] uv: vec2; +#ifdef VERTEX_TANGENTS + [[location(3)]] world_tangent: vec4; +#endif +}; + +[[stage(fragment)]] +fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { + return vec4(1.0, 0.0, 1.0, 1.0); +} \ No newline at end of file diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_struct.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_struct.wgsl new file mode 100644 index 0000000000..600fdcfa18 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_struct.wgsl @@ -0,0 +1,6 @@ +struct Mesh2d { + model: mat4x4; + inverse_transpose_model: mat4x4; + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32; +}; diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_view_bind_group.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_view_bind_group.wgsl new file mode 100644 index 0000000000..976df7b9c0 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_view_bind_group.wgsl @@ -0,0 +1,10 @@ +struct View { + view_proj: mat4x4; + inverse_view: mat4x4; + projection: mat4x4; + world_position: vec3; + near: f32; + far: f32; + width: f32; + height: f32; +}; diff --git a/crates/bevy_sprite/src/mesh2d/mod.rs b/crates/bevy_sprite/src/mesh2d/mod.rs new file mode 100644 index 0000000000..13383ea2d3 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/mod.rs @@ -0,0 +1,7 @@ +mod color_material; +mod material; +mod mesh; + +pub use color_material::*; +pub use material::*; +pub use mesh::*; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 3e5477f4b9..f4d0f035a9 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,30 +1,35 @@ -use std::{cmp::Ordering, ops::Range}; +use std::cmp::Ordering; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, Rect, Sprite, SPRITE_SHADER_HANDLE, }; -use bevy_asset::{AssetEvent, Assets, Handle}; +use bevy_asset::{AssetEvent, Assets, Handle, HandleId}; use bevy_core::FloatOrd; use bevy_core_pipeline::Transparent2d; use bevy_ecs::{ prelude::*, - system::{lifetimeless::*, SystemState}, + system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{const_vec2, Vec2}; +use bevy_reflect::Uuid; use bevy_render::{ color::Color, render_asset::RenderAssets, - render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass}, + render_phase::{ + BatchedPhaseItem, DrawFunctions, EntityRenderCommand, RenderCommand, RenderCommandResult, + RenderPhase, SetItemPipeline, TrackedRenderPass, + }, render_resource::{std140::AsStd140, *}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, Image}, - view::{ComputedVisibility, ViewUniform, ViewUniformOffset, ViewUniforms}, + view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, Visibility}, RenderWorld, }; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; +use copyless::VecHelper; pub struct SpritePipeline { view_layout: BindGroupLayout, @@ -79,9 +84,29 @@ impl FromWorld for SpritePipeline { } } -#[derive(Clone, Copy, Hash, PartialEq, Eq)] -pub struct SpritePipelineKey { - colored: bool, +bitflags::bitflags! { + #[repr(transparent)] + // NOTE: Apparently quadro drivers support up to 64x MSAA. + // MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA. + pub struct SpritePipelineKey: u32 { + const NONE = 0; + const COLORED = (1 << 0); + const MSAA_RESERVED_BITS = SpritePipelineKey::MSAA_MASK_BITS << SpritePipelineKey::MSAA_SHIFT_BITS; + } +} + +impl SpritePipelineKey { + const MSAA_MASK_BITS: u32 = 0b111111; + const MSAA_SHIFT_BITS: u32 = 32 - 6; + + pub fn from_msaa_samples(msaa_samples: u32) -> Self { + let msaa_bits = ((msaa_samples - 1) & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; + SpritePipelineKey::from_bits(msaa_bits).unwrap() + } + + pub fn msaa_samples(&self) -> u32 { + ((self.bits >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + 1 + } } impl SpecializedPipeline for SpritePipeline { @@ -105,7 +130,7 @@ impl SpecializedPipeline for SpritePipeline { ], }; let mut shader_defs = Vec::new(); - if key.colored { + if key.contains(SpritePipelineKey::COLORED) { shader_defs.push("COLORED".to_string()); vertex_buffer_layout.attributes.push(VertexAttribute { format: VertexFormat::Uint32, @@ -144,7 +169,7 @@ impl SpecializedPipeline for SpritePipeline { }, depth_stencil: None, multisample: MultisampleState { - count: 1, + count: key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, @@ -153,12 +178,17 @@ impl SpecializedPipeline for SpritePipeline { } } +#[derive(Component, Clone, Copy)] pub struct ExtractedSprite { - pub transform: Mat4, + pub transform: GlobalTransform, pub color: Color, - pub rect: Rect, - pub handle: Handle, - pub atlas_size: Option, + /// Select an area of the texture + pub rect: Option, + /// Change the on-screen size of the sprite + pub custom_size: Option, + /// Handle to the `Image` of this sprite + /// PERF: storing a `HandleId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) + pub image_handle_id: HandleId, pub flip_x: bool, pub flip_y: bool, } @@ -201,16 +231,10 @@ pub fn extract_sprite_events( pub fn extract_sprites( mut render_world: ResMut, - images: Res>, texture_atlases: Res>, - sprite_query: Query<( - &ComputedVisibility, - &Sprite, - &GlobalTransform, - &Handle, - )>, + sprite_query: Query<(&Visibility, &Sprite, &GlobalTransform, &Handle)>, atlas_query: Query<( - &ComputedVisibility, + &Visibility, &TextureAtlasSprite, &GlobalTransform, &Handle, @@ -218,46 +242,40 @@ pub fn extract_sprites( ) { let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); extracted_sprites.sprites.clear(); - for (computed_visibility, sprite, transform, handle) in sprite_query.iter() { - if !computed_visibility.is_visible { + for (visibility, sprite, transform, handle) in sprite_query.iter() { + if !visibility.is_visible { continue; } - if let Some(image) = images.get(handle) { - let size = image.texture_descriptor.size; - - 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(), - }); - }; + // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive + extracted_sprites.sprites.alloc().init(ExtractedSprite { + color: sprite.color, + transform: *transform, + // Use the full texture + rect: None, + // Pass the custom size + custom_size: sprite.custom_size, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id, + }); } - for (computed_visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { - if !computed_visibility.is_visible { + for (visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + if !visibility.is_visible { continue; } if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - if images.contains(&texture_atlas.texture) { - 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(), - }); - } + let rect = Some(texture_atlas.textures[atlas_sprite.index as usize]); + extracted_sprites.sprites.alloc().init(ExtractedSprite { + color: atlas_sprite.color, + transform: *transform, + // Select the area in the texture atlas + rect, + // Pass the custom size + custom_size: atlas_sprite.custom_size, + flip_x: atlas_sprite.flip_x, + flip_y: atlas_sprite.flip_y, + image_handle_id: texture_atlas.texture.id, + }); } } } @@ -293,177 +311,28 @@ impl Default for SpriteMeta { } } -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]), +const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; + +const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + const_vec2!([-0.5, -0.5]), + const_vec2!([0.5, -0.5]), + const_vec2!([0.5, 0.5]), + const_vec2!([-0.5, 0.5]), ]; -#[derive(Component)] +const QUAD_UVS: [Vec2; 4] = [ + const_vec2!([0., 1.]), + const_vec2!([1., 1.]), + const_vec2!([1., 0.]), + const_vec2!([0., 0.]), +]; + +#[derive(Component, Eq, PartialEq, Copy, Clone)] pub struct SpriteBatch { - range: Range, - handle: Handle, - z: f32, + image_handle_id: HandleId, colored: bool, } -pub fn prepare_sprites( - mut commands: Commands, - render_device: Res, - render_queue: Res, - mut sprite_meta: ResMut, - mut extracted_sprites: ResMut, -) { - sprite_meta.vertices.clear(); - sprite_meta.colored_vertices.clear(); - - // 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 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 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; - - 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; - } - - 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; - } - } - - // 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)] pub struct ImageBindGroups { values: HashMap, BindGroup>, @@ -471,8 +340,10 @@ pub struct ImageBindGroups { #[allow(clippy::too_many_arguments)] pub fn queue_sprites( + mut commands: Commands, draw_functions: Res>, render_device: Res, + render_queue: Res, mut sprite_meta: ResMut, view_uniforms: Res, sprite_pipeline: Res, @@ -480,7 +351,8 @@ pub fn queue_sprites( mut pipeline_cache: ResMut, mut image_bind_groups: ResMut, gpu_images: Res>, - sprite_batches: Query<(Entity, &SpriteBatch)>, + msaa: Res, + mut extracted_sprites: ResMut, mut views: Query<&mut RenderPhase>, events: Res, ) { @@ -494,6 +366,12 @@ pub fn queue_sprites( } if let Some(view_binding) = view_uniforms.uniforms.binding() { + let sprite_meta = &mut sprite_meta; + + // Clear the vertex buffers + sprite_meta.vertices.clear(); + sprite_meta.colored_vertices.clear(); + sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { binding: 0, @@ -502,104 +380,256 @@ pub fn queue_sprites( label: Some("sprite_view_bind_group"), 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 key = SpritePipelineKey::from_msaa_samples(msaa.samples); + let pipeline = pipelines.specialize(&mut pipeline_cache, &sprite_pipeline, key); let colored_pipeline = pipelines.specialize( &mut pipeline_cache, &sprite_pipeline, - SpritePipelineKey { colored: true }, + key | SpritePipelineKey::COLORED, ); + + // Vertex buffer indices + let mut index = 0; + let mut colored_index = 0; + + // FIXME: VisibleEntities is ignored for mut transparent_phase in views.iter_mut() { - for (entity, batch) in sprite_batches.iter() { - image_bind_groups - .values - .entry(batch.handle.clone_weak()) - .or_insert_with(|| { - let gpu_image = gpu_images.get(&batch.handle).unwrap(); - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&gpu_image.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: Some("sprite_material_bind_group"), - layout: &sprite_pipeline.material_layout, - }) - }); - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline: if batch.colored { - colored_pipeline + let extracted_sprites = &mut extracted_sprites.sprites; + let image_bind_groups = &mut *image_bind_groups; + + transparent_phase.items.reserve(extracted_sprites.len()); + + // Sort sprites by z for correct transparency and then by handle to improve batching + extracted_sprites.sort_unstable_by(|a, b| { + match a + .transform + .translation + .z + .partial_cmp(&b.transform.translation.z) + { + Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), + Some(other) => other, + } + }); + + // Impossible starting values that will be replaced on the first iteration + let mut current_batch = SpriteBatch { + image_handle_id: HandleId::Id(Uuid::nil(), u64::MAX), + colored: false, + }; + let mut current_batch_entity = Entity::from_raw(u32::MAX); + let mut current_image_size = Vec2::ZERO; + // Add a phase item for each sprite, and detect when succesive items can be batched. + // Spawn an entity with a `SpriteBatch` component for each possible batch. + // Compatible items share the same entity. + // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted + // by any other phase item (and they can interrupt other items from batching). + for extracted_sprite in extracted_sprites.iter() { + let new_batch = SpriteBatch { + image_handle_id: extracted_sprite.image_handle_id, + colored: extracted_sprite.color != Color::WHITE, + }; + if new_batch != current_batch { + // Set-up a new possible batch + if let Some(gpu_image) = + gpu_images.get(&Handle::weak(new_batch.image_handle_id)) + { + current_batch = new_batch; + current_image_size = Vec2::new(gpu_image.size.width, gpu_image.size.height); + current_batch_entity = commands.spawn_bundle((current_batch,)).id(); + + image_bind_groups + .values + .entry(Handle::weak(current_batch.image_handle_id)) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &gpu_image.texture_view, + ), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("sprite_material_bind_group"), + layout: &sprite_pipeline.material_layout, + }) + }); } else { - pipeline - }, - entity, - sort_key: FloatOrd(batch.z), + // Skip this item if the texture is not ready + continue; + } + } + + // Calculate vertex data for this item + + let mut uvs = QUAD_UVS; + if extracted_sprite.flip_x { + uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; + } + if extracted_sprite.flip_y { + uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; + } + + // By default, the size of the quad is the size of the texture + let mut quad_size = current_image_size; + + // If a rect is specified, adjust UVs and the size of the quad + if let Some(rect) = extracted_sprite.rect { + let rect_size = rect.size(); + for uv in &mut uvs { + *uv = (rect.min + *uv * rect_size) / current_image_size; + } + quad_size = rect_size; + } + + // Override the size if a custom one is specified + if let Some(custom_size) = extracted_sprite.custom_size { + quad_size = custom_size; + } + + // Apply size and global transform + let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { + extracted_sprite + .transform + .mul_vec3((quad_pos * quad_size).extend(0.)) + .into() }); + + // These items will be sorted by depth with other phase items + let sort_key = FloatOrd(extracted_sprite.transform.translation.z); + + // Store the vertex data and add the item to the render phase + 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 i in QUAD_INDICES.iter() { + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: positions[*i], + uv: uvs[*i].into(), + color, + }); + } + let item_start = colored_index; + colored_index += QUAD_INDICES.len() as u32; + let item_end = colored_index; + + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline: colored_pipeline, + entity: current_batch_entity, + sort_key, + batch_range: Some(item_start..item_end), + }); + } else { + for i in QUAD_INDICES.iter() { + sprite_meta.vertices.push(SpriteVertex { + position: positions[*i], + uv: uvs[*i].into(), + }); + } + let item_start = index; + index += QUAD_INDICES.len() as u32; + let item_end = index; + + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline, + entity: current_batch_entity, + sort_key, + batch_range: Some(item_start..item_end), + }); + } } } + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); + sprite_meta + .colored_vertices + .write_buffer(&render_device, &render_queue); } } -pub struct DrawSprite { - params: SystemState<( - SRes, - SRes, - SRes, - SQuery>, - SQuery>, - )>, -} +pub type DrawSprite = ( + SetItemPipeline, + SetSpriteViewBindGroup<0>, + SetSpriteTextureBindGroup<1>, + DrawSpriteBatch, +); -impl DrawSprite { - pub fn new(world: &mut World) -> Self { - Self { - params: SystemState::new(world), - } - } -} +pub struct SetSpriteViewBindGroup; +impl EntityRenderCommand for SetSpriteViewBindGroup { + type Param = (SRes, SQuery>); -impl Draw for DrawSprite { - fn draw<'w>( - &mut self, - world: &'w World, - pass: &mut TrackedRenderPass<'w>, + fn render<'w>( view: Entity, - item: &Transparent2d, - ) { - 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 sprite_batch = sprites.get(item.entity).unwrap(); - if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) { - pass.set_render_pipeline(pipeline); - 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(), - &[view_uniform.offset], - ); - pass.set_bind_group( - 1, - image_bind_groups.values.get(&sprite_batch.handle).unwrap(), - &[], - ); - - pass.draw(sprite_batch.range.clone(), 0..1); - } + _item: Entity, + (sprite_meta, view_query): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let view_uniform = view_query.get(view).unwrap(); + pass.set_bind_group( + I, + sprite_meta.into_inner().view_bind_group.as_ref().unwrap(), + &[view_uniform.offset], + ); + RenderCommandResult::Success + } +} +pub struct SetSpriteTextureBindGroup; +impl EntityRenderCommand for SetSpriteTextureBindGroup { + type Param = (SRes, SQuery>); + + fn render<'w>( + _view: Entity, + item: Entity, + (image_bind_groups, query_batch): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let sprite_batch = query_batch.get(item).unwrap(); + let image_bind_groups = image_bind_groups.into_inner(); + + pass.set_bind_group( + 1, + image_bind_groups + .values + .get(&Handle::weak(sprite_batch.image_handle_id)) + .unwrap(), + &[], + ); + RenderCommandResult::Success + } +} + +pub struct DrawSpriteBatch; +impl RenderCommand

for DrawSpriteBatch { + type Param = (SRes, SQuery>); + + fn render<'w>( + _view: Entity, + item: &P, + (sprite_meta, query_batch): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let sprite_batch = query_batch.get(item.entity()).unwrap(); + let sprite_meta = sprite_meta.into_inner(); + 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.draw(item.batch_range().as_ref().unwrap().clone(), 0..1); + RenderCommandResult::Success } } diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index 84d2e3872a..49d366fa64 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -28,6 +28,9 @@ pub struct TextureAtlasSprite { pub index: usize, pub flip_x: bool, pub flip_y: bool, + /// An optional custom size for the sprite that will be used when rendering, instead of the size + /// of the sprite's image in the atlas + pub custom_size: Option, } impl Default for TextureAtlasSprite { @@ -37,6 +40,7 @@ impl Default for TextureAtlasSprite { color: Color::WHITE, flip_x: false, flip_y: false, + custom_size: None, } } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index fcf6c1e604..3eb062f44e 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -51,7 +51,7 @@ impl Plugin for TextPlugin { let render_app = app.sub_app_mut(RenderApp); render_app.add_system_to_stage( RenderStage::Extract, - extract_text2d_sprite.after(SpriteSystem::ExtractSprite), + extract_text2d_sprite.after(SpriteSystem::ExtractSprites), ); } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 789dab4160..bee204e755 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -5,8 +5,8 @@ use bevy_ecs::{ query::{Changed, QueryState, With}, system::{Local, Query, QuerySet, Res, ResMut}, }; -use bevy_math::{Mat4, Size, Vec3}; -use bevy_render::{texture::Image, RenderWorld}; +use bevy_math::{Size, Vec3}; +use bevy_render::{texture::Image, view::Visibility, RenderWorld}; use bevy_sprite::{ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_window::Windows; @@ -24,6 +24,7 @@ pub struct Text2dBundle { pub transform: Transform, pub global_transform: GlobalTransform, pub text_2d_size: Text2dSize, + pub visibility: Visibility, } impl Default for Text2dBundle { @@ -35,6 +36,7 @@ impl Default for Text2dBundle { text_2d_size: Text2dSize { size: Size::default(), }, + visibility: Default::default(), } } } @@ -44,16 +46,20 @@ pub fn extract_text2d_sprite( texture_atlases: Res>, text_pipeline: Res, windows: Res, - text2d_query: Query<(Entity, &Text, &GlobalTransform, &Text2dSize)>, + text2d_query: Query<(Entity, &Visibility, &Text, &GlobalTransform, &Text2dSize)>, ) { let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + let scale_factor = if let Some(window) = windows.get_primary() { window.scale_factor() as f32 } else { 1. }; - for (entity, text, transform, calculated_size) in text2d_query.iter() { + for (entity, visibility, text, transform, calculated_size) in text2d_query.iter() { + if !visibility.is_visible { + continue; + } let (width, height) = (calculated_size.size.width, calculated_size.size.height); if let Some(text_layout) = text_pipeline.get_glyphs(&entity) { @@ -68,6 +74,9 @@ pub fn extract_text2d_sprite( HorizontalAlign::Right => Vec3::new(-width, 0.0, 0.0), }; + let mut text_transform = *transform; + text_transform.scale /= scale_factor; + for text_glyph in text_glyphs { let color = text.sections[text_glyph.section_index] .style @@ -78,22 +87,20 @@ pub fn extract_text2d_sprite( .unwrap(); let handle = atlas.texture.clone_weak(); let index = text_glyph.atlas_info.glyph_index as usize; - let rect = atlas.textures[index]; - let atlas_size = Some(atlas.size); + let rect = Some(atlas.textures[index]); - let transform = - Mat4::from_rotation_translation(transform.rotation, transform.translation) - * Mat4::from_scale(transform.scale / scale_factor) - * Mat4::from_translation( - alignment_offset * scale_factor + text_glyph.position.extend(0.), - ); + let glyph_transform = Transform::from_translation( + alignment_offset * scale_factor + text_glyph.position.extend(0.), + ); + + let transform = text_transform.mul_transform(glyph_transform); extracted_sprites.sprites.push(ExtractedSprite { transform, color, rect, - handle, - atlas_size, + custom_size: None, + image_handle_id: handle.id, flip_x: false, flip_y: false, }); diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 328def43ff..83b608bf0c 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -152,7 +152,7 @@ impl EntityRenderCommand for SetUiViewBindGroup { (ui_meta, view_query): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - let view_uniform = view_query.get(view).unwrap(); // TODO: store bind group as component? + let view_uniform = view_query.get(view).unwrap(); pass.set_bind_group( I, ui_meta.into_inner().view_bind_group.as_ref().unwrap(), diff --git a/examples/2d/mesh2d.rs b/examples/2d/mesh2d.rs new file mode 100644 index 0000000000..70a76ca023 --- /dev/null +++ b/examples/2d/mesh2d.rs @@ -0,0 +1,22 @@ +use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + commands.spawn_bundle(MaterialMesh2dBundle { + mesh: meshes.add(Mesh::from(shape::Quad::default())).into(), + transform: Transform::default().with_scale(Vec3::splat(128.)), + material: materials.add(ColorMaterial::from(Color::PURPLE)), + ..Default::default() + }); +} diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs new file mode 100644 index 0000000000..371645839a --- /dev/null +++ b/examples/2d/mesh2d_manual.rs @@ -0,0 +1,358 @@ +use bevy::{ + core::FloatOrd, + core_pipeline::Transparent2d, + prelude::*, + reflect::TypeUuid, + render::{ + mesh::Indices, + render_asset::RenderAssets, + render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::{ + BlendState, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace, + MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache, + RenderPipelineDescriptor, SpecializedPipeline, SpecializedPipelines, TextureFormat, + VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + }, + texture::BevyDefault, + view::VisibleEntities, + RenderApp, RenderStage, + }, + sprite::{ + DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dUniform, + SetMesh2dBindGroup, SetMesh2dViewBindGroup, + }, +}; + +/// This example shows how to manually render 2d items using "mid level render apis" with a custom pipeline for 2d meshes +/// It doesn't use the [`Material2d`] abstraction, but changes the vertex buffer to include vertex color +/// Check out the "mesh2d" example for simpler / higher level 2d meshes +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ColoredMesh2dPlugin) + .add_startup_system(star) + .run(); +} + +fn star( + mut commands: Commands, + // We will add a new Mesh for the star being created + mut meshes: ResMut>, +) { + // Let's define the mesh for the object we want to draw: a nice star. + // We will specify here what kind of topology is used to define the mesh, + // that is, how triangles are built from the vertices. We will use a + // triangle list, meaning that each vertex of the triangle has to be + // specified. + let mut star = Mesh::new(PrimitiveTopology::TriangleList); + + // Vertices need to have a position attribute. We will use the following + // vertices (I hope you can spot the star in the schema). + // + // 1 + // + // 10 2 + // 9 0 3 + // 8 4 + // 6 + // 7 5 + // + // These vertices are specificed in 3D space. + let mut v_pos = vec![[0.0, 0.0, 0.0]]; + for i in 0..10 { + // Angle of each vertex is 1/10 of TAU, plus PI/2 for positioning vertex 0 + let a = std::f32::consts::FRAC_PI_2 - i as f32 * std::f32::consts::TAU / 10.0; + // Radius of internal vertices (2, 4, 6, 8, 10) is 100, it's 200 for external + let r = (1 - i % 2) as f32 * 100.0 + 100.0; + // Add the vertex coordinates + v_pos.push([r * a.cos(), r * a.sin(), 0.0]); + } + // Set the position attribute + star.set_attribute(Mesh::ATTRIBUTE_POSITION, v_pos); + // And a RGB color attribute as well + let mut v_color = vec![[0.0, 0.0, 0.0, 1.0]]; + v_color.extend_from_slice(&[[1.0, 1.0, 0.0, 1.0]; 10]); + star.set_attribute(Mesh::ATTRIBUTE_COLOR, v_color); + + // Now, we specify the indices of the vertex that are going to compose the + // triangles in our star. Vertices in triangles have to be specified in CCW + // winding (that will be the front face, colored). Since we are using + // triangle list, we will specify each triangle as 3 vertices + // First triangle: 0, 2, 1 + // Second triangle: 0, 3, 2 + // Third triangle: 0, 4, 3 + // etc + // Last triangle: 0, 1, 10 + let mut indices = vec![0, 1, 10]; + for i in 2..=10 { + indices.extend_from_slice(&[0, i, i - 1]); + } + star.set_indices(Some(Indices::U32(indices))); + + // We can now spawn the entities for the star and the camera + commands.spawn_bundle(( + // We use a marker component to identify the custom colored meshes + ColoredMesh2d::default(), + // The `Handle` needs to be wrapped in a `Mesh2dHandle` to use 2d rendering instead of 3d + Mesh2dHandle(meshes.add(star)), + // These other components are needed for 2d meshes to be rendered + Transform::default(), + GlobalTransform::default(), + Visibility::default(), + ComputedVisibility::default(), + )); + commands + // And use an orthographic projection + .spawn_bundle(OrthographicCameraBundle::new_2d()); +} + +/// A marker component for colored 2d meshes +#[derive(Component, Default)] +pub struct ColoredMesh2d; + +/// Custom pipeline for 2d meshes with vertex colors +pub struct ColoredMesh2dPipeline { + /// this pipeline wraps the standard [`Mesh2dPipeline`] + mesh2d_pipeline: Mesh2dPipeline, +} + +impl FromWorld for ColoredMesh2dPipeline { + fn from_world(world: &mut World) -> Self { + Self { + mesh2d_pipeline: Mesh2dPipeline::from_world(world), + } + } +} + +// We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline` +impl SpecializedPipeline for ColoredMesh2dPipeline { + type Key = Mesh2dPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + // Customize how to store the meshes' vertex attributes in the vertex buffer + // Our meshes only have position and color + let vertex_attributes = vec![ + // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) + VertexAttribute { + format: VertexFormat::Float32x3, + // this offset is the size of the color attribute, which is stored first + offset: 16, + // position is available at location 0 in the shader + shader_location: 0, + }, + // Color + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 0, + shader_location: 1, + }, + ]; + // This is the sum of the size of position and color attributes (12 + 16 = 28) + let vertex_array_stride = 28; + + RenderPipelineDescriptor { + vertex: VertexState { + // Use our custom shader + shader: COLORED_MESH2D_SHADER_HANDLE.typed::(), + entry_point: "vertex".into(), + shader_defs: Vec::new(), + // Use our custom vertex buffer + buffers: vec![VertexBufferLayout { + array_stride: vertex_array_stride, + step_mode: VertexStepMode::Vertex, + attributes: vertex_attributes, + }], + }, + fragment: Some(FragmentState { + // Use our custom shader + shader: COLORED_MESH2D_SHADER_HANDLE.typed::(), + shader_defs: Vec::new(), + entry_point: "fragment".into(), + targets: vec![ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + }], + }), + // Use the two standard uniforms for 2d meshes + layout: Some(vec![ + // Bind group 0 is the view uniform + self.mesh2d_pipeline.view_layout.clone(), + // Bind group 1 is the mesh uniform + self.mesh2d_pipeline.mesh_layout.clone(), + ]), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: Some(Face::Back), + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: key.primitive_topology(), + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("colored_mesh2d_pipeline".into()), + } + } +} + +// This specifies how to render a colored 2d mesh +type DrawColoredMesh2d = ( + // Set the pipeline + SetItemPipeline, + // Set the view uniform as bind group 0 + SetMesh2dViewBindGroup<0>, + // Set the mesh uniform as bind group 1 + SetMesh2dBindGroup<1>, + // Draw the mesh + DrawMesh2d, +); + +// The custom shader can be inline like here, included from another file at build time +// using `include_str!()`, or loaded like any other asset with `asset_server.load()`. +const COLORED_MESH2D_SHADER: &str = r" +// Import the standard 2d mesh uniforms and set their bind groups +#import bevy_sprite::mesh2d_view_bind_group +[[group(0), binding(0)]] +var view: View; +#import bevy_sprite::mesh2d_struct +[[group(1), binding(0)]] +var mesh: Mesh2d; + +// The structure of the vertex buffer is as specified in `specialize()` +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] color: vec4; +}; + +struct VertexOutput { + // The vertex shader must set the on-screen position of the vertex + [[builtin(position)]] clip_position: vec4; + // We pass the vertex color to the framgent shader in location 0 + [[location(0)]] color: vec4; +}; + +/// Entry point for the vertex shader +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + // Project the world position of the mesh into screen position + out.clip_position = view.view_proj * mesh.model * vec4(vertex.position, 1.0); + out.color = vertex.color; + return out; +} + +// The input of the fragment shader must correspond to the output of the vertex shader for all `location`s +struct FragmentInput { + // The color is interpolated between vertices by default + [[location(0)]] color: vec4; +}; + +/// Entry point for the fragment shader +[[stage(fragment)]] +fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { + return in.color; +} +"; + +/// Plugin that renders [`ColoredMesh2d`]s +pub struct ColoredMesh2dPlugin; + +/// Handle to the custom shader with a unique random ID +pub const COLORED_MESH2D_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 13828845428412094821); + +impl Plugin for ColoredMesh2dPlugin { + fn build(&self, app: &mut App) { + // Load our custom shader + let mut shaders = app.world.get_resource_mut::>().unwrap(); + shaders.set_untracked( + COLORED_MESH2D_SHADER_HANDLE, + Shader::from_wgsl(COLORED_MESH2D_SHADER), + ); + + // Register our custom draw function and pipeline, and add our render systems + let render_app = app.get_sub_app_mut(RenderApp).unwrap(); + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Extract, extract_colored_mesh2d) + .add_system_to_stage(RenderStage::Queue, queue_colored_mesh2d); + } +} + +/// Extract the [`ColoredMesh2d`] marker component into the render app +pub fn extract_colored_mesh2d( + mut commands: Commands, + mut previous_len: Local, + query: Query<(Entity, &ComputedVisibility), With>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, computed_visibility) in query.iter() { + if !computed_visibility.is_visible { + continue; + } + values.push((entity, (ColoredMesh2d,))); + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + +/// Queue the 2d meshes marked with [`ColoredMesh2d`] using our custom pipeline and draw function +#[allow(clippy::too_many_arguments)] +pub fn queue_colored_mesh2d( + transparent_draw_functions: Res>, + colored_mesh2d_pipeline: Res, + mut pipelines: ResMut>, + mut pipeline_cache: ResMut, + msaa: Res, + render_meshes: Res>, + colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With>, + mut views: Query<(&VisibleEntities, &mut RenderPhase)>, +) { + if colored_mesh2d.is_empty() { + return; + } + // Iterate each view (a camera is a view) + for (visible_entities, mut transparent_phase) in views.iter_mut() { + let draw_colored_mesh2d = transparent_draw_functions + .read() + .get_id::() + .unwrap(); + + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples); + + // Queue all entities visible to that view + for visible_entity in &visible_entities.entities { + if let Ok((mesh2d_handle, mesh2d_uniform)) = colored_mesh2d.get(*visible_entity) { + // Get our specialized pipeline + let mut mesh2d_key = mesh_key; + if let Some(mesh) = render_meshes.get(&mesh2d_handle.0) { + mesh2d_key |= + Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); + } + + let pipeline_id = + pipelines.specialize(&mut pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key); + + let mesh_z = mesh2d_uniform.transform.w_axis.z; + transparent_phase.add(Transparent2d { + entity: *visible_entity, + draw_function: draw_colored_mesh2d, + pipeline: pipeline_id, + // The 2d render items are sorted according to their z value before rendering, + // in order to get correct transparency + sort_key: FloatOrd(mesh_z), + // This material is not batched + batch_range: None, + }); + } + } + } +} diff --git a/examples/README.md b/examples/README.md index 84d61d12be..c540e58af7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -84,6 +84,8 @@ Example | File | Description --- | --- | --- `contributors` | [`2d/contributors.rs`](./2d/contributors.rs) | Displays each contributor as a bouncy bevy-ball! `many_sprites` | [`2d/many_sprites.rs`](./2d/many_sprites.rs) | Displays many sprites in a grid arragement! Used for performance testing. +`mesh2d` | [`2d/mesh2d.rs`](./2d/mesh2d.rs) | Renders a 2d mesh +`mesh2d_manual` | [`2d/mesh2d_manual.rs`](./2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis. `rect` | [`2d/rect.rs`](./2d/rect.rs) | Renders a rectangle `sprite` | [`2d/sprite.rs`](./2d/sprite.rs) | Renders a sprite `sprite_sheet` | [`2d/sprite_sheet.rs`](./2d/sprite_sheet.rs) | Renders an animated sprite