Add 2d meshes and materials (#3460)

# Objective

The current 2d rendering is specialized to render sprites, we need a generic way to render 2d items, using meshes and materials like we have for 3d.

## Solution

I cloned a good part of `bevy_pbr` into `bevy_sprite/src/mesh2d`, removed lighting and pbr itself, adapted it to 2d rendering, added a `ColorMaterial`, and modified the sprite rendering to break batches around 2d meshes.

~~The PR is a bit crude; I tried to change as little as I could in both the parts copied from 3d and the current sprite rendering to make reviewing easier. In the future, I expect we could make the sprite rendering a normal 2d material, cleanly integrated with the rest.~~ _edit: see <https://github.com/bevyengine/bevy/pull/3460#issuecomment-1003605194>_

## Remaining work

- ~~don't require mesh normals~~ _out of scope_
- ~~add an example~~ _done_
- support 2d meshes & materials in the UI?
- bikeshed names (I didn't think hard about naming, please check if it's fine)

## Remaining questions

- ~~should we add a depth buffer to 2d now that there are 2d meshes?~~ _let's revisit that when we have an opaque render phase_
- ~~should we add MSAA support to the sprites, or remove it from the 2d meshes?~~ _I added MSAA to sprites since it's really needed for 2d meshes_
- ~~how to customize vertex attributes?~~ _#3120_



Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
davier 2022-01-08 01:29:08 +00:00
parent 32f7997c56
commit c2da7800e3
29 changed files with 2262 additions and 360 deletions

View file

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

View file

@ -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::<ClearColor>();
@ -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::<Transparent2d>)
.add_system_to_stage(
RenderStage::PhaseSort,
sort_phase_system::<Transparent2d>
.label(CorePipelineRenderSystems::SortTransparent2d),
)
.add_system_to_stage(
RenderStage::PhaseSort,
batch_phase_system::<Transparent2d>
.after(CorePipelineRenderSystems::SortTransparent2d),
)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Opaque3d>)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<AlphaMask3d>)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Transparent3d>);
@ -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<Range<u32>>,
}
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<Range<u32>> {
&self.batch_range
}
fn batch_range_mut(&mut self) -> &mut Option<Range<u32>> {
&mut self.batch_range
}
}
pub struct Opaque3d {
pub distance: f32,
pub pipeline: CachedPipelineId,

View file

@ -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,
};

View file

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

View file

@ -53,3 +53,4 @@ hex = "0.4.2"
hexasphere = "6.0.0"
parking_lot = "0.11.0"
regex = "1.5"
copyless = "0.1.5"

View file

@ -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<u32>>;
/// Range in the vertex buffer of this item
fn batch_range_mut(&mut self) -> &mut Option<Range<u32>>;
/// 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<P: EntityPhaseItem, E: EntityRenderCommand> RenderCommand<P> for E {
type Param = E::Param;

View file

@ -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<I: PhaseItem> {
@ -22,7 +24,7 @@ impl<I: PhaseItem> RenderPhase<I> {
/// 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<I: PhaseItem> RenderPhase<I> {
}
}
impl<I: BatchedPhaseItem> RenderPhase<I> {
/// 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<I: PhaseItem>(mut render_phases: Query<&mut RenderPhase<I>>) {
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<I: BatchedPhaseItem>(mut render_phases: Query<&mut RenderPhase<I>>) {
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<Range<u32>>,
}
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<std::ops::Range<u32>> {
&self.batch_range
}
fn batch_range_mut(&mut self) -> &mut Option<std::ops::Range<u32>> {
&mut self.batch_range
}
}
let mut render_phase = RenderPhase::<TestPhaseItem>::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);
}
}

View file

@ -3,6 +3,7 @@ use crate::{
renderer::{RenderDevice, RenderQueue},
};
use bevy_core::{cast_slice, Pod};
use copyless::VecHelper;
use wgpu::BufferUsages;
pub struct BufferVec<T: Pod> {
@ -55,7 +56,7 @@ impl<T: Pod> BufferVec<T> {
pub fn push(&mut self, value: T) -> usize {
let index = self.values.len();
self.values.push(value);
self.values.alloc().init(value);
index
}

View file

@ -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,
})
}
}

View file

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

View file

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

View file

@ -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<Image>,
/// 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,
}

View file

@ -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::<Assets<Shader>>().unwrap();
let sprite_shader = Shader::from_wgsl(include_str!("render/sprite.wgsl"));
shaders.set_untracked(SPRITE_SHADER_HANDLE, sprite_shader);
app.add_asset::<TextureAtlas>().register_type::<Sprite>();
app.add_asset::<TextureAtlas>()
.register_type::<Sprite>()
.add_plugin(Mesh2dRenderPlugin)
.add_plugin(ColorMaterialPlugin);
let render_app = app.sub_app_mut(RenderApp);
render_app
.init_resource::<ImageBindGroups>()
@ -62,20 +67,12 @@ impl Plugin for SpritePlugin {
.init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>()
.init_resource::<SpriteAssetEvents>()
.add_render_command::<Transparent2d, DrawSprite>()
.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::<DrawFunctions<Transparent2d>>()
.unwrap()
.write()
.add(draw_sprite);
}
}

View file

@ -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::<Assets<Shader>>().unwrap();
shaders.set_untracked(
COLOR_MATERIAL_SHADER_HANDLE,
Shader::from_wgsl(include_str!("color_material.wgsl")),
);
app.add_plugin(Material2dPlugin::<ColorMaterial>::default());
app.world
.get_resource_mut::<Assets<ColorMaterial>>()
.unwrap()
.set_untracked(
Handle::<ColorMaterial>::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<Handle<Image>>,
}
impl Default for ColorMaterial {
fn default() -> Self {
ColorMaterial {
color: Color::rgb(1.0, 0.0, 1.0),
texture: None,
}
}
}
impl From<Color> for ColorMaterial {
fn from(color: Color) -> Self {
ColorMaterial {
color,
..Default::default()
}
}
}
impl From<Handle<Image>> for ColorMaterial {
fn from(texture: Handle<Image>) -> 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<Handle<Image>>,
}
impl RenderAsset for ColorMaterial {
type ExtractedAsset = ColorMaterial;
type PreparedAsset = GpuColorMaterial;
type Param = (
SRes<RenderDevice>,
SRes<Material2dPipeline<ColorMaterial>>,
SRes<RenderAssets<Image>>,
);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
material: Self::ExtractedAsset,
(render_device, color_pipeline, gpu_images): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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<Handle<Shader>> {
Some(COLOR_MATERIAL_SHADER_HANDLE.typed())
}
#[inline]
fn bind_group(render_asset: &<Self as RenderAsset>::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<ColorMaterial>;

View file

@ -0,0 +1,41 @@
#import bevy_sprite::mesh2d_view_bind_group
#import bevy_sprite::mesh2d_struct
struct ColorMaterial {
color: vec4<f32>;
// '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<uniform> view: View;
[[group(1), binding(0)]]
var<uniform> material: ColorMaterial;
[[group(1), binding(1)]]
var texture: texture_2d<f32>;
[[group(1), binding(2)]]
var texture_sampler: sampler;
[[group(2), binding(0)]]
var<uniform> mesh: Mesh2d;
struct FragmentInput {
[[builtin(front_facing)]] is_front: bool;
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
};
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
var output_color: vec4<f32> = material.color;
if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) {
output_color = output_color * textureSample(texture, texture_sampler, in.uv);
}
return output_color;
}

View file

@ -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: &<Self as RenderAsset>::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<Handle<Shader>> {
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<Handle<Shader>> {
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: &<Self as RenderAsset>::PreparedAsset) -> &[u32] {
&[]
}
}
impl<M: Material2d> SpecializedMaterial2d for M {
type Key = ();
#[inline]
fn key(_material: &<Self as RenderAsset>::PreparedAsset) -> Self::Key {}
#[inline]
fn specialize(_key: Self::Key, _descriptor: &mut RenderPipelineDescriptor) {}
#[inline]
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup {
<M as Material2d>::bind_group(material)
}
#[inline]
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout {
<M as Material2d>::bind_group_layout(render_device)
}
#[inline]
fn vertex_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
<M as Material2d>::vertex_shader(asset_server)
}
#[inline]
fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
<M as Material2d>::fragment_shader(asset_server)
}
#[allow(unused_variables)]
#[inline]
fn dynamic_uniform_indices(material: &<Self as RenderAsset>::PreparedAsset) -> &[u32] {
<M as Material2d>::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: &<Self as RenderAsset>::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: &<Self as RenderAsset>::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<Handle<Shader>> {
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<Handle<Shader>> {
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: &<Self as RenderAsset>::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<M: SpecializedMaterial2d>(PhantomData<M>);
impl<M: SpecializedMaterial2d> Default for Material2dPlugin<M> {
fn default() -> Self {
Self(Default::default())
}
}
impl<M: SpecializedMaterial2d> Plugin for Material2dPlugin<M> {
fn build(&self, app: &mut App) {
app.add_asset::<M>()
.add_plugin(ExtractComponentPlugin::<Handle<M>>::default())
.add_plugin(RenderAssetPlugin::<M>::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<Transparent2d, DrawMaterial2d<M>>()
.init_resource::<Material2dPipeline<M>>()
.init_resource::<SpecializedPipelines<Material2dPipeline<M>>>()
.add_system_to_stage(RenderStage::Queue, queue_material2d_meshes::<M>);
}
}
}
pub struct Material2dPipeline<M: SpecializedMaterial2d> {
pub mesh2d_pipeline: Mesh2dPipeline,
pub material2d_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
marker: PhantomData<M>,
}
impl<M: SpecializedMaterial2d> SpecializedPipeline for Material2dPipeline<M> {
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<M: SpecializedMaterial2d> FromWorld for Material2dPipeline<M> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.get_resource::<AssetServer>().unwrap();
let render_device = world.get_resource::<RenderDevice>().unwrap();
let material2d_layout = M::bind_group_layout(render_device);
Material2dPipeline {
mesh2d_pipeline: world.get_resource::<Mesh2dPipeline>().unwrap().clone(),
material2d_layout,
vertex_shader: M::vertex_shader(asset_server),
fragment_shader: M::fragment_shader(asset_server),
marker: PhantomData,
}
}
}
type DrawMaterial2d<M> = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMaterial2dBindGroup<M, 1>,
SetMesh2dBindGroup<2>,
DrawMesh2d,
);
pub struct SetMaterial2dBindGroup<M: SpecializedMaterial2d, const I: usize>(PhantomData<M>);
impl<M: SpecializedMaterial2d, const I: usize> EntityRenderCommand
for SetMaterial2dBindGroup<M, I>
{
type Param = (SRes<RenderAssets<M>>, SQuery<Read<Handle<M>>>);
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<M: SpecializedMaterial2d>(
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
material2d_pipeline: Res<Material2dPipeline<M>>,
mut pipelines: ResMut<SpecializedPipelines<Material2dPipeline<M>>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderAssets<M>>,
material2d_meshes: Query<(&Handle<M>, &Mesh2dHandle, &Mesh2dUniform)>,
mut views: Query<(&VisibleEntities, &mut RenderPhase<Transparent2d>)>,
) {
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::<DrawMaterial2d<M>>()
.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<M: SpecializedMaterial2d> {
pub mesh: Mesh2dHandle,
pub material: Handle<M>,
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<M: SpecializedMaterial2d> Default for MaterialMesh2dBundle<M> {
fn default() -> Self {
Self {
mesh: Default::default(),
material: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
visibility: Default::default(),
computed_visibility: Default::default(),
}
}
}

View file

@ -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<Mesh>`] to differentiate from the 3d pipelines which use the handles directly as components
#[derive(Default, Clone, Component)]
pub struct Mesh2dHandle(pub Handle<Mesh>);
impl From<Handle<Mesh>> for Mesh2dHandle {
fn from(handle: Handle<Mesh>) -> 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::<Assets<Shader>>().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::<Mesh2dUniform>::default());
app.sub_app_mut(RenderApp)
.init_resource::<Mesh2dPipeline>()
.init_resource::<SpecializedPipelines<Mesh2dPipeline>>()
.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<usize>,
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::<RenderDevice>().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::<RenderQueue>().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<Image>,
handle_option: &Option<Handle<Image>>,
) -> 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::<Shader>(),
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>(),
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<Mesh2dPipeline>,
render_device: Res<RenderDevice>,
mesh2d_uniforms: Res<ComponentUniforms<Mesh2dUniform>>,
) {
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<RenderDevice>,
mesh2d_pipeline: Res<Mesh2dPipeline>,
view_uniforms: Res<ViewUniforms>,
views: Query<Entity, With<ExtractedView>>,
) {
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<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetMesh2dViewBindGroup<I> {
type Param = SQuery<(Read<ViewUniformOffset>, Read<Mesh2dViewBindGroup>)>;
#[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<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetMesh2dBindGroup<I> {
type Param = (
SRes<Mesh2dBindGroup>,
SQuery<Read<DynamicUniformIndex<Mesh2dUniform>>>,
);
#[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<RenderAssets<Mesh>>, SQuery<Read<Mesh2dHandle>>);
#[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
}
}
}

View file

@ -0,0 +1,68 @@
#import bevy_sprite::mesh2d_view_bind_group
#import bevy_sprite::mesh2d_struct
struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] tangent: vec4<f32>;
#endif
};
struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
};
[[group(0), binding(0)]]
var<uniform> view: View;
[[group(2), binding(0)]]
var<uniform> mesh: Mesh2d;
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.model * vec4<f32>(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<f32>(
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<f32>(
mat3x3<f32>(
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<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
};
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
}

View file

@ -0,0 +1,6 @@
struct Mesh2d {
model: mat4x4<f32>;
inverse_transpose_model: mat4x4<f32>;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
};

View file

@ -0,0 +1,10 @@
struct View {
view_proj: mat4x4<f32>;
inverse_view: mat4x4<f32>;
projection: mat4x4<f32>;
world_position: vec3<f32>;
near: f32;
far: f32;
width: f32;
height: f32;
};

View file

@ -0,0 +1,7 @@
mod color_material;
mod material;
mod mesh;
pub use color_material::*;
pub use material::*;
pub use mesh::*;

View file

@ -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<Image>,
pub atlas_size: Option<Vec2>,
/// Select an area of the texture
pub rect: Option<Rect>,
/// Change the on-screen size of the sprite
pub custom_size: Option<Vec2>,
/// Handle to the `Image` of this sprite
/// PERF: storing a `HandleId` instead of `Handle<Image>` 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<RenderWorld>,
images: Res<Assets<Image>>,
texture_atlases: Res<Assets<TextureAtlas>>,
sprite_query: Query<(
&ComputedVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
)>,
sprite_query: Query<(&Visibility, &Sprite, &GlobalTransform, &Handle<Image>)>,
atlas_query: Query<(
&ComputedVisibility,
&Visibility,
&TextureAtlasSprite,
&GlobalTransform,
&Handle<TextureAtlas>,
@ -218,46 +242,40 @@ pub fn extract_sprites(
) {
let mut extracted_sprites = render_world.get_resource_mut::<ExtractedSprites>().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<u32>,
handle: Handle<Image>,
z: f32,
image_handle_id: HandleId,
colored: bool,
}
pub fn prepare_sprites(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut sprite_meta: ResMut<SpriteMeta>,
mut extracted_sprites: ResMut<ExtractedSprites>,
) {
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<Handle<Image>> = None;
let mut current_batch_colored = false;
let mut last_z = 0.0;
for extracted_sprite in extracted_sprites.sprites.iter() {
let colored = extracted_sprite.color != Color::WHITE;
if let Some(current_batch_handle) = &current_batch_handle {
if *current_batch_handle != extracted_sprite.handle || current_batch_colored != colored
{
if current_batch_colored {
commands.spawn_bundle((SpriteBatch {
range: colored_start..colored_end,
handle: current_batch_handle.clone_weak(),
z: last_z,
colored: true,
},));
colored_start = colored_end;
} else {
commands.spawn_bundle((SpriteBatch {
range: start..end,
handle: current_batch_handle.clone_weak(),
z: last_z,
colored: false,
},));
start = end;
}
}
}
current_batch_handle = Some(extracted_sprite.handle.clone_weak());
current_batch_colored = colored;
let sprite_rect = extracted_sprite.rect;
// Specify the corners of the sprite
let 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<Handle<Image>, BindGroup>,
@ -471,8 +340,10 @@ pub struct ImageBindGroups {
#[allow(clippy::too_many_arguments)]
pub fn queue_sprites(
mut commands: Commands,
draw_functions: Res<DrawFunctions<Transparent2d>>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut sprite_meta: ResMut<SpriteMeta>,
view_uniforms: Res<ViewUniforms>,
sprite_pipeline: Res<SpritePipeline>,
@ -480,7 +351,8 @@ pub fn queue_sprites(
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut image_bind_groups: ResMut<ImageBindGroups>,
gpu_images: Res<RenderAssets<Image>>,
sprite_batches: Query<(Entity, &SpriteBatch)>,
msaa: Res<Msaa>,
mut extracted_sprites: ResMut<ExtractedSprites>,
mut views: Query<&mut RenderPhase<Transparent2d>>,
events: Res<SpriteAssetEvents>,
) {
@ -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::<DrawSprite>().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<SpriteMeta>,
SRes<ImageBindGroups>,
SRes<RenderPipelineCache>,
SQuery<Read<ViewUniformOffset>>,
SQuery<Read<SpriteBatch>>,
)>,
}
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<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetSpriteViewBindGroup<I> {
type Param = (SRes<SpriteMeta>, SQuery<Read<ViewUniformOffset>>);
impl Draw<Transparent2d> 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<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetSpriteTextureBindGroup<I> {
type Param = (SRes<ImageBindGroups>, SQuery<Read<SpriteBatch>>);
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<P: BatchedPhaseItem> RenderCommand<P> for DrawSpriteBatch {
type Param = (SRes<SpriteMeta>, SQuery<Read<SpriteBatch>>);
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
}
}

View file

@ -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<Vec2>,
}
impl Default for TextureAtlasSprite {
@ -37,6 +40,7 @@ impl Default for TextureAtlasSprite {
color: Color::WHITE,
flip_x: false,
flip_y: false,
custom_size: None,
}
}
}

View file

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

View file

@ -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<Assets<TextureAtlas>>,
text_pipeline: Res<DefaultTextPipeline>,
windows: Res<Windows>,
text2d_query: Query<(Entity, &Text, &GlobalTransform, &Text2dSize)>,
text2d_query: Query<(Entity, &Visibility, &Text, &GlobalTransform, &Text2dSize)>,
) {
let mut extracted_sprites = render_world.get_resource_mut::<ExtractedSprites>().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,
});

View file

@ -152,7 +152,7 @@ impl<const I: usize> EntityRenderCommand for SetUiViewBindGroup<I> {
(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(),

22
examples/2d/mesh2d.rs Normal file
View file

@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
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()
});
}

View file

@ -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<Assets<Mesh>>,
) {
// 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<Mesh>` 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::<Shader>(),
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>(),
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<uniform> view: View;
#import bevy_sprite::mesh2d_struct
[[group(1), binding(0)]]
var<uniform> mesh: Mesh2d;
// The structure of the vertex buffer is as specified in `specialize()`
struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec4<f32>;
};
struct VertexOutput {
// The vertex shader must set the on-screen position of the vertex
[[builtin(position)]] clip_position: vec4<f32>;
// We pass the vertex color to the framgent shader in location 0
[[location(0)]] color: vec4<f32>;
};
/// 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<f32>(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<f32>;
};
/// Entry point for the fragment shader
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
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::<Assets<Shader>>().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::<Transparent2d, DrawColoredMesh2d>()
.init_resource::<ColoredMesh2dPipeline>()
.init_resource::<SpecializedPipelines<ColoredMesh2dPipeline>>()
.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<usize>,
query: Query<(Entity, &ComputedVisibility), With<ColoredMesh2d>>,
) {
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<DrawFunctions<Transparent2d>>,
colored_mesh2d_pipeline: Res<ColoredMesh2dPipeline>,
mut pipelines: ResMut<SpecializedPipelines<ColoredMesh2dPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<Mesh>>,
colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With<ColoredMesh2d>>,
mut views: Query<(&VisibleEntities, &mut RenderPhase<Transparent2d>)>,
) {
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::<DrawColoredMesh2d>()
.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,
});
}
}
}
}

View file

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