diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index f02c78cf6d0f6..befa69a3a65e8 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -11,6 +11,7 @@ pub mod graph { pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; pub const FXAA: &str = "fxaa"; + pub const GIZMO: &str = "gizmo"; pub const UPSCALING: &str = "upscaling"; pub const END_MAIN_PASS_POST_PROCESSING: &str = "end_main_pass_post_processing"; } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index d69f6c17b6d78..9d633c3292471 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -12,6 +12,7 @@ pub mod graph { pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; pub const FXAA: &str = "fxaa"; + pub const GIZMO: &str = "gizmo"; pub const UPSCALING: &str = "upscaling"; pub const END_MAIN_PASS_POST_PROCESSING: &str = "end_main_pass_post_processing"; } diff --git a/crates/bevy_core_pipeline/src/gizmo_2d/mod.rs b/crates/bevy_core_pipeline/src/gizmo_2d/mod.rs new file mode 100644 index 0000000000000..7ee2afbd68d39 --- /dev/null +++ b/crates/bevy_core_pipeline/src/gizmo_2d/mod.rs @@ -0,0 +1,104 @@ +use bevy_app::Plugin; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{Commands, Query}, +}; +use bevy_render::{ + prelude::Camera, + render_graph::RenderGraph, + render_phase::{ + sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, + RenderPhase, + }, + render_resource::*, + Extract, ExtractSchedule, RenderApp, +}; +use bevy_utils::FloatOrd; + +use crate::core_2d::{self, Camera2d}; + +use self::node::Gizmo2dNode; + +pub mod node; + +pub struct Gizmo2dPlugin; + +impl Plugin for Gizmo2dPlugin { + fn build(&self, app: &mut bevy_app::App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + + render_app + .init_resource::>() + .add_system(sort_phase_system::) + .add_system_to_schedule(ExtractSchedule, extract_gizmo_line_2d_camera_phase); + + let gizmo_node = Gizmo2dNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_2d::graph::NAME).unwrap(); + + graph.add_node(core_2d::graph::node::GIZMO, gizmo_node); + graph.add_slot_edge( + graph.input_node().id, + core_2d::graph::input::VIEW_ENTITY, + core_2d::graph::node::GIZMO, + Gizmo2dNode::IN_VIEW, + ); + graph.add_node_edge( + core_2d::graph::node::END_MAIN_PASS_POST_PROCESSING, + core_2d::graph::node::GIZMO, + ); + graph.add_node_edge(core_2d::graph::node::GIZMO, core_2d::graph::node::UPSCALING); + } +} + +pub struct GizmoLine2d { + pub sort_key: FloatOrd, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for GizmoLine2d { + type SortKey = FloatOrd; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + items.sort_by_key(|item| item.sort_key()); + } +} + +impl CachedRenderPipelinePhaseItem for GizmoLine2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub fn extract_gizmo_line_2d_camera_phase( + mut commands: Commands, + cameras_2d: Extract>>, +) { + for (entity, camera) in &cameras_2d { + if camera.is_active { + commands + .get_or_spawn(entity) + .insert(RenderPhase::::default()); + } + } +} diff --git a/crates/bevy_core_pipeline/src/gizmo_2d/node.rs b/crates/bevy_core_pipeline/src/gizmo_2d/node.rs new file mode 100644 index 0000000000000..8b089c5f8b0ef --- /dev/null +++ b/crates/bevy_core_pipeline/src/gizmo_2d/node.rs @@ -0,0 +1,68 @@ +use bevy_ecs::prelude::*; +use bevy_render::{ + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_phase::RenderPhase, + render_resource::{LoadOp, Operations, RenderPassDescriptor}, + renderer::RenderContext, + view::{ExtractedView, ViewTarget}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use super::GizmoLine2d; + +pub struct Gizmo2dNode { + view_query: + QueryState<(&'static ViewTarget, &'static RenderPhase), With>, +} + +impl Gizmo2dNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for Gizmo2dNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let Ok(( + view_target, + gizmo_phase, + )) = self.view_query.get_manual(world, view_entity) else { + return Ok(()); + }; + { + #[cfg(feature = "trace")] + let _gizmo_line_2d_pass = info_span!("gizmo_line_2d_pass").entered(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("gizmo_line_2d"), + color_attachments: &[Some(view_target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: None, + }); + + gizmo_phase.render(&mut render_pass, world, view_entity); + } + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/gizmo_3d/mod.rs b/crates/bevy_core_pipeline/src/gizmo_3d/mod.rs new file mode 100644 index 0000000000000..6166b76ad1f2a --- /dev/null +++ b/crates/bevy_core_pipeline/src/gizmo_3d/mod.rs @@ -0,0 +1,109 @@ +use std::cmp::Reverse; + +use bevy_app::Plugin; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{Commands, Query}, +}; +use bevy_render::{ + prelude::Camera, + render_graph::RenderGraph, + render_phase::{ + sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, + RenderPhase, + }, + render_resource::*, + Extract, ExtractSchedule, RenderApp, +}; +use bevy_utils::FloatOrd; +use core_3d::Camera3d; + +use crate::core_3d; + +use self::node::Gizmo3dNode; + +mod node; + +pub struct Gizmo3dPlugin; + +impl Plugin for Gizmo3dPlugin { + fn build(&self, app: &mut bevy_app::App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + + render_app + .init_resource::>() + .add_system(sort_phase_system::) + .add_system_to_schedule(ExtractSchedule, extract_gizmo_line_3d_camera_phase); + + let gizmo_node = Gizmo3dNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_3d::graph::NAME).unwrap(); + + graph.add_node(core_3d::graph::node::GIZMO, gizmo_node); + graph.add_slot_edge( + graph.input_node().id, + core_3d::graph::input::VIEW_ENTITY, + core_3d::graph::node::GIZMO, + Gizmo3dNode::IN_VIEW, + ); + graph.add_node_edge( + core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING, + core_3d::graph::node::GIZMO, + ); + graph.add_node_edge(core_3d::graph::node::GIZMO, core_3d::graph::node::UPSCALING); + } +} + +pub struct GizmoLine3d { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for GizmoLine3d { + // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort. + type SortKey = Reverse; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + Reverse(FloatOrd(self.distance)) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + // Key negated to match reversed SortKey ordering + radsort::sort_by_key(items, |item| -item.distance); + } +} + +impl CachedRenderPipelinePhaseItem for GizmoLine3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub fn extract_gizmo_line_3d_camera_phase( + mut commands: Commands, + cameras_3d: Extract>>, +) { + for (entity, camera) in &cameras_3d { + if camera.is_active { + commands + .get_or_spawn(entity) + .insert(RenderPhase::::default()); + } + } +} diff --git a/crates/bevy_core_pipeline/src/gizmo_3d/node.rs b/crates/bevy_core_pipeline/src/gizmo_3d/node.rs new file mode 100644 index 0000000000000..a1c48d5e7d7c9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/gizmo_3d/node.rs @@ -0,0 +1,85 @@ +use bevy_ecs::prelude::*; +use bevy_render::{ + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + 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 crate::core_3d::Camera3dDepthLoadOp; + +use super::GizmoLine3d; + +pub struct Gizmo3dNode { + view_query: QueryState< + ( + &'static ViewTarget, + &'static RenderPhase, + &'static ViewDepthTexture, + ), + With, + >, +} + +impl Gizmo3dNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for Gizmo3dNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let Ok(( + view_target, + gizmo_phase, + depth, + )) = self.view_query.get_manual(world, view_entity) else { + return Ok(()); + }; + + { + #[cfg(feature = "trace")] + let _gizmo_line_3d_pass = info_span!("gizmo_line_3d_pass").entered(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("gizmo_line_3d"), + color_attachments: &[Some(view_target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(Operations { + load: Camera3dDepthLoadOp::Load.into(), + store: false, + }), + stencil_ops: None, + }), + }); + + gizmo_phase.render(&mut render_pass, world, view_entity); + } + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 5b0fe9eaea21c..f300573fddbcc 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -4,6 +4,8 @@ pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; pub mod fxaa; +pub mod gizmo_2d; +pub mod gizmo_3d; pub mod prepass; pub mod tonemapping; pub mod upscaling; diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index dd6b42efdc960..50e50a669d751 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -21,3 +21,5 @@ bevy_utils = { path = "../bevy_utils", version = "0.9.0" } bevy_core = { path = "../bevy_core", version = "0.9.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.9.0" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.9.0" } + +radsort = "0.1" diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3e388b42934a2..1975d49b37800 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -2,6 +2,10 @@ use std::mem; use bevy_app::{CoreSet, Plugin}; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_core_pipeline::{ + gizmo_2d::{Gizmo2dPlugin, GizmoLine2d}, + gizmo_3d::{Gizmo3dPlugin, GizmoLine3d}, +}; use bevy_ecs::{ prelude::{Component, DetectChanges}, schedule::IntoSystemConfig, @@ -51,31 +55,35 @@ impl Plugin for GizmoPlugin { .init_resource::() .add_system(update_gizmo_meshes.in_base_set(CoreSet::Last)); + #[cfg(feature = "bevy_sprite")] + app.add_plugin(Gizmo2dPlugin); + + #[cfg(feature = "bevy_pbr")] + app.add_plugin(Gizmo3dPlugin); + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app.add_system_to_schedule(ExtractSchedule, extract_gizmo_data); #[cfg(feature = "bevy_sprite")] { - use bevy_core_pipeline::core_2d::Transparent2d; use pipeline_2d::*; render_app - .add_render_command::() - .init_resource::() - .init_resource::>() + .init_resource::() + .init_resource::>() + .add_render_command::() .add_system(queue_gizmos_2d.in_set(RenderSet::Queue)); } #[cfg(feature = "bevy_pbr")] { - use bevy_core_pipeline::core_3d::Opaque3d; use pipeline_3d::*; render_app - .add_render_command::() - .init_resource::() - .init_resource::>() + .init_resource::() + .init_resource::>() + .add_render_command::() .add_system(queue_gizmos_3d.in_set(RenderSet::Queue)); } } diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 905be5bc6074b..e2162e995e0d6 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,5 +1,5 @@ use bevy_asset::Handle; -use bevy_core_pipeline::core_2d::Transparent2d; +use bevy_core_pipeline::gizmo_2d::GizmoLine2d; use bevy_ecs::{ prelude::Entity, query::With, @@ -20,21 +20,21 @@ use bevy_utils::FloatOrd; use crate::{GizmoMesh, LINE_SHADER_HANDLE}; #[derive(Resource)] -pub(crate) struct GizmoLinePipeline { +pub(crate) struct GizmoPipeline2d { mesh_pipeline: Mesh2dPipeline, shader: Handle, } -impl FromWorld for GizmoLinePipeline { +impl FromWorld for GizmoPipeline2d { fn from_world(render_world: &mut World) -> Self { - GizmoLinePipeline { + GizmoPipeline2d { mesh_pipeline: render_world.resource::().clone(), shader: LINE_SHADER_HANDLE.typed(), } } } -impl SpecializedMeshPipeline for GizmoLinePipeline { +impl SpecializedMeshPipeline for GizmoPipeline2d { type Key = Mesh2dPipelineKey; fn specialize( @@ -94,16 +94,16 @@ pub(crate) type DrawGizmoLines = ( #[allow(clippy::too_many_arguments)] pub(crate) fn queue_gizmos_2d( - draw_functions: Res>, - pipeline: Res, + draw_functions: Res>, + pipeline: Res, pipeline_cache: Res, - mut specialized_pipelines: ResMut>, + mut specialized_pipelines: ResMut>, gpu_meshes: Res>, msaa: Res, mesh_handles: Query<(Entity, &Mesh2dHandle), With>, - mut views: Query<&mut RenderPhase>, + mut views: Query<&mut RenderPhase>, ) { - let draw_function = draw_functions.read().get_id::().unwrap(); + let draw_function = draw_functions.read().id::(); let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()); for mut phase in &mut views { for (entity, mesh_handle) in &mesh_handles { @@ -113,12 +113,11 @@ pub(crate) fn queue_gizmos_2d( let pipeline = specialized_pipelines .specialize(&pipeline_cache, &pipeline, key, &mesh.layout) .unwrap(); - phase.add(Transparent2d { + phase.add(GizmoLine2d { entity, draw_function, pipeline, sort_key: FloatOrd(f32::MAX), - batch_range: None, }); } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 2fec669eb6a2d..1e364f3260de1 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,5 +1,5 @@ use bevy_asset::Handle; -use bevy_core_pipeline::core_3d::Opaque3d; +use bevy_core_pipeline::gizmo_3d::GizmoLine3d; use bevy_ecs::{ entity::Entity, query::With, @@ -24,21 +24,21 @@ use bevy_render::{ use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE}; #[derive(Resource)] -pub(crate) struct GizmoPipeline { +pub(crate) struct GizmoPipeline3d { mesh_pipeline: MeshPipeline, shader: Handle, } -impl FromWorld for GizmoPipeline { +impl FromWorld for GizmoPipeline3d { fn from_world(render_world: &mut World) -> Self { - GizmoPipeline { + GizmoPipeline3d { mesh_pipeline: render_world.resource::().clone(), shader: LINE_SHADER_HANDLE.typed(), } } } -impl SpecializedMeshPipeline for GizmoPipeline { +impl SpecializedMeshPipeline for GizmoPipeline3d { type Key = (bool, MeshPipelineKey); fn specialize( @@ -155,38 +155,40 @@ pub(crate) type DrawGizmoLines = ( #[allow(clippy::too_many_arguments)] pub(crate) fn queue_gizmos_3d( - draw_functions: Res>, - pipeline: Res, - mut pipelines: ResMut>, + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, pipeline_cache: Res, render_meshes: Res>, msaa: Res, mesh_handles: Query<(Entity, &Handle), With>, config: Res, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, ) { - let draw_function = draw_functions.read().get_id::().unwrap(); + let draw_function = draw_functions.read().id::(); let key = MeshPipelineKey::from_msaa_samples(msaa.samples()); for (view, mut phase) in &mut views { let key = key | MeshPipelineKey::from_hdr(view.hdr); for (entity, mesh_handle) in &mesh_handles { - if let Some(mesh) = render_meshes.get(mesh_handle) { - let key = key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); - let pipeline = pipelines - .specialize( - &pipeline_cache, - &pipeline, - (!config.on_top, key), - &mesh.layout, - ) - .unwrap(); - phase.add(Opaque3d { - entity, - pipeline, - draw_function, - distance: 0., - }); - } + let Some(mesh) = render_meshes.get(mesh_handle) else { + continue; + }; + + let key = key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); + let pipeline = pipelines + .specialize( + &pipeline_cache, + &pipeline, + (!config.on_top, key), + &mesh.layout, + ) + .unwrap(); + phase.add(GizmoLine3d { + entity, + pipeline, + draw_function, + distance: 0., + }); } } } diff --git a/examples/3d/3d_gizmos.rs b/examples/3d/3d_gizmos.rs index ed63623716fb0..288eb219f4dd1 100644 --- a/examples/3d/3d_gizmos.rs +++ b/examples/3d/3d_gizmos.rs @@ -9,14 +9,43 @@ fn main() { .add_startup_system(setup) .add_system(system) .add_system(rotate_camera) + .add_system(update_config) .run(); } -fn setup(mut commands: Commands) { +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { commands.spawn(Camera3dBundle { transform: Transform::from_xyz(0., 1.5, 6.).looking_at(Vec3::ZERO, Vec3::Y), ..default() }); + + // plane + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })), + material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()), + ..default() + }); + // cube + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); } fn system(mut gizmos: Gizmos, time: Res