From ae31361949e0609195f2387e2eaec6ec7c0cbe31 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Mon, 27 Mar 2023 23:35:16 -0700 Subject: [PATCH] Split opaque and transparent phases (#8090) # Objective Fixes #8089. ## Solution Splits the MainPass3dNode into 2 nodes, one for the opaque + alpha passes and one for the transparent pass. --- ## Changelog - Split MainPass3dNode into MainOpaquePass3dNode and MainTransparentPass3dNode - Combine opaque and alpha phases in MainOpaquePass3dNode into one pass - Create `START_MAIN_PASS` and `END_MAIN_PASS` empty nodes as labels - Main pass becomes `START_MAIN_PASS -> MAIN_OPAQUE_PASS -> MAIN_TRANSPARENT_PASS -> END_MAIN_PASS` ## Migration Guide Nodes that previously added edges involving `MAIN_PASS` should now add edges to or from `START_MAIN_PASS` or `END_MAIN_PASS` respectively. --- crates/bevy_core_pipeline/src/bloom/mod.rs | 2 +- .../src/core_3d/main_opaque_pass_3d_node.rs | 126 +++++++++++ .../src/core_3d/main_pass_3d_node.rs | 213 ------------------ .../core_3d/main_transparent_pass_3d_node.rs | 115 ++++++++++ crates/bevy_core_pipeline/src/core_3d/mod.rs | 32 ++- .../bevy_core_pipeline/src/msaa_writeback.rs | 2 +- crates/bevy_core_pipeline/src/taa/mod.rs | 2 +- crates/bevy_pbr/src/lib.rs | 2 +- crates/bevy_ui/src/render/mod.rs | 2 +- 9 files changed, 271 insertions(+), 225 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs delete mode 100644 crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs create mode 100644 crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 7fd84bed46..e408480b52 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -83,7 +83,7 @@ impl Plugin for BloomPlugin { draw_3d_graph.add_node(core_3d::graph::node::BLOOM, bloom_node); // MAIN_PASS -> BLOOM -> TONEMAPPING draw_3d_graph.add_node_edge( - crate::core_3d::graph::node::MAIN_PASS, + crate::core_3d::graph::node::END_MAIN_PASS, core_3d::graph::node::BLOOM, ); draw_3d_graph.add_node_edge( diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs new file mode 100644 index 0000000000..b295b4bb1c --- /dev/null +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -0,0 +1,126 @@ +use crate::{ + clear_color::{ClearColor, ClearColorConfig}, + core_3d::{Camera3d, Opaque3d}, + prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_ecs::prelude::*; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_phase::RenderPhase, + render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use super::{AlphaMask3d, Camera3dDepthLoadOp}; + +/// A [`Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`] [`RenderPhase`]. +pub struct MainOpaquePass3dNode { + query: QueryState< + ( + &'static ExtractedCamera, + &'static RenderPhase, + &'static RenderPhase, + &'static Camera3d, + &'static ViewTarget, + &'static ViewDepthTexture, + Option<&'static DepthPrepass>, + Option<&'static NormalPrepass>, + Option<&'static MotionVectorPrepass>, + ), + With, + >, +} + +impl MainOpaquePass3dNode { + pub fn new(world: &mut World) -> Self { + Self { + query: world.query_filtered(), + } + } +} + +impl Node for MainOpaquePass3dNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + let Ok(( + camera, + opaque_phase, + alpha_mask_phase, + camera_3d, + target, + depth, + depth_prepass, + normal_prepass, + motion_vector_prepass + )) = self.query.get_manual(world, view_entity) else { + // No window + return Ok(()); + }; + + // Run the opaque pass, sorted front-to-back + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("main_opaque_pass_3d"), + // NOTE: The opaque pass loads the color + // buffer as well as writing to it. + color_attachments: &[Some(target.get_color_attachment(Operations { + load: match camera_3d.clear_color { + ClearColorConfig::Default => { + LoadOp::Clear(world.resource::().0.into()) + } + ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), + ClearColorConfig::None => LoadOp::Load, + }, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it + depth_ops: Some(Operations { + load: if depth_prepass.is_some() + || normal_prepass.is_some() + || motion_vector_prepass.is_some() + { + // if any prepass runs, it will generate a depth buffer so we should use it, + // even if only the normal_prepass is used. + Camera3dDepthLoadOp::Load + } else { + // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. + camera_3d.depth_load_op.clone() + } + .into(), + store: true, + }), + stencil_ops: None, + }), + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + opaque_phase.render(&mut render_pass, world, view_entity); + + if !alpha_mask_phase.items.is_empty() { + alpha_mask_phase.render(&mut render_pass, world, view_entity); + } + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs deleted file mode 100644 index b06b36dfa6..0000000000 --- a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::{ - clear_color::{ClearColor, ClearColorConfig}, - core_3d::{AlphaMask3d, Camera3d, Opaque3d, Transparent3d}, - prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass}, -}; -use bevy_ecs::prelude::*; -use bevy_render::{ - camera::ExtractedCamera, - render_graph::{Node, NodeRunError, RenderGraphContext}, - render_phase::RenderPhase, - render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor}, - renderer::RenderContext, - view::{ExtractedView, ViewDepthTexture, ViewTarget}, -}; -#[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; - -use super::Camera3dDepthLoadOp; - -pub struct MainPass3dNode { - query: QueryState< - ( - &'static ExtractedCamera, - &'static RenderPhase, - &'static RenderPhase, - &'static RenderPhase, - &'static Camera3d, - &'static ViewTarget, - &'static ViewDepthTexture, - Option<&'static DepthPrepass>, - Option<&'static NormalPrepass>, - Option<&'static MotionVectorPrepass>, - ), - With, - >, -} - -impl MainPass3dNode { - pub fn new(world: &mut World) -> Self { - Self { - query: world.query_filtered(), - } - } -} - -impl Node for MainPass3dNode { - fn update(&mut self, world: &mut World) { - self.query.update_archetypes(world); - } - - fn run( - &self, - graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, - ) -> Result<(), NodeRunError> { - let view_entity = graph.view_entity(); - let Ok(( - camera, - opaque_phase, - alpha_mask_phase, - transparent_phase, - camera_3d, - target, - depth, - depth_prepass, - normal_prepass, - motion_vector_prepass, - )) = self.query.get_manual(world, view_entity) else { - // No window - return Ok(()); - }; - - // Always run opaque pass to ensure screen is cleared - { - // Run the opaque pass, sorted front-to-back - // NOTE: Scoped to drop the mutable borrow of render_context - #[cfg(feature = "trace")] - let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); - - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_opaque_pass_3d"), - // NOTE: The opaque pass loads the color - // buffer as well as writing to it. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: match camera_3d.clear_color { - ClearColorConfig::Default => { - LoadOp::Clear(world.resource::().0.into()) - } - ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), - ClearColorConfig::None => LoadOp::Load, - }, - store: true, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it - depth_ops: Some(Operations { - load: if depth_prepass.is_some() - || normal_prepass.is_some() - || motion_vector_prepass.is_some() - { - // if any prepass runs, it will generate a depth buffer so we should use it, - // even if only the normal_prepass is used. - Camera3dDepthLoadOp::Load - } else { - // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. - camera_3d.depth_load_op.clone() - } - .into(), - store: true, - }), - stencil_ops: None, - }), - }); - - if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); - } - - opaque_phase.render(&mut render_pass, world, view_entity); - } - - if !alpha_mask_phase.items.is_empty() { - // Run the alpha mask pass, sorted front-to-back - // NOTE: Scoped to drop the mutable borrow of render_context - #[cfg(feature = "trace")] - let _main_alpha_mask_pass_3d_span = info_span!("main_alpha_mask_pass_3d").entered(); - - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_alpha_mask_pass_3d"), - // NOTE: The alpha_mask pass loads the color buffer as well as overwriting it where appropriate. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: true, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: The alpha mask pass loads the depth buffer and possibly overwrites it - depth_ops: Some(Operations { - load: LoadOp::Load, - store: true, - }), - stencil_ops: None, - }), - }); - - if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); - } - - alpha_mask_phase.render(&mut render_pass, world, view_entity); - } - - if !transparent_phase.items.is_empty() { - // Run the transparent pass, sorted back-to-front - // NOTE: Scoped to drop the mutable borrow of render_context - #[cfg(feature = "trace")] - let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered(); - - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_transparent_pass_3d"), - // NOTE: The transparent pass loads the color buffer as well as overwriting it where appropriate. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: true, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // 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_ops: Some(Operations { - load: LoadOp::Load, - store: true, - }), - stencil_ops: None, - }), - }); - - if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); - } - - transparent_phase.render(&mut render_pass, world, view_entity); - } - - // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't - // reset for the next render pass so add an empty render pass without a custom viewport - #[cfg(feature = "webgl")] - if camera.viewport.is_some() { - #[cfg(feature = "trace")] - let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered(); - let pass_descriptor = RenderPassDescriptor { - label: Some("reset_viewport_pass_3d"), - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: true, - }))], - depth_stencil_attachment: None, - }; - - render_context - .command_encoder() - .begin_render_pass(&pass_descriptor); - } - - Ok(()) - } -} diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs new file mode 100644 index 0000000000..7c4ddc28ae --- /dev/null +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -0,0 +1,115 @@ +use crate::core_3d::Transparent3d; +use bevy_ecs::prelude::*; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_phase::RenderPhase, + render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +/// A [`Node`] that runs the [`Transparent3d`] [`RenderPhase`]. +pub struct MainTransparentPass3dNode { + query: QueryState< + ( + &'static ExtractedCamera, + &'static RenderPhase, + &'static ViewTarget, + &'static ViewDepthTexture, + ), + With, + >, +} + +impl MainTransparentPass3dNode { + pub fn new(world: &mut World) -> Self { + Self { + query: world.query_filtered(), + } + } +} + +impl Node for MainTransparentPass3dNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + let Ok(( + camera, + transparent_phase, + target, + depth, + )) = self.query.get_manual(world, view_entity) else { + // No window + return Ok(()); + }; + + if !transparent_phase.items.is_empty() { + // Run the transparent pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("main_transparent_pass_3d"), + // NOTE: The transparent pass loads the color buffer as well as overwriting it where appropriate. + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // 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_ops: Some(Operations { + load: LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + transparent_phase.render(&mut render_pass, world, view_entity); + } + + // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't + // reset for the next render pass so add an empty render pass without a custom viewport + #[cfg(feature = "webgl")] + if camera.viewport.is_some() { + #[cfg(feature = "trace")] + let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered(); + let pass_descriptor = RenderPassDescriptor { + label: Some("reset_viewport_pass_3d"), + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: None, + }; + + render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + } + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 4bd6430287..4f1f807477 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -1,5 +1,6 @@ mod camera_3d; -mod main_pass_3d_node; +mod main_opaque_pass_3d_node; +mod main_transparent_pass_3d_node; pub mod graph { pub const NAME: &str = "core_3d"; @@ -9,7 +10,10 @@ pub mod graph { pub mod node { pub const MSAA_WRITEBACK: &str = "msaa_writeback"; pub const PREPASS: &str = "prepass"; - pub const MAIN_PASS: &str = "main_pass"; + pub const START_MAIN_PASS: &str = "start_main_pass"; + pub const MAIN_OPAQUE_PASS: &str = "main_opaque_pass"; + pub const MAIN_TRANSPARENT_PASS: &str = "main_transparent_pass"; + pub const END_MAIN_PASS: &str = "end_main_pass"; pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; pub const FXAA: &str = "fxaa"; @@ -21,7 +25,8 @@ pub mod graph { use std::cmp::Reverse; pub use camera_3d::*; -pub use main_pass_3d_node::*; +pub use main_opaque_pass_3d_node::*; +pub use main_transparent_pass_3d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; @@ -82,20 +87,33 @@ impl Plugin for Core3dPlugin { ); let prepass_node = PrepassNode::new(&mut render_app.world); - let pass_node_3d = MainPass3dNode::new(&mut render_app.world); + let opaque_node_3d = MainOpaquePass3dNode::new(&mut render_app.world); + let transparent_node_3d = MainTransparentPass3dNode::new(&mut render_app.world); let tonemapping = TonemappingNode::new(&mut render_app.world); let upscaling = UpscalingNode::new(&mut render_app.world); let mut graph = render_app.world.resource_mut::(); let mut draw_3d_graph = RenderGraph::default(); draw_3d_graph.add_node(graph::node::PREPASS, prepass_node); - draw_3d_graph.add_node(graph::node::MAIN_PASS, pass_node_3d); + draw_3d_graph.add_node(graph::node::START_MAIN_PASS, EmptyNode); + draw_3d_graph.add_node(graph::node::MAIN_OPAQUE_PASS, opaque_node_3d); + draw_3d_graph.add_node(graph::node::MAIN_TRANSPARENT_PASS, transparent_node_3d); + draw_3d_graph.add_node(graph::node::END_MAIN_PASS, EmptyNode); draw_3d_graph.add_node(graph::node::TONEMAPPING, tonemapping); draw_3d_graph.add_node(graph::node::END_MAIN_PASS_POST_PROCESSING, EmptyNode); draw_3d_graph.add_node(graph::node::UPSCALING, upscaling); - draw_3d_graph.add_node_edge(graph::node::PREPASS, graph::node::MAIN_PASS); - draw_3d_graph.add_node_edge(graph::node::MAIN_PASS, graph::node::TONEMAPPING); + draw_3d_graph.add_node_edge(graph::node::PREPASS, graph::node::START_MAIN_PASS); + draw_3d_graph.add_node_edge(graph::node::START_MAIN_PASS, graph::node::MAIN_OPAQUE_PASS); + draw_3d_graph.add_node_edge( + graph::node::MAIN_OPAQUE_PASS, + graph::node::MAIN_TRANSPARENT_PASS, + ); + draw_3d_graph.add_node_edge( + graph::node::MAIN_TRANSPARENT_PASS, + graph::node::END_MAIN_PASS, + ); + draw_3d_graph.add_node_edge(graph::node::END_MAIN_PASS, graph::node::TONEMAPPING); draw_3d_graph.add_node_edge( graph::node::TONEMAPPING, graph::node::END_MAIN_PASS_POST_PROCESSING, diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 43401ec568..c3e7697f4b 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -45,7 +45,7 @@ impl Plugin for MsaaWritebackPlugin { ); core_3d.add_node_edge( crate::core_3d::graph::node::MSAA_WRITEBACK, - crate::core_3d::graph::node::MAIN_PASS, + crate::core_3d::graph::node::START_MAIN_PASS, ); } } diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 0efde9ab1a..19d7d56347 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -81,7 +81,7 @@ impl Plugin for TemporalAntiAliasPlugin { draw_3d_graph.add_node(draw_3d_graph::node::TAA, taa_node); // MAIN_PASS -> TAA -> BLOOM -> TONEMAPPING draw_3d_graph.add_node_edge( - crate::core_3d::graph::node::MAIN_PASS, + crate::core_3d::graph::node::END_MAIN_PASS, draw_3d_graph::node::TAA, ); draw_3d_graph.add_node_edge(draw_3d_graph::node::TAA, crate::core_3d::graph::node::BLOOM); diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index b3d7741db1..4b27f57824 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -292,7 +292,7 @@ impl Plugin for PbrPlugin { draw_3d_graph.add_node(draw_3d_graph::node::SHADOW_PASS, shadow_pass_node); draw_3d_graph.add_node_edge( draw_3d_graph::node::SHADOW_PASS, - bevy_core_pipeline::core_3d::graph::node::MAIN_PASS, + bevy_core_pipeline::core_3d::graph::node::START_MAIN_PASS, ); } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 146b358da0..9eccf115c7 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -124,7 +124,7 @@ pub fn build_ui_render(app: &mut App) { RunGraphOnViewNode::new(draw_ui_graph::NAME), ); graph_3d.add_node_edge( - bevy_core_pipeline::core_3d::graph::node::MAIN_PASS, + bevy_core_pipeline::core_3d::graph::node::END_MAIN_PASS, draw_ui_graph::node::UI_PASS, ); graph_3d.add_node_edge(