diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 3261ef5a61..9b951ae2d1 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -19,6 +19,7 @@ pub mod graph { MainOpaquePass, MainTransparentPass, EndMainPass, + Wireframe, Bloom, PostProcessing, Tonemapping, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index fa092b7fd4..5444ed05ec 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -28,6 +28,7 @@ pub mod graph { MainTransmissivePass, MainTransparentPass, EndMainPass, + Wireframe, LateDownsampleDepth, Taa, MotionBlur, diff --git a/crates/bevy_pbr/src/render/wireframe.wgsl b/crates/bevy_pbr/src/render/wireframe.wgsl index 981e5e1b1d..3873ffa3dd 100644 --- a/crates/bevy_pbr/src/render/wireframe.wgsl +++ b/crates/bevy_pbr/src/render/wireframe.wgsl @@ -1,12 +1,12 @@ #import bevy_pbr::forward_io::VertexOutput -struct WireframeMaterial { - color: vec4, -}; +struct PushConstants { + color: vec4 +} + +var push_constants: PushConstants; -@group(2) @binding(0) -var material: WireframeMaterial; @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { - return material.color; + return push_constants.color; } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 88082c2880..fa4e920539 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -1,15 +1,60 @@ -use crate::{Material, MaterialPipeline, MaterialPipelineKey, MaterialPlugin, MeshMaterial3d}; -use bevy_app::{Plugin, Startup, Update}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle}; -use bevy_color::{Color, LinearRgba}; -use bevy_ecs::prelude::*; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{ - extract_resource::ExtractResource, - mesh::{Mesh3d, MeshVertexBufferLayoutRef}, - prelude::*, - render_resource::*, +use crate::{ + DrawMesh, MeshPipeline, MeshPipelineKey, RenderMeshInstanceFlags, RenderMeshInstances, + SetMeshBindGroup, SetMeshViewBindGroup, ViewKeyCache, ViewSpecializationTicks, }; +use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; +use bevy_asset::{ + load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + AssetEvents, AssetId, Assets, Handle, UntypedAssetId, +}; +use bevy_color::{Color, ColorToComponents}; +use bevy_core_pipeline::core_3d::{ + graph::{Core3d, Node3d}, + Camera3d, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Tick, + prelude::*, + query::QueryItem, + system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem}, +}; +use bevy_platform_support::{ + collections::{HashMap, HashSet}, + hash::FixedHasher, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::camera::extract_cameras; +use bevy_render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::ExtractedCamera, + extract_resource::ExtractResource, + mesh::{ + allocator::{MeshAllocator, SlabId}, + Mesh3d, MeshVertexBufferLayoutRef, RenderMesh, + }, + prelude::*, + render_asset::{ + prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, + }, + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_phase::{ + AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, + CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, + PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, + SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, + }, + render_resource::*, + renderer::RenderContext, + sync_world::{MainEntity, MainEntityHashMap}, + view::{ + ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities, + RetainedViewEntity, ViewDepthTexture, ViewTarget, + }, + Extract, Render, RenderApp, RenderDebugFlags, RenderSet, +}; +use core::{hash::Hash, ops::Range}; +use tracing::error; pub const WIREFRAME_SHADER_HANDLE: Handle = weak_handle!("2646a633-f8e3-4380-87ae-b44d881abbce"); @@ -24,9 +69,20 @@ pub const WIREFRAME_SHADER_HANDLE: Handle = /// /// This is a native only feature. #[derive(Debug, Default)] -pub struct WireframePlugin; +pub struct WireframePlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl WireframePlugin { + /// Creates a new [`WireframePlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { debug_flags } + } +} + impl Plugin for WireframePlugin { - fn build(&self, app: &mut bevy_app::App) { + fn build(&self, app: &mut App) { load_internal_asset!( app, WIREFRAME_SHADER_HANDLE, @@ -34,25 +90,83 @@ impl Plugin for WireframePlugin { Shader::from_wgsl ); - app.register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() - .add_plugins(MaterialPlugin::::default()) - .register_asset_reflect::() - .add_systems(Startup, setup_global_wireframe_material) - .add_systems( - Update, + app.add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + RenderAssetPlugin::::default(), + )) + .init_asset::() + .init_resource::>() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup_global_wireframe_material) + .add_systems( + Update, + ( + global_color_changed.run_if(resource_changed::), + wireframe_color_changed, + // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global + // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. + (apply_wireframe_material, apply_global_wireframe_material).chain(), + ), + ) + .add_systems( + PostUpdate, + check_wireframe_entities_needing_specialization + .after(AssetEvents) + .run_if(resource_exists::), + ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_render_graph_node::>(Core3d, Node3d::Wireframe) + .add_render_graph_edges( + Core3d, ( - global_color_changed.run_if(resource_changed::), - wireframe_color_changed, - // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global - // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. - (apply_wireframe_material, apply_global_wireframe_material).chain(), + Node3d::EndMainPass, + Node3d::Wireframe, + Node3d::PostProcessing, + ), + ) + .add_systems( + ExtractSchedule, + ( + extract_wireframe_3d_camera, + extract_wireframe_entities_needing_specialization.after(extract_cameras), + extract_wireframe_materials, + ), + ) + .add_systems( + Render, + ( + specialize_wireframes + .in_set(RenderSet::PrepareMeshes) + .after(prepare_assets::) + .after(prepare_assets::), + queue_wireframes + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::), ), ); } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } } /// Enables wireframe rendering for any entity it is attached to. @@ -60,34 +174,274 @@ impl Plugin for WireframePlugin { /// /// This requires the [`WireframePlugin`] to be enabled. #[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[reflect(Component, Default, Debug, PartialEq)] pub struct Wireframe; +pub struct Wireframe3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Wireframe3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: Wireframe3dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for Wireframe3d { + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + fn batch_range(&self) -> &Range { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl CachedRenderPipelinePhaseItem for Wireframe3d { + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +impl BinnedPhaseItem for Wireframe3d { + type BinKey = Wireframe3dBinKey; + type BatchSetKey = Wireframe3dBatchSetKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The wireframe material asset ID. + pub asset_id: UntypedAssetId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for Wireframe3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data that must be identical in order to *batch* phase items together. +/// +/// Note that a *batch set* (if multi-draw is in use) contains multiple batches. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBinKey { + /// The wireframe mesh asset ID. + pub asset_id: UntypedAssetId, +} + +pub struct SetWireframe3dPushConstants; + +impl RenderCommand

for SetWireframe3dPushConstants { + type Param = ( + SRes, + SRes>, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + + pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&wireframe_material.color), + ); + RenderCommandResult::Success + } +} + +pub type DrawWireframe3d = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshBindGroup<1>, + SetWireframe3dPushConstants, + DrawMesh, +); + +#[derive(Resource, Clone)] +pub struct Wireframe3dPipeline { + mesh_pipeline: MeshPipeline, + shader: Handle, +} + +impl FromWorld for Wireframe3dPipeline { + fn from_world(render_world: &mut World) -> Self { + Wireframe3dPipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: WIREFRAME_SHADER_HANDLE, + } + } +} + +impl SpecializedMeshPipeline for Wireframe3dPipeline { + type Key = MeshPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self.mesh_pipeline.specialize(key, layout)?; + descriptor.label = Some("wireframe_3d_pipeline".into()); + descriptor.push_constant_ranges.push(PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..16, + }); + let fragment = descriptor.fragment.as_mut().unwrap(); + fragment.shader = self.shader.clone(); + descriptor.primitive.polygon_mode = PolygonMode::Line; + descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0; + Ok(descriptor) + } +} + +#[derive(Default)] +struct Wireframe3dNode; +impl ViewNode for Wireframe3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(wireframe_phase) = world.get_resource::>() + else { + return Ok(()); + }; + + let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("wireframe_3d_pass"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) { + error!("Error encountered while rendering the stencil phase {err:?}"); + return Err(NodeRunError::DrawError(err)); + } + + Ok(()) + } +} + /// Sets the color of the [`Wireframe`] of the entity it is attached to. /// /// If this component is present but there's no [`Wireframe`] component, /// it will still affect the color of the wireframe when [`WireframeConfig::global`] is set to true. /// /// This overrides the [`WireframeConfig::default_color`]. -// TODO: consider caching materials based on this color. -// This could blow up in size if people use random colored wireframes for each mesh. -// It will also be important to remove unused materials from the cache. #[derive(Component, Debug, Clone, Default, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[reflect(Component, Default, Debug)] pub struct WireframeColor { pub color: Color, } +#[derive(Component, Debug, Clone, Default)] +pub struct ExtractedWireframeColor { + pub color: [f32; 4], +} + /// Disables wireframe rendering for any entity it is attached to. /// It will ignore the [`WireframeConfig`] global setting. /// /// This requires the [`WireframePlugin`] to be enabled. #[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[reflect(Component, Default, Debug, PartialEq)] pub struct NoWireframe; #[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] +#[reflect(Resource, Debug, Default)] pub struct WireframeConfig { /// Whether to show wireframes for all meshes. /// Can be overridden for individual meshes by adding a [`Wireframe`] or [`NoWireframe`] component. @@ -98,12 +452,112 @@ pub struct WireframeConfig { pub default_color: Color, } +#[derive(Asset, Reflect, Clone, Debug, Default)] +#[reflect(Clone, Default)] +pub struct WireframeMaterial { + pub color: Color, +} + +pub struct RenderWireframeMaterial { + pub color: [f32; 4], +} + +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct Mesh3dWireframe(pub Handle); + +impl AsAssetId for Mesh3dWireframe { + type Asset = WireframeMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +impl RenderAsset for RenderWireframeMaterial { + type SourceAsset = WireframeMaterial; + type Param = (); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + _param: &mut SystemParamItem, + ) -> Result> { + Ok(RenderWireframeMaterial { + color: source_asset.color.to_linear().to_f32_array(), + }) + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct RenderWireframeInstances(MainEntityHashMap>); + +#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)] +pub struct WireframeEntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)] +pub struct WireframeEntitySpecializationTicks { + pub entities: MainEntityHashMap, +} + +/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedWireframePipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedWireframeViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + #[derive(Resource)] struct GlobalWireframeMaterial { // This handle will be reused when the global config is enabled handle: Handle, } +pub fn extract_wireframe_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &Mesh3dWireframe), + Or<(Changed, Changed)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>, +) { + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.insert(entity.into(), material.id()); + } else { + material_instances.remove(&MainEntity::from(entity)); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + material_instances.remove(&MainEntity::from(entity)); + } + } +} + fn setup_global_wireframe_material( mut commands: Commands, mut materials: ResMut>, @@ -112,7 +566,7 @@ fn setup_global_wireframe_material( // Create the handle used for the global material commands.insert_resource(GlobalWireframeMaterial { handle: materials.add(WireframeMaterial { - color: config.default_color.into(), + color: config.default_color, }), }); } @@ -124,7 +578,7 @@ fn global_color_changed( global_material: Res, ) { if let Some(global_material) = materials.get_mut(&global_material.handle) { - global_material.color = config.default_color.into(); + global_material.color = config.default_color; } } @@ -132,13 +586,13 @@ fn global_color_changed( fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< - (&mut MeshMaterial3d, &WireframeColor), + (&mut Mesh3dWireframe, &WireframeColor), (With, Changed), >, ) { for (mut handle, wireframe_color) in &mut colors_changed { handle.0 = materials.add(WireframeMaterial { - color: wireframe_color.color.into(), + color: wireframe_color.color, }); } } @@ -150,22 +604,22 @@ fn apply_wireframe_material( mut materials: ResMut>, wireframes: Query< (Entity, Option<&WireframeColor>), - (With, Without>), + (With, Without), >, - no_wireframes: Query, With>)>, + no_wireframes: Query, With)>, mut removed_wireframes: RemovedComponents, global_material: Res, ) { for e in removed_wireframes.read().chain(no_wireframes.iter()) { if let Ok(mut commands) = commands.get_entity(e) { - commands.remove::>(); + commands.remove::(); } } let mut material_to_spawn = vec![]; for (e, maybe_color) in &wireframes { let material = get_wireframe_material(maybe_color, &mut materials, &global_material); - material_to_spawn.push((e, MeshMaterial3d(material))); + material_to_spawn.push((e, Mesh3dWireframe(material))); } commands.try_insert_batch(material_to_spawn); } @@ -178,12 +632,9 @@ fn apply_global_wireframe_material( config: Res, meshes_without_material: Query< (Entity, Option<&WireframeColor>), - (WireframeFilter, Without>), - >, - meshes_with_global_material: Query< - Entity, - (WireframeFilter, With>), + (WireframeFilter, Without), >, + meshes_with_global_material: Query)>, global_material: Res, mut materials: ResMut>, ) { @@ -193,14 +644,12 @@ fn apply_global_wireframe_material( let material = get_wireframe_material(maybe_color, &mut materials, &global_material); // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified - material_to_spawn.push((e, MeshMaterial3d(material))); + material_to_spawn.push((e, Mesh3dWireframe(material))); } commands.try_insert_batch(material_to_spawn); } else { for e in &meshes_with_global_material { - commands - .entity(e) - .remove::>(); + commands.entity(e).remove::(); } } } @@ -213,7 +662,7 @@ fn get_wireframe_material( ) -> Handle { if let Some(wireframe_color) = maybe_color { wireframe_materials.add(WireframeMaterial { - color: wireframe_color.color.into(), + color: wireframe_color.color, }) } else { // If there's no color specified we can use the global material since it's already set to use the default_color @@ -221,28 +670,241 @@ fn get_wireframe_material( } } -#[derive(Default, AsBindGroup, Debug, Clone, Asset, Reflect)] -#[reflect(Default, Clone)] -pub struct WireframeMaterial { - #[uniform(0)] - pub color: LinearRgba, -} - -impl Material for WireframeMaterial { - fn fragment_shader() -> ShaderRef { - WIREFRAME_SHADER_HANDLE.into() - } - - fn specialize( - _pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - _layout: &MeshVertexBufferLayoutRef, - _key: MaterialPipelineKey, - ) -> Result<(), SpecializedMeshPipelineError> { - descriptor.primitive.polygon_mode = PolygonMode::Line; - if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { - depth_stencil.bias.slope_scale = 1.0; +fn extract_wireframe_3d_camera( + mut wireframe_3d_phases: ResMut>, + cameras: Extract), With>>, + mut live_entities: Local>, + gpu_preprocessing_support: Res, +) { + live_entities.clear(); + for (main_entity, camera, no_indirect_drawing) in &cameras { + if !camera.is_active { + continue; + } + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + wireframe_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_entities.insert(retained_view_entity); + } + + // Clear out all dead views. + wireframe_3d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn extract_wireframe_entities_needing_specialization( + entities_needing_specialization: Extract>, + mut entity_specialization_ticks: ResMut, + views: Query<&ExtractedView>, + mut specialized_wireframe_pipeline_cache: ResMut, + mut removed_meshes_query: Extract>, + ticks: SystemChangeTick, +) { + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } + + for entity in removed_meshes_query.read() { + for view in &views { + if let Some(specialized_wireframe_pipeline_cache) = + specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity) + { + specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity)); + } + } + } +} + +pub fn check_wireframe_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + >, + mut entities_needing_specialization: ResMut, +) { + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} + +pub fn specialize_wireframes( + render_meshes: Res>, + render_mesh_instances: Res, + render_wireframe_instances: Res, + render_visibility_ranges: Res, + wireframe_phases: Res>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !wireframe_phases.contains_key(&view.retained_view_entity) { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + if !render_wireframe_instances.contains_key(visible_entity) { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key; + mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology()); + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +fn queue_wireframes( + custom_draw_functions: Res>, + render_mesh_instances: Res, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + specialized_wireframe_pipeline_cache: Res, + render_wireframe_instances: Res, + mut wireframe_3d_phases: ResMut>, + mut views: Query<(&ExtractedView, &RenderVisibleEntities)>, +) { + for (view, visible_entities) in &mut views { + let Some(wireframe_phase) = wireframe_3d_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let draw_wireframe = custom_draw_functions.read().id::(); + + let Some(view_specialized_material_pipeline_cache) = + specialized_wireframe_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else { + continue; + }; + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) { + continue; + } + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let bin_key = Wireframe3dBinKey { + asset_id: mesh_instance.mesh_asset_id.untyped(), + }; + let batch_set_key = Wireframe3dBatchSetKey { + pipeline: pipeline_id, + asset_id: wireframe_instance.untyped(), + draw_function: draw_wireframe, + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + wireframe_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); } - Ok(()) } } diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 439dd9a643..0aef177765 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -1,18 +1,61 @@ -use crate::{Material2d, Material2dKey, Material2dPlugin, Mesh2d}; -use bevy_app::{Plugin, Startup, Update}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle}; -use bevy_color::{Color, LinearRgba}; -use bevy_ecs::prelude::*; +use crate::{ + DrawMesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup, + SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks, +}; +use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; +use bevy_asset::{ + load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + AssetEvents, AssetId, Assets, Handle, UntypedAssetId, +}; +use bevy_color::{Color, ColorToComponents}; +use bevy_core_pipeline::core_2d::{ + graph::{Core2d, Node2d}, + Camera2d, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Tick, + prelude::*, + query::QueryItem, + system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem}, +}; +use bevy_platform_support::{ + collections::{HashMap, HashSet}, + hash::FixedHasher, +}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - extract_resource::ExtractResource, mesh::MeshVertexBufferLayoutRef, prelude::*, + batching::gpu_preprocessing::GpuPreprocessingMode, + camera::ExtractedCamera, + extract_resource::ExtractResource, + mesh::{ + allocator::{MeshAllocator, SlabId}, + Mesh2d, MeshVertexBufferLayoutRef, RenderMesh, + }, + prelude::*, + render_asset::{ + prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, + }, + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_phase::{ + AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, + CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem, + PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, + SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, + }, render_resource::*, + renderer::RenderContext, + sync_world::{MainEntity, MainEntityHashMap}, + view::{ + ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget, + }, + Extract, Render, RenderApp, RenderDebugFlags, RenderSet, }; - -use super::MeshMaterial2d; +use core::{hash::Hash, ops::Range}; +use tracing::error; pub const WIREFRAME_2D_SHADER_HANDLE: Handle = - weak_handle!("3d8a3853-2927-4de2-9dc7-3971e7e40970"); + weak_handle!("2d8a3853-2927-4de2-9dc7-3971e7e40970"); /// A [`Plugin`] that draws wireframes for 2D meshes. /// @@ -24,9 +67,20 @@ pub const WIREFRAME_2D_SHADER_HANDLE: Handle = /// /// This is a native only feature. #[derive(Debug, Default)] -pub struct Wireframe2dPlugin; +pub struct Wireframe2dPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Wireframe2dPlugin { + /// Creates a new [`Wireframe2dPlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { debug_flags } + } +} + impl Plugin for Wireframe2dPlugin { - fn build(&self, app: &mut bevy_app::App) { + fn build(&self, app: &mut App) { load_internal_asset!( app, WIREFRAME_2D_SHADER_HANDLE, @@ -34,25 +88,83 @@ impl Plugin for Wireframe2dPlugin { Shader::from_wgsl ); - app.register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() - .add_plugins(Material2dPlugin::::default()) - .register_asset_reflect::() - .add_systems(Startup, setup_global_wireframe_material) - .add_systems( - Update, + app.add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + RenderAssetPlugin::::default(), + )) + .init_asset::() + .init_resource::>() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup_global_wireframe_material) + .add_systems( + Update, + ( + global_color_changed.run_if(resource_changed::), + wireframe_color_changed, + // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global + // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. + (apply_wireframe_material, apply_global_wireframe_material).chain(), + ), + ) + .add_systems( + PostUpdate, + check_wireframe_entities_needing_specialization + .after(AssetEvents) + .run_if(resource_exists::), + ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_render_graph_node::>(Core2d, Node2d::Wireframe) + .add_render_graph_edges( + Core2d, ( - global_color_changed.run_if(resource_changed::), - wireframe_color_changed, - // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global - // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. - (apply_wireframe_material, apply_global_wireframe_material).chain(), + Node2d::EndMainPass, + Node2d::Wireframe, + Node2d::PostProcessing, + ), + ) + .add_systems( + ExtractSchedule, + ( + extract_wireframe_2d_camera, + extract_wireframe_entities_needing_specialization, + extract_wireframe_materials, + ), + ) + .add_systems( + Render, + ( + specialize_wireframes + .in_set(RenderSet::PrepareMeshes) + .after(prepare_assets::) + .after(prepare_assets::), + queue_wireframes + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::), ), ); } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } } /// Enables wireframe rendering for any entity it is attached to. @@ -60,9 +172,248 @@ impl Plugin for Wireframe2dPlugin { /// /// This requires the [`Wireframe2dPlugin`] to be enabled. #[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[reflect(Component, Default, Debug, PartialEq)] pub struct Wireframe2d; +pub struct Wireframe2dPhaseItem { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Wireframe2dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: Wireframe2dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for Wireframe2dPhaseItem { + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + fn batch_range(&self) -> &Range { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl CachedRenderPipelinePhaseItem for Wireframe2dPhaseItem { + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +impl BinnedPhaseItem for Wireframe2dPhaseItem { + type BinKey = Wireframe2dBinKey; + type BatchSetKey = Wireframe2dBatchSetKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe2dBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The wireframe material asset ID. + pub asset_id: UntypedAssetId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for Wireframe2dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data that must be identical in order to *batch* phase items together. +/// +/// Note that a *batch set* (if multi-draw is in use) contains multiple batches. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe2dBinKey { + /// The wireframe mesh asset ID. + pub asset_id: UntypedAssetId, +} + +pub struct SetWireframe2dPushConstants; + +impl RenderCommand

for SetWireframe2dPushConstants { + type Param = ( + SRes, + SRes>, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + + pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&wireframe_material.color), + ); + RenderCommandResult::Success + } +} + +pub type DrawWireframe2d = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetMesh2dBindGroup<1>, + SetWireframe2dPushConstants, + DrawMesh2d, +); + +#[derive(Resource, Clone)] +pub struct Wireframe2dPipeline { + mesh_pipeline: Mesh2dPipeline, + shader: Handle, +} + +impl FromWorld for Wireframe2dPipeline { + fn from_world(render_world: &mut World) -> Self { + Wireframe2dPipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: WIREFRAME_2D_SHADER_HANDLE, + } + } +} + +impl SpecializedMeshPipeline for Wireframe2dPipeline { + type Key = Mesh2dPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self.mesh_pipeline.specialize(key, layout)?; + descriptor.label = Some("wireframe_2d_pipeline".into()); + descriptor.push_constant_ranges.push(PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..16, + }); + let fragment = descriptor.fragment.as_mut().unwrap(); + fragment.shader = self.shader.clone(); + descriptor.primitive.polygon_mode = PolygonMode::Line; + descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0; + Ok(descriptor) + } +} + +#[derive(Default)] +struct Wireframe2dNode; +impl ViewNode for Wireframe2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(wireframe_phase) = + world.get_resource::>() + else { + return Ok(()); + }; + + let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("wireframe_2d_pass"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) { + error!("Error encountered while rendering the stencil phase {err:?}"); + return Err(NodeRunError::DrawError(err)); + } + + Ok(()) + } +} + /// Sets the color of the [`Wireframe2d`] of the entity it is attached to. /// /// If this component is present but there's no [`Wireframe2d`] component, @@ -70,23 +421,28 @@ pub struct Wireframe2d; /// /// This overrides the [`Wireframe2dConfig::default_color`]. #[derive(Component, Debug, Clone, Default, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[reflect(Component, Default, Debug)] pub struct Wireframe2dColor { pub color: Color, } +#[derive(Component, Debug, Clone, Default)] +pub struct ExtractedWireframeColor { + pub color: [f32; 4], +} + /// Disables wireframe rendering for any entity it is attached to. /// It will ignore the [`Wireframe2dConfig`] global setting. /// /// This requires the [`Wireframe2dPlugin`] to be enabled. #[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[reflect(Component, Default, Debug, PartialEq)] pub struct NoWireframe2d; #[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] +#[reflect(Resource, Debug, Default)] pub struct Wireframe2dConfig { - /// Whether to show wireframes for all 2D meshes. + /// Whether to show wireframes for all meshes. /// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component. pub global: bool, /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have @@ -95,21 +451,121 @@ pub struct Wireframe2dConfig { pub default_color: Color, } +#[derive(Asset, Reflect, Clone, Debug, Default)] +#[reflect(Clone, Default)] +pub struct Wireframe2dMaterial { + pub color: Color, +} + +pub struct RenderWireframeMaterial { + pub color: [f32; 4], +} + +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct Mesh2dWireframe(pub Handle); + +impl AsAssetId for Mesh2dWireframe { + type Asset = Wireframe2dMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +impl RenderAsset for RenderWireframeMaterial { + type SourceAsset = Wireframe2dMaterial; + type Param = (); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + _param: &mut SystemParamItem, + ) -> Result> { + Ok(RenderWireframeMaterial { + color: source_asset.color.to_linear().to_f32_array(), + }) + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct RenderWireframeInstances(MainEntityHashMap>); + +#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)] +pub struct WireframeEntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)] +pub struct WireframeEntitySpecializationTicks { + pub entities: MainEntityHashMap, +} + +/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedWireframePipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedWireframeViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + #[derive(Resource)] -struct GlobalWireframe2dMaterial { +struct GlobalWireframeMaterial { // This handle will be reused when the global config is enabled handle: Handle, } +pub fn extract_wireframe_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &Mesh2dWireframe), + Or<(Changed, Changed)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>, +) { + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.insert(entity.into(), material.id()); + } else { + material_instances.remove(&MainEntity::from(entity)); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + material_instances.remove(&MainEntity::from(entity)); + } + } +} + fn setup_global_wireframe_material( mut commands: Commands, mut materials: ResMut>, config: Res, ) { // Create the handle used for the global material - commands.insert_resource(GlobalWireframe2dMaterial { + commands.insert_resource(GlobalWireframeMaterial { handle: materials.add(Wireframe2dMaterial { - color: config.default_color.into(), + color: config.default_color, }), }); } @@ -118,10 +574,10 @@ fn setup_global_wireframe_material( fn global_color_changed( config: Res, mut materials: ResMut>, - global_material: Res, + global_material: Res, ) { if let Some(global_material) = materials.get_mut(&global_material.handle) { - global_material.color = config.default_color.into(); + global_material.color = config.default_color; } } @@ -129,13 +585,13 @@ fn global_color_changed( fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< - (&mut MeshMaterial2d, &Wireframe2dColor), + (&mut Mesh2dWireframe, &Wireframe2dColor), (With, Changed), >, ) { for (mut handle, wireframe_color) in &mut colors_changed { handle.0 = materials.add(Wireframe2dMaterial { - color: wireframe_color.color.into(), + color: wireframe_color.color, }); } } @@ -147,100 +603,277 @@ fn apply_wireframe_material( mut materials: ResMut>, wireframes: Query< (Entity, Option<&Wireframe2dColor>), - ( - With, - Without>, - ), - >, - no_wireframes: Query< - Entity, - ( - With, - With>, - ), + (With, Without), >, + no_wireframes: Query, With)>, mut removed_wireframes: RemovedComponents, - global_material: Res, + global_material: Res, ) { for e in removed_wireframes.read().chain(no_wireframes.iter()) { if let Ok(mut commands) = commands.get_entity(e) { - commands.remove::>(); + commands.remove::(); } } - let mut wireframes_to_spawn = vec![]; - for (e, wireframe_color) in &wireframes { - let material = if let Some(wireframe_color) = wireframe_color { - materials.add(Wireframe2dMaterial { - color: wireframe_color.color.into(), - }) - } else { - // If there's no color specified we can use the global material since it's already set to use the default_color - global_material.handle.clone() - }; - wireframes_to_spawn.push((e, MeshMaterial2d(material))); + let mut material_to_spawn = vec![]; + for (e, maybe_color) in &wireframes { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + material_to_spawn.push((e, Mesh2dWireframe(material))); } - commands.try_insert_batch(wireframes_to_spawn); + commands.try_insert_batch(material_to_spawn); } -type Wireframe2dFilter = (With, Without, Without); +type WireframeFilter = (With, Without, Without); /// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component. fn apply_global_wireframe_material( mut commands: Commands, config: Res, meshes_without_material: Query< - Entity, - ( - Wireframe2dFilter, - Without>, - ), + (Entity, Option<&Wireframe2dColor>), + (WireframeFilter, Without), >, - meshes_with_global_material: Query< - Entity, - (Wireframe2dFilter, With>), - >, - global_material: Res, + meshes_with_global_material: Query)>, + global_material: Res, + mut materials: ResMut>, ) { if config.global { let mut material_to_spawn = vec![]; - for e in &meshes_without_material { + for (e, maybe_color) in &meshes_without_material { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified - material_to_spawn.push((e, MeshMaterial2d(global_material.handle.clone()))); + material_to_spawn.push((e, Mesh2dWireframe(material))); } commands.try_insert_batch(material_to_spawn); } else { for e in &meshes_with_global_material { - commands - .entity(e) - .remove::>(); + commands.entity(e).remove::(); } } } -#[derive(Default, AsBindGroup, Debug, Clone, Asset, Reflect)] -#[reflect(Clone, Default)] -pub struct Wireframe2dMaterial { - #[uniform(0)] - pub color: LinearRgba, -} - -impl Material2d for Wireframe2dMaterial { - fn fragment_shader() -> ShaderRef { - WIREFRAME_2D_SHADER_HANDLE.into() - } - - fn depth_bias(&self) -> f32 { - 1.0 - } - - fn specialize( - descriptor: &mut RenderPipelineDescriptor, - _layout: &MeshVertexBufferLayoutRef, - _key: Material2dKey, - ) -> Result<(), SpecializedMeshPipelineError> { - descriptor.primitive.polygon_mode = PolygonMode::Line; - Ok(()) +/// Gets a handle to a wireframe material with a fallback on the default material +fn get_wireframe_material( + maybe_color: Option<&Wireframe2dColor>, + wireframe_materials: &mut Assets, + global_material: &GlobalWireframeMaterial, +) -> Handle { + if let Some(wireframe_color) = maybe_color { + wireframe_materials.add(Wireframe2dMaterial { + color: wireframe_color.color, + }) + } else { + // If there's no color specified we can use the global material since it's already set to use the default_color + global_material.handle.clone() + } +} + +fn extract_wireframe_2d_camera( + mut wireframe_2d_phases: ResMut>, + cameras: Extract>>, + mut live_entities: Local>, +) { + live_entities.clear(); + for (main_entity, camera) in &cameras { + if !camera.is_active { + continue; + } + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + wireframe_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None); + live_entities.insert(retained_view_entity); + } + + // Clear out all dead views. + wireframe_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn extract_wireframe_entities_needing_specialization( + entities_needing_specialization: Extract>, + mut entity_specialization_ticks: ResMut, + views: Query<&ExtractedView>, + mut specialized_wireframe_pipeline_cache: ResMut, + mut removed_meshes_query: Extract>, + ticks: SystemChangeTick, +) { + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } + + for entity in removed_meshes_query.read() { + for view in &views { + if let Some(specialized_wireframe_pipeline_cache) = + specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity) + { + specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity)); + } + } + } +} + +pub fn check_wireframe_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + >, + mut entities_needing_specialization: ResMut, +) { + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} + +pub fn specialize_wireframes( + render_meshes: Res>, + render_mesh_instances: Res, + render_wireframe_instances: Res, + wireframe_phases: Res>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !wireframe_phases.contains_key(&view.retained_view_entity) { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity.main_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity.main_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + if !render_wireframe_instances.contains_key(visible_entity) { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else { + continue; + }; + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key; + mesh_key |= Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()); + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +fn queue_wireframes( + custom_draw_functions: Res>, + render_mesh_instances: Res, + mesh_allocator: Res, + specialized_wireframe_pipeline_cache: Res, + render_wireframe_instances: Res, + mut wireframe_2d_phases: ResMut>, + mut views: Query<(&ExtractedView, &RenderVisibleEntities)>, +) { + for (view, visible_entities) in &mut views { + let Some(wireframe_phase) = wireframe_2d_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let draw_wireframe = custom_draw_functions.read().id::(); + + let Some(view_specialized_material_pipeline_cache) = + specialized_wireframe_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else { + continue; + }; + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) { + continue; + } + let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let bin_key = Wireframe2dBinKey { + asset_id: mesh_instance.mesh_asset_id.untyped(), + }; + let batch_set_key = Wireframe2dBatchSetKey { + pipeline: pipeline_id, + asset_id: wireframe_instance.untyped(), + draw_function: draw_wireframe, + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + wireframe_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + InputUniformIndex::default(), + if mesh_instance.automatic_batching { + BinnedRenderPhaseType::BatchableMesh + } else { + BinnedRenderPhaseType::UnbatchableMesh + }, + current_change_tick, + ); + } } } diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl index fac02d6456..c7bb3aa791 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl @@ -1,11 +1,12 @@ #import bevy_sprite::mesh2d_vertex_output::VertexOutput -struct WireframeMaterial { - color: vec4, -}; +struct PushConstants { + color: vec4 +} + +var push_constants: PushConstants; -@group(2) @binding(0) var material: WireframeMaterial; @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { - return material.color; + return push_constants.color; } diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index bde8e7bb8c..dbdd846eba 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -12,7 +12,7 @@ fn main() { app.add_plugins(( DefaultPlugins, #[cfg(not(target_arch = "wasm32"))] - Wireframe2dPlugin, + Wireframe2dPlugin::default(), )) .add_systems(Startup, setup); #[cfg(not(target_arch = "wasm32"))] diff --git a/examples/2d/wireframe_2d.rs b/examples/2d/wireframe_2d.rs index fe4cc26187..aac599fcb0 100644 --- a/examples/2d/wireframe_2d.rs +++ b/examples/2d/wireframe_2d.rs @@ -31,7 +31,7 @@ fn main() { ..default() }), // You need to add this plugin to enable wireframe rendering - Wireframe2dPlugin, + Wireframe2dPlugin::default(), )) // Wireframes can be configured with this resource. This can be changed at runtime. .insert_resource(Wireframe2dConfig { diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 0346abd352..2ef3a65dc3 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -22,7 +22,7 @@ fn main() { .add_plugins(( DefaultPlugins.set(ImagePlugin::default_nearest()), #[cfg(not(target_arch = "wasm32"))] - WireframePlugin, + WireframePlugin::default(), )) .add_systems(Startup, setup) .add_systems( diff --git a/examples/3d/wireframe.rs b/examples/3d/wireframe.rs index e62e9f0029..eedee90f96 100644 --- a/examples/3d/wireframe.rs +++ b/examples/3d/wireframe.rs @@ -31,7 +31,7 @@ fn main() { ..default() }), // You need to add this plugin to enable wireframe rendering - WireframePlugin, + WireframePlugin::default(), )) // Wireframes can be configured with this resource. This can be changed at runtime. .insert_resource(WireframeConfig {