From 5abc32ceda1a440271c9f9ef71279bd9f7f9ff5c Mon Sep 17 00:00:00 2001 From: IceSentry Date: Tue, 6 Aug 2024 20:22:09 -0400 Subject: [PATCH] Add 2d opaque phase with depth buffer (#13069) This PR is based on top of #12982 # Objective - Mesh2d currently only has an alpha blended phase. Most sprites don't need transparency though. - For some 2d games it can be useful to have a 2d depth buffer ## Solution - Add an opaque phase to render Mesh2d that don't need transparency - This phase currently uses the `SortedRenderPhase` to make it easier to implement based on the already existing transparent phase. A follow up PR will switch this to `BinnedRenderPhase`. - Add a 2d depth buffer - Use that depth buffer in the transparent phase to make sure that sprites and transparent mesh2d are displayed correctly ## Testing I added the mesh2d_transforms example that layers many opaque and transparent mesh2d to make sure they all get displayed correctly. I also confirmed it works with sprites by modifying that example locally. --- ## Changelog - Added `AlphaMode2d` - Added `Opaque2d` render phase - Camera2d now have a `ViewDepthTexture` component ## Migration Guide - `ColorMaterial` now contains `AlphaMode2d`. To keep previous behaviour, use `AlphaMode::BLEND`. If you know your sprite is opaque, use `AlphaMode::OPAQUE` ## Follow up PRs - See tracking issue: #13265 --------- Co-authored-by: Alice Cecile Co-authored-by: Christopher Biscardi --- Cargo.toml | 11 ++ .../src/core_2d/main_opaque_pass_2d_node.rs | 87 +++++++++++ .../core_2d/main_transparent_pass_2d_node.rs | 20 ++- crates/bevy_core_pipeline/src/core_2d/mod.rs | 142 +++++++++++++++++- crates/bevy_gizmos/src/pipeline_2d.rs | 36 ++++- .../bevy_sprite/src/mesh2d/color_material.rs | 22 ++- crates/bevy_sprite/src/mesh2d/material.rs | 132 ++++++++++++---- crates/bevy_sprite/src/mesh2d/mesh.rs | 39 ++++- crates/bevy_sprite/src/render/mod.rs | 22 ++- examples/2d/mesh2d_alpha_mode.rs | 97 ++++++++++++ examples/README.md | 1 + examples/stress_tests/bevymark.rs | 34 ++++- 12 files changed, 591 insertions(+), 52 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs create mode 100644 examples/2d/mesh2d_alpha_mode.rs diff --git a/Cargo.toml b/Cargo.toml index 78c01b0c63..99e3ccff91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -596,6 +596,17 @@ description = "Demonstrates transparency in 2d" category = "2D Rendering" wasm = true +[[example]] +name = "mesh2d_alpha_mode" +path = "examples/2d/mesh2d_alpha_mode.rs" +doc-scrape-examples = true + +[package.metadata.example.mesh2d_alpha_mode] +name = "Mesh2d Alpha Mode" +description = "Used to test alpha modes with mesh2d" +category = "2D Rendering" +wasm = true + [[example]] name = "pixel_grid_snap" path = "examples/2d/pixel_grid_snap.rs" diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs new file mode 100644 index 0000000000..3001340d47 --- /dev/null +++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs @@ -0,0 +1,87 @@ +use crate::core_2d::Opaque2d; +use bevy_ecs::{prelude::World, query::QueryItem}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewSortedRenderPhases}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ViewDepthTexture, ViewTarget}, +}; +use bevy_utils::tracing::error; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque2d`] [`ViewSortedRenderPhases`] +#[derive(Default)] +pub struct MainOpaquePass2dNode; +impl ViewNode for MainOpaquePass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, target, depth): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(opaque_phases) = world.get_resource::>() else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = [Some(target.get_color_attachment())]; + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + let Some(opaque_phase) = opaque_phases.get(&view_entity) else { + return Ok(()); + }; + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _main_opaque_pass_2d_span = info_span!("main_opaque_pass_2d").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_opaque_pass_2d_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_opaque_pass_2d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_2d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Opaque draws + if !opaque_phase.items.is_empty() { + #[cfg(feature = "trace")] + let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered(); + if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the 2d opaque phase {err:?}"); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 8f0d42acab..2c82ccc800 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -5,9 +5,9 @@ use bevy_render::{ diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, - render_resource::RenderPassDescriptor, + render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::ViewTarget, + view::{ViewDepthTexture, ViewTarget}, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -16,13 +16,17 @@ use bevy_utils::tracing::info_span; pub struct MainTransparentPass2dNode {} impl ViewNode for MainTransparentPass2dNode { - type ViewQuery = (&'static ExtractedCamera, &'static ViewTarget); + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewDepthTexture, + ); fn run<'w>( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, target): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + (camera, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(transparent_phases) = @@ -46,7 +50,13 @@ impl ViewNode for MainTransparentPass2dNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_transparent_pass_2d"), color_attachments: &[Some(target.get_color_attachment())], - depth_stencil_attachment: None, + // NOTE: For the transparent pass we load the depth buffer. There should be no + // need to write to it, but store is set to `true` as a workaround for issue #3776, + // https://github.com/bevyengine/bevy/issues/3776 + // so that wgpu does not clear the depth buffer. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }); diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index f04287081d..e479e45853 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,4 +1,5 @@ mod camera_2d; +mod main_opaque_pass_2d_node; mod main_transparent_pass_2d_node; pub mod graph { @@ -15,6 +16,7 @@ pub mod graph { pub enum Node2d { MsaaWriteback, StartMainPass, + MainOpaquePass, MainTransparentPass, EndMainPass, Bloom, @@ -30,21 +32,29 @@ pub mod graph { use std::ops::Range; +use bevy_utils::HashMap; pub use camera_2d::*; +pub use main_opaque_pass_2d_node::*; pub use main_transparent_pass_2d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_render::{ - camera::Camera, + camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_phase::{ sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewSortedRenderPhases, }, - render_resource::CachedRenderPipelineId, + render_resource::{ + CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, + }, + renderer::RenderDevice, + texture::TextureCache, + view::{Msaa, ViewDepthTexture}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -52,6 +62,8 @@ use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; use self::graph::{Core2d, Node2d}; +pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + pub struct Core2dPlugin; impl Plugin for Core2dPlugin { @@ -63,17 +75,27 @@ impl Plugin for Core2dPlugin { return; }; render_app + .init_resource::>() .init_resource::>() .init_resource::>() + .init_resource::>() .add_systems(ExtractSchedule, extract_core_2d_camera_phases) .add_systems( Render, - sort_phase_system::.in_set(RenderSet::PhaseSort), + ( + sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), + prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources), + ), ); render_app .add_render_sub_graph(Core2d) .add_render_graph_node::(Core2d, Node2d::StartMainPass) + .add_render_graph_node::>( + Core2d, + Node2d::MainOpaquePass, + ) .add_render_graph_node::>( Core2d, Node2d::MainTransparentPass, @@ -86,6 +108,7 @@ impl Plugin for Core2dPlugin { Core2d, ( Node2d::StartMainPass, + Node2d::MainOpaquePass, Node2d::MainTransparentPass, Node2d::EndMainPass, Node2d::Tonemapping, @@ -96,6 +119,67 @@ impl Plugin for Core2dPlugin { } } +/// Opaque 2D [`SortedPhaseItem`]s. +pub struct Opaque2d { + pub sort_key: FloatOrd, + pub entity: Entity, + pub pipeline: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} +impl PhaseItem for Opaque2d { + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl SortedPhaseItem for Opaque2d { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn sort(items: &mut [Self]) { + // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. + radsort::sort_by_key(items, |item| item.sort_key().0); + } +} + +impl CachedRenderPipelinePhaseItem for Opaque2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + pub struct Transparent2d { pub sort_key: FloatOrd, pub entity: Entity, @@ -162,6 +246,7 @@ impl CachedRenderPipelinePhaseItem for Transparent2d { pub fn extract_core_2d_camera_phases( mut commands: Commands, mut transparent_2d_phases: ResMut>, + mut opaque_2d_phases: ResMut>, cameras_2d: Extract>>, mut live_entities: Local, ) { @@ -174,10 +259,61 @@ pub fn extract_core_2d_camera_phases( commands.get_or_spawn(entity); transparent_2d_phases.insert_or_clear(entity); + opaque_2d_phases.insert_or_clear(entity); live_entities.insert(entity); } // Clear out all dead views. transparent_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); + opaque_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn prepare_core_2d_depth_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + transparent_2d_phases: ResMut>, + opaque_2d_phases: ResMut>, + views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With,)>, +) { + let mut textures = HashMap::default(); + for (entity, camera, msaa) in &views_2d { + if !opaque_2d_phases.contains_key(&entity) || !transparent_2d_phases.contains_key(&entity) { + continue; + }; + + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + // The size of the depth texture + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: CORE_2D_DEPTH_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands + .entity(entity) + .insert(ViewDepthTexture::new(cached_texture, Some(0.0))); + } } diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 77b4efb574..0f6552f787 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -7,7 +7,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; -use bevy_core_pipeline::core_2d::Transparent2d; +use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, @@ -139,7 +139,22 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { }), layout, primitive: PrimitiveState::default(), - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.mesh_key.msaa_samples(), mask: !0, @@ -224,7 +239,22 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { }), layout, primitive: PrimitiveState::default(), - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.mesh_key.msaa_samples(), mask: !0, diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 38d74bb473..cc9c22baae 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,7 +1,7 @@ -use crate::{Material2d, Material2dPlugin, MaterialMesh2dBundle}; +use crate::{AlphaMode2d, Material2d, Material2dPlugin, MaterialMesh2dBundle}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; -use bevy_color::{Color, ColorToComponents, LinearRgba}; +use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; use bevy_math::Vec4; use bevy_reflect::prelude::*; use bevy_render::{ @@ -46,6 +46,7 @@ impl Plugin for ColorMaterialPlugin { #[uniform(0, ColorMaterialUniform)] pub struct ColorMaterial { pub color: Color, + pub alpha_mode: AlphaMode2d, #[texture(1)] #[sampler(2)] pub texture: Option>, @@ -63,6 +64,8 @@ impl Default for ColorMaterial { ColorMaterial { color: Color::WHITE, texture: None, + // TODO should probably default to AlphaMask once supported? + alpha_mode: AlphaMode2d::Blend, } } } @@ -71,6 +74,11 @@ impl From for ColorMaterial { fn from(color: Color) -> Self { ColorMaterial { color, + alpha_mode: if color.alpha() < 1.0 { + AlphaMode2d::Blend + } else { + AlphaMode2d::Opaque + }, ..Default::default() } } @@ -89,9 +97,9 @@ impl From> for ColorMaterial { bitflags::bitflags! { #[repr(transparent)] pub struct ColorMaterialFlags: u32 { - const TEXTURE = 1 << 0; - const NONE = 0; - const UNINITIALIZED = 0xFFFF; + const TEXTURE = 1 << 0; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; } } @@ -120,6 +128,10 @@ impl Material2d for ColorMaterial { fn fragment_shader() -> ShaderRef { COLOR_MATERIAL_SHADER_HANDLE.into() } + + fn alpha_mode(&self) -> AlphaMode2d { + self.alpha_mode + } } /// A component bundle for entities with a [`Mesh2dHandle`](crate::Mesh2dHandle) and a [`ColorMaterial`]. diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index b29f4a2654..801c4b722f 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,16 +1,17 @@ use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ - core_2d::Transparent2d, + core_2d::{Opaque2d, Transparent2d}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{ + entity::EntityHashMap, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_math::FloatOrd; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{ @@ -32,8 +33,7 @@ use bevy_render::{ }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::error; -use std::hash::Hash; -use std::marker::PhantomData; +use std::{hash::Hash, marker::PhantomData}; use crate::{ DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, @@ -121,6 +121,10 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { 0.0 } + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Opaque + } + /// Customizes the default [`RenderPipelineDescriptor`]. #[allow(unused_variables)] #[inline] @@ -133,6 +137,23 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { } } +/// Sets how a 2d material's base color alpha channel is used for transparency. +/// Currently, this only works with [`Mesh2d`](crate::mesh2d::Mesh2d). Sprites are always transparent. +/// +/// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. +/// We use a separate type because 2d doesn't support all the transparency modes that 3d does. +#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] +#[reflect(Default, Debug)] +pub enum AlphaMode2d { + /// Base color alpha values are overridden to be fully opaque (1.0). + #[default] + Opaque, + /// The base color alpha value defines the opacity of the color. + /// Standard alpha-blending is used to blend the fragment's color + /// with the color behind it. + Blend, +} + /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`] /// asset type (which includes [`Material2d`] types). pub struct Material2dPlugin(PhantomData); @@ -153,6 +174,7 @@ where if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .add_render_command::>() .add_render_command::>() .init_resource::>() .init_resource::>>() @@ -348,6 +370,13 @@ impl RenderCommand

} } +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode2d) -> Mesh2dPipelineKey { + match alpha_mode { + AlphaMode2d::Blend => Mesh2dPipelineKey::BLEND_ALPHA, + _ => Mesh2dPipelineKey::NONE, + } +} + pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { match tonemapping { Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, @@ -365,6 +394,7 @@ pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelin #[allow(clippy::too_many_arguments)] pub fn queue_material2d_meshes( + opaque_draw_functions: Res>, transparent_draw_functions: Res>, material2d_pipeline: Res>, mut pipelines: ResMut>>, @@ -374,6 +404,7 @@ pub fn queue_material2d_meshes( mut render_mesh_instances: ResMut, render_material_instances: Res>, mut transparent_render_phases: ResMut>, + mut opaque_render_phases: ResMut>, mut views: Query<( Entity, &ExtractedView, @@ -394,7 +425,12 @@ pub fn queue_material2d_meshes( continue; }; + let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { + continue; + }; + let draw_transparent_2d = transparent_draw_functions.read().id::>(); + let draw_opaque_2d = opaque_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); @@ -421,8 +457,9 @@ pub fn queue_material2d_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let mesh_key = - view_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()); + let mesh_key = view_key + | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()) + | material_2d.properties.mesh_pipeline_key_bits; let pipeline_id = pipelines.specialize( &pipeline_cache, @@ -443,21 +480,37 @@ pub fn queue_material2d_meshes( }; mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); - let mesh_z = mesh_instance.transforms.world_from_local.translation.z; - transparent_phase.add(Transparent2d { - entity: *visible_entity, - draw_function: draw_transparent_2d, - 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 + material_2d.depth_bias), - // Batching is done in batch_and_prepare_render_phase - batch_range: 0..1, - extra_index: PhaseItemExtraIndex::NONE, - }); + + match material_2d.properties.alpha_mode { + AlphaMode2d::Opaque => { + opaque_phase.add(Opaque2d { + entity: *visible_entity, + draw_function: draw_opaque_2d, + pipeline: pipeline_id, + // Front-to-back ordering + sort_key: -FloatOrd(mesh_z + material_2d.properties.depth_bias), + // Batching is done in batch_and_prepare_render_phase + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::NONE, + }); + } + AlphaMode2d::Blend => { + transparent_phase.add(Transparent2d { + entity: *visible_entity, + draw_function: draw_transparent_2d, + 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 + material_2d.properties.depth_bias), + // Batching is done in batch_and_prepare_render_phase + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::NONE, + }); + } + } } } } @@ -465,12 +518,27 @@ pub fn queue_material2d_meshes( #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct Material2dBindGroupId(pub Option); +/// Common [`Material2d`] properties, calculated for a specific material instance. +pub struct Material2dProperties { + /// The [`AlphaMode2d`] of this material. + pub alpha_mode: AlphaMode2d, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may + pub depth_bias: f32, + /// The bits in the [`Mesh2dPipelineKey`] for this material. + /// + /// These are precalculated so that we can just "or" them together in + /// [`queue_material2d_meshes`]. + pub mesh_pipeline_key_bits: Mesh2dPipelineKey, +} + /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { pub bindings: Vec<(u32, OwnedBindingResource)>, pub bind_group: BindGroup, pub key: T::Data, - pub depth_bias: f32, + pub properties: Material2dProperties, } impl PreparedMaterial2d { @@ -492,19 +560,27 @@ impl RenderAsset for PreparedMaterial2d { fn prepare_asset( material: Self::SourceAsset, (render_device, images, fallback_image, pipeline): &mut SystemParamItem, - ) -> Result> { + ) -> Result> { match material.as_bind_group( &pipeline.material2d_layout, render_device, images, fallback_image, ) { - Ok(prepared) => Ok(PreparedMaterial2d { - bindings: prepared.bindings, - bind_group: prepared.bind_group, - key: prepared.data, - depth_bias: material.depth_bias(), - }), + Ok(prepared) => { + let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); + Ok(PreparedMaterial2d { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + properties: Material2dProperties { + depth_bias: material.depth_bias(), + alpha_mode: material.alpha_mode(), + mesh_pipeline_key_bits, + }, + }) + } Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index c5d497d1d4..d21ad1fc60 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,13 +1,13 @@ use bevy_app::Plugin; use bevy_asset::{load_internal_asset, AssetId, Handle}; -use bevy_core_pipeline::core_2d::{Camera2d, Transparent2d}; +use bevy_core_pipeline::core_2d::{Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_core_pipeline::tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{ + entity::EntityHashMap, prelude::*, query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, @@ -107,6 +107,8 @@ impl Plugin for Mesh2dRenderPlugin { .add_systems( Render, ( + batch_and_prepare_sorted_render_phase:: + .in_set(RenderSet::PrepareResources), batch_and_prepare_sorted_render_phase:: .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: @@ -388,6 +390,7 @@ bitflags::bitflags! { const HDR = 1 << 0; const TONEMAP_IN_SHADER = 1 << 1; const DEBAND_DITHER = 1 << 2; + const BLEND_ALPHA = 1 << 3; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -539,6 +542,17 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { false => TextureFormat::bevy_default(), }; + let (depth_write_enabled, label, blend); + if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) { + label = "transparent_mesh2d_pipeline"; + blend = Some(BlendState::ALPHA_BLENDING); + depth_write_enabled = false; + } else { + label = "opaque_mesh2d_pipeline"; + blend = None; + depth_write_enabled = true; + } + Ok(RenderPipelineDescriptor { vertex: VertexState { shader: MESH2D_SHADER_HANDLE, @@ -552,7 +566,7 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, - blend: Some(BlendState::ALPHA_BLENDING), + blend, write_mask: ColorWrites::ALL, })], }), @@ -567,13 +581,28 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { topology: key.primitive_topology(), strip_index_format: None, }, - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, - label: Some("transparent_mesh2d_pipeline".into()), + label: Some(label.into()), }) } } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 8b767cde41..60c64131fe 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -7,7 +7,7 @@ use crate::{ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ - core_2d::Transparent2d, + core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, DebandDither, Tonemapping, TonemappingLuts, @@ -294,7 +294,25 @@ impl SpecializedRenderPipeline for SpritePipeline { topology: PrimitiveTopology::TriangleList, strip_index_format: None, }, - depth_stencil: None, + // Sprites are always alpha blended so they never need to write to depth. + // They just need to read it in case an opaque mesh2d + // that wrote to depth is present. + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.msaa_samples(), mask: !0, diff --git a/examples/2d/mesh2d_alpha_mode.rs b/examples/2d/mesh2d_alpha_mode.rs new file mode 100644 index 0000000000..2038bce0ef --- /dev/null +++ b/examples/2d/mesh2d_alpha_mode.rs @@ -0,0 +1,97 @@ +//! This example is used to test how transforms interact with alpha modes for [`MaterialMesh2dBundle`] entities. +//! This makes sure the depth buffer is correctly being used for opaque and transparent 2d meshes + +use bevy::{ + color::palettes::css::{BLUE, GREEN, WHITE}, + prelude::*, + sprite::{AlphaMode2d, MaterialMesh2dBundle}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(Camera2dBundle::default()); + + let texture_handle = asset_server.load("branding/icon.png"); + let mesh_handle = meshes.add(Rectangle::from_size(Vec2::splat(256.0))); + + // opaque + // Each sprite should be square with the transparent parts being completely black + // The blue sprite should be on top with the white and green one behind it + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: WHITE.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-400.0, 0.0, 0.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: BLUE.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-300.0, 0.0, 1.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: GREEN.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-200.0, 0.0, -1.0), + ..default() + }); + + // Test the interaction between opaque and transparent meshes + // The white sprite should be: + // - fully opaque + // - on top of the green sprite + // - behind the blue sprite + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: WHITE.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(200.0, 0.0, 0.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: BLUE.with_alpha(0.7).into(), + alpha_mode: AlphaMode2d::Blend, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(300.0, 0.0, 1.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: GREEN.with_alpha(0.7).into(), + alpha_mode: AlphaMode2d::Blend, + texture: Some(texture_handle), + }), + transform: Transform::from_xyz(400.0, 0.0, -1.0), + ..default() + }); +} diff --git a/examples/README.md b/examples/README.md index dc58b56cf0..52f7b1d5ae 100644 --- a/examples/README.md +++ b/examples/README.md @@ -109,6 +109,7 @@ Example | Description [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh [Mesh 2D With Vertex Colors](../examples/2d/mesh2d_vertex_color_texture.rs) | Renders a 2d mesh with vertex color attributes +[Mesh2d Alpha Mode](../examples/2d/mesh2d_alpha_mode.rs) | Used to test alpha modes with mesh2d [Move Sprite](../examples/2d/move_sprite.rs) | Changes the transform of a sprite [Pixel Grid Snapping](../examples/2d/pixel_grid_snap.rs) | Shows how to create graphics that snap to the pixel grid by rendering to a texture in 2D [Sprite](../examples/2d/sprite.rs) | Renders a sprite diff --git a/examples/stress_tests/bevymark.rs b/examples/stress_tests/bevymark.rs index 067f761507..dcf85508ea 100644 --- a/examples/stress_tests/bevymark.rs +++ b/examples/stress_tests/bevymark.rs @@ -13,7 +13,7 @@ use bevy::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }, - sprite::{MaterialMesh2dBundle, Mesh2dHandle}, + sprite::{AlphaMode2d, MaterialMesh2dBundle, Mesh2dHandle}, utils::Duration, window::{PresentMode, WindowResolution}, winit::{UpdateMode, WinitSettings}, @@ -71,6 +71,10 @@ struct Args { /// generate z values in increasing order rather than randomly #[argh(switch)] ordered_z: bool, + + /// the alpha mode used to spawn the sprites + #[argh(option, default = "AlphaMode::Blend")] + alpha_mode: AlphaMode, } #[derive(Default, Clone)] @@ -94,6 +98,27 @@ impl FromStr for Mode { } } +#[derive(Default, Clone)] +enum AlphaMode { + Opaque, + #[default] + Blend, +} + +impl FromStr for AlphaMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "opaque" => Ok(Self::Opaque), + "blend" => Ok(Self::Blend), + _ => Err(format!( + "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend'" + )), + } + } +} + const FIXED_TIMESTEP: f32 = 0.2; fn main() { @@ -573,10 +598,16 @@ fn init_materials( } .max(1); + let alpha_mode = match args.alpha_mode { + AlphaMode::Opaque => AlphaMode2d::Opaque, + AlphaMode::Blend => AlphaMode2d::Blend, + }; + let mut materials = Vec::with_capacity(capacity); materials.push(assets.add(ColorMaterial { color: Color::WHITE, texture: textures.first().cloned(), + alpha_mode, })); // We're seeding the PRNG here to make this example deterministic for testing purposes. @@ -588,6 +619,7 @@ fn init_materials( assets.add(ColorMaterial { color: Color::srgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()), texture: textures.choose(&mut texture_rng).cloned(), + alpha_mode, }) }) .take(capacity - materials.len()),