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 <alice.i.cecile@gmail.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
IceSentry 2024-08-06 20:22:09 -04:00 committed by GitHub
parent e7d40c9b08
commit 5abc32ceda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 591 additions and 52 deletions

View file

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

View file

@ -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::<ViewSortedRenderPhases<Opaque2d>>() 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(())
}
}

View file

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

View file

@ -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::<DrawFunctions<Opaque2d>>()
.init_resource::<DrawFunctions<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Opaque2d>>()
.add_systems(ExtractSchedule, extract_core_2d_camera_phases)
.add_systems(
Render,
sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort),
(
sort_phase_system::<Opaque2d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort),
prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources),
),
);
render_app
.add_render_sub_graph(Core2d)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::StartMainPass)
.add_render_graph_node::<ViewNodeRunner<MainOpaquePass2dNode>>(
Core2d,
Node2d::MainOpaquePass,
)
.add_render_graph_node::<ViewNodeRunner<MainTransparentPass2dNode>>(
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<u32>,
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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &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<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_2d_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>,
cameras_2d: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<EntityHashSet>,
) {
@ -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<TextureCache>,
render_device: Res<RenderDevice>,
transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>,
views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With<Camera2d>,)>,
) {
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)));
}
}

View file

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

View file

@ -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<Handle<Image>>,
@ -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<Color> 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<Handle<Image>> 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`].

View file

@ -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<M: Material2d>(PhantomData<M>);
@ -153,6 +174,7 @@ where
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<Opaque2d, DrawMaterial2d<M>>()
.add_render_command::<Transparent2d, DrawMaterial2d<M>>()
.init_resource::<RenderMaterial2dInstances<M>>()
.init_resource::<SpecializedMeshPipelines<Material2dPipeline<M>>>()
@ -348,6 +370,13 @@ impl<P: PhaseItem, M: Material2d, const I: usize> RenderCommand<P>
}
}
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<M: Material2d>(
opaque_draw_functions: Res<DrawFunctions<Opaque2d>>,
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
material2d_pipeline: Res<Material2dPipeline<M>>,
mut pipelines: ResMut<SpecializedMeshPipelines<Material2dPipeline<M>>>,
@ -374,6 +404,7 @@ pub fn queue_material2d_meshes<M: Material2d>(
mut render_mesh_instances: ResMut<RenderMesh2dInstances>,
render_material_instances: Res<RenderMaterial2dInstances<M>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_render_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>,
mut views: Query<(
Entity,
&ExtractedView,
@ -394,7 +425,12 @@ pub fn queue_material2d_meshes<M: Material2d>(
continue;
};
let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else {
continue;
};
let draw_transparent_2d = transparent_draw_functions.read().id::<DrawMaterial2d<M>>();
let draw_opaque_2d = opaque_draw_functions.read().id::<DrawMaterial2d<M>>();
let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr);
@ -421,8 +457,9 @@ pub fn queue_material2d_meshes<M: Material2d>(
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<M: Material2d>(
};
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<M: Material2d>(
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)]
pub struct Material2dBindGroupId(pub Option<BindGroupId>);
/// 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<T: Material2d> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub key: T::Data,
pub depth_bias: f32,
pub properties: Material2dProperties,
}
impl<T: Material2d> PreparedMaterial2d<T> {
@ -492,19 +560,27 @@ impl<M: Material2d> RenderAsset for PreparedMaterial2d<M> {
fn prepare_asset(
material: Self::SourceAsset,
(render_device, images, fallback_image, pipeline): &mut SystemParamItem<Self::Param>,
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
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))
}

View file

@ -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::<Opaque2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
write_batched_instance_buffer::<Mesh2dPipeline>
@ -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()),
})
}
}

View file

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

View file

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

View file

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

View file

@ -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<Self, Self::Err> {
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()),