mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
32f7997c56
commit
c2da7800e3
29 changed files with 2262 additions and 360 deletions
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -53,3 +53,4 @@ hex = "0.4.2"
|
|||
hexasphere = "6.0.0"
|
||||
parking_lot = "0.11.0"
|
||||
regex = "1.5"
|
||||
copyless = "0.1.5"
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
235
crates/bevy_sprite/src/mesh2d/color_material.rs
Normal file
235
crates/bevy_sprite/src/mesh2d/color_material.rs
Normal 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>;
|
41
crates/bevy_sprite/src/mesh2d/color_material.wgsl
Normal file
41
crates/bevy_sprite/src/mesh2d/color_material.wgsl
Normal 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;
|
||||
}
|
345
crates/bevy_sprite/src/mesh2d/material.rs
Normal file
345
crates/bevy_sprite/src/mesh2d/material.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
512
crates/bevy_sprite/src/mesh2d/mesh.rs
Normal file
512
crates/bevy_sprite/src/mesh2d/mesh.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
68
crates/bevy_sprite/src/mesh2d/mesh2d.wgsl
Normal file
68
crates/bevy_sprite/src/mesh2d/mesh2d.wgsl
Normal 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);
|
||||
}
|
6
crates/bevy_sprite/src/mesh2d/mesh2d_struct.wgsl
Normal file
6
crates/bevy_sprite/src/mesh2d/mesh2d_struct.wgsl
Normal 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;
|
||||
};
|
10
crates/bevy_sprite/src/mesh2d/mesh2d_view_bind_group.wgsl
Normal file
10
crates/bevy_sprite/src/mesh2d/mesh2d_view_bind_group.wgsl
Normal 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;
|
||||
};
|
7
crates/bevy_sprite/src/mesh2d/mod.rs
Normal file
7
crates/bevy_sprite/src/mesh2d/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod color_material;
|
||||
mod material;
|
||||
mod mesh;
|
||||
|
||||
pub use color_material::*;
|
||||
pub use material::*;
|
||||
pub use mesh::*;
|
|
@ -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) = ¤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<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
22
examples/2d/mesh2d.rs
Normal 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()
|
||||
});
|
||||
}
|
358
examples/2d/mesh2d_manual.rs
Normal file
358
examples/2d/mesh2d_manual.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue