Skip to main content

bevy_sprite_render/mesh2d/
wireframe2d.rs

1use crate::{
2    init_mesh_2d_pipeline, DrawMesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances,
3    SetMesh2dBindGroup, SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks,
4};
5use bevy_app::{App, Plugin, PostUpdate, Startup, Update};
6use bevy_asset::{
7    embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp,
8    AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId,
9};
10use bevy_camera::{visibility::ViewVisibility, Camera, Camera2d};
11use bevy_color::{Color, ColorToComponents};
12use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d};
13use bevy_derive::{Deref, DerefMut};
14use bevy_ecs::{
15    change_detection::Tick,
16    prelude::*,
17    query::QueryItem,
18    system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem},
19};
20use bevy_mesh::{Mesh2d, MeshVertexBufferLayoutRef};
21use bevy_platform::{
22    collections::{HashMap, HashSet},
23    hash::FixedHasher,
24};
25use bevy_reflect::{std_traits::ReflectDefault, Reflect};
26use bevy_render::{
27    batching::gpu_preprocessing::GpuPreprocessingMode,
28    camera::ExtractedCamera,
29    diagnostic::RecordDiagnostics,
30    extract_resource::ExtractResource,
31    mesh::{
32        allocator::{MeshAllocator, SlabId},
33        RenderMesh,
34    },
35    prelude::*,
36    render_asset::{
37        prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
38    },
39    render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},
40    render_phase::{
41        AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType,
42        CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem,
43        PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
44        SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases,
45    },
46    render_resource::*,
47    renderer::RenderContext,
48    sync_world::{MainEntity, MainEntityHashMap},
49    view::{
50        ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget,
51    },
52    Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,
53};
54use bevy_shader::Shader;
55use core::{hash::Hash, ops::Range};
56use tracing::error;
57
58/// A [`Plugin`] that draws wireframes for 2D meshes.
59///
60/// Wireframes currently do not work when using webgl or webgpu.
61/// Supported rendering backends:
62/// - DX12
63/// - Vulkan
64/// - Metal
65///
66/// This is a native only feature.
67#[derive(Debug, Default)]
68pub struct Wireframe2dPlugin {
69    /// Debugging flags that can optionally be set when constructing the renderer.
70    pub debug_flags: RenderDebugFlags,
71}
72
73impl Wireframe2dPlugin {
74    /// Creates a new [`Wireframe2dPlugin`] with the given debug flags.
75    pub fn new(debug_flags: RenderDebugFlags) -> Self {
76        Self { debug_flags }
77    }
78}
79
80impl Plugin for Wireframe2dPlugin {
81    fn build(&self, app: &mut App) {
82        embedded_asset!(app, "wireframe2d.wgsl");
83
84        app.add_plugins((
85            BinnedRenderPhasePlugin::<Wireframe2dPhaseItem, Mesh2dPipeline>::new(self.debug_flags),
86            RenderAssetPlugin::<RenderWireframeMaterial>::default(),
87        ))
88        .init_asset::<Wireframe2dMaterial>()
89        .init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
90        .init_resource::<Wireframe2dConfig>()
91        .init_resource::<WireframeEntitiesNeedingSpecialization>()
92        .add_systems(Startup, setup_global_wireframe_material)
93        .add_systems(
94            Update,
95            (
96                global_color_changed.run_if(resource_changed::<Wireframe2dConfig>),
97                wireframe_color_changed,
98                // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global
99                // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed.
100                (apply_wireframe_material, apply_global_wireframe_material).chain(),
101            ),
102        )
103        .add_systems(
104            PostUpdate,
105            check_wireframe_entities_needing_specialization
106                .after(AssetEventSystems)
107                .run_if(resource_exists::<Wireframe2dConfig>),
108        );
109
110        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
111            return;
112        };
113
114        render_app
115            .init_resource::<WireframeEntitySpecializationTicks>()
116            .init_resource::<SpecializedWireframePipelineCache>()
117            .init_resource::<DrawFunctions<Wireframe2dPhaseItem>>()
118            .add_render_command::<Wireframe2dPhaseItem, DrawWireframe2d>()
119            .init_resource::<RenderWireframeInstances>()
120            .init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
121            .add_render_graph_node::<ViewNodeRunner<Wireframe2dNode>>(Core2d, Node2d::Wireframe)
122            .add_render_graph_edges(
123                Core2d,
124                (
125                    Node2d::EndMainPass,
126                    Node2d::Wireframe,
127                    Node2d::PostProcessing,
128                ),
129            )
130            .add_systems(
131                RenderStartup,
132                init_wireframe_2d_pipeline.after(init_mesh_2d_pipeline),
133            )
134            .add_systems(
135                ExtractSchedule,
136                (
137                    extract_wireframe_2d_camera,
138                    extract_wireframe_entities_needing_specialization,
139                    extract_wireframe_materials,
140                ),
141            )
142            .add_systems(
143                Render,
144                (
145                    specialize_wireframes
146                        .in_set(RenderSystems::PrepareMeshes)
147                        .after(prepare_assets::<RenderWireframeMaterial>)
148                        .after(prepare_assets::<RenderMesh>),
149                    queue_wireframes
150                        .in_set(RenderSystems::QueueMeshes)
151                        .after(prepare_assets::<RenderWireframeMaterial>),
152                ),
153            );
154    }
155}
156
157/// Enables wireframe rendering for any entity it is attached to.
158/// It will ignore the [`Wireframe2dConfig`] global setting.
159///
160/// This requires the [`Wireframe2dPlugin`] to be enabled.
161#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
162#[reflect(Component, Default, Debug, PartialEq)]
163pub struct Wireframe2d;
164
165pub struct Wireframe2dPhaseItem {
166    /// Determines which objects can be placed into a *batch set*.
167    ///
168    /// Objects in a single batch set can potentially be multi-drawn together,
169    /// if it's enabled and the current platform supports it.
170    pub batch_set_key: Wireframe2dBatchSetKey,
171    /// The key, which determines which can be batched.
172    pub bin_key: Wireframe2dBinKey,
173    /// An entity from which data will be fetched, including the mesh if
174    /// applicable.
175    pub representative_entity: (Entity, MainEntity),
176    /// The ranges of instances.
177    pub batch_range: Range<u32>,
178    /// An extra index, which is either a dynamic offset or an index in the
179    /// indirect parameters list.
180    pub extra_index: PhaseItemExtraIndex,
181}
182
183impl PhaseItem for Wireframe2dPhaseItem {
184    fn entity(&self) -> Entity {
185        self.representative_entity.0
186    }
187
188    fn main_entity(&self) -> MainEntity {
189        self.representative_entity.1
190    }
191
192    fn draw_function(&self) -> DrawFunctionId {
193        self.batch_set_key.draw_function
194    }
195
196    fn batch_range(&self) -> &Range<u32> {
197        &self.batch_range
198    }
199
200    fn batch_range_mut(&mut self) -> &mut Range<u32> {
201        &mut self.batch_range
202    }
203
204    fn extra_index(&self) -> PhaseItemExtraIndex {
205        self.extra_index.clone()
206    }
207
208    fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
209        (&mut self.batch_range, &mut self.extra_index)
210    }
211}
212
213impl CachedRenderPipelinePhaseItem for Wireframe2dPhaseItem {
214    fn cached_pipeline(&self) -> CachedRenderPipelineId {
215        self.batch_set_key.pipeline
216    }
217}
218
219impl BinnedPhaseItem for Wireframe2dPhaseItem {
220    type BinKey = Wireframe2dBinKey;
221    type BatchSetKey = Wireframe2dBatchSetKey;
222
223    fn new(
224        batch_set_key: Self::BatchSetKey,
225        bin_key: Self::BinKey,
226        representative_entity: (Entity, MainEntity),
227        batch_range: Range<u32>,
228        extra_index: PhaseItemExtraIndex,
229    ) -> Self {
230        Self {
231            batch_set_key,
232            bin_key,
233            representative_entity,
234            batch_range,
235            extra_index,
236        }
237    }
238}
239
240#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
241pub struct Wireframe2dBatchSetKey {
242    /// The identifier of the render pipeline.
243    pub pipeline: CachedRenderPipelineId,
244
245    /// The wireframe material asset ID.
246    pub asset_id: UntypedAssetId,
247
248    /// The function used to draw.
249    pub draw_function: DrawFunctionId,
250    /// The ID of the slab of GPU memory that contains vertex data.
251    ///
252    /// For non-mesh items, you can fill this with 0 if your items can be
253    /// multi-drawn, or with a unique value if they can't.
254    pub vertex_slab: SlabId,
255
256    /// The ID of the slab of GPU memory that contains index data, if present.
257    ///
258    /// For non-mesh items, you can safely fill this with `None`.
259    pub index_slab: Option<SlabId>,
260}
261
262impl PhaseItemBatchSetKey for Wireframe2dBatchSetKey {
263    fn indexed(&self) -> bool {
264        self.index_slab.is_some()
265    }
266}
267
268/// Data that must be identical in order to *batch* phase items together.
269///
270/// Note that a *batch set* (if multi-draw is in use) contains multiple batches.
271#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
272pub struct Wireframe2dBinKey {
273    /// The wireframe mesh asset ID.
274    pub asset_id: UntypedAssetId,
275}
276
277pub struct SetWireframe2dPushConstants;
278
279impl<P: PhaseItem> RenderCommand<P> for SetWireframe2dPushConstants {
280    type Param = (
281        SRes<RenderWireframeInstances>,
282        SRes<RenderAssets<RenderWireframeMaterial>>,
283    );
284    type ViewQuery = ();
285    type ItemQuery = ();
286
287    #[inline]
288    fn render<'w>(
289        item: &P,
290        _view: (),
291        _item_query: Option<()>,
292        (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>,
293        pass: &mut TrackedRenderPass<'w>,
294    ) -> RenderCommandResult {
295        let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else {
296            return RenderCommandResult::Failure("No wireframe material found for entity");
297        };
298        let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else {
299            return RenderCommandResult::Failure("No wireframe material found for entity");
300        };
301
302        pass.set_push_constants(
303            ShaderStages::FRAGMENT,
304            0,
305            bytemuck::bytes_of(&wireframe_material.color),
306        );
307        RenderCommandResult::Success
308    }
309}
310
311pub type DrawWireframe2d = (
312    SetItemPipeline,
313    SetMesh2dViewBindGroup<0>,
314    SetMesh2dBindGroup<1>,
315    SetWireframe2dPushConstants,
316    DrawMesh2d,
317);
318
319#[derive(Resource, Clone)]
320pub struct Wireframe2dPipeline {
321    mesh_pipeline: Mesh2dPipeline,
322    shader: Handle<Shader>,
323}
324
325pub fn init_wireframe_2d_pipeline(
326    mut commands: Commands,
327    mesh_2d_pipeline: Res<Mesh2dPipeline>,
328    asset_server: Res<AssetServer>,
329) {
330    commands.insert_resource(Wireframe2dPipeline {
331        mesh_pipeline: mesh_2d_pipeline.clone(),
332        shader: load_embedded_asset!(asset_server.as_ref(), "wireframe2d.wgsl"),
333    });
334}
335
336impl SpecializedMeshPipeline for Wireframe2dPipeline {
337    type Key = Mesh2dPipelineKey;
338
339    fn specialize(
340        &self,
341        key: Self::Key,
342        layout: &MeshVertexBufferLayoutRef,
343    ) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
344        let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
345        descriptor.label = Some("wireframe_2d_pipeline".into());
346        descriptor.push_constant_ranges.push(PushConstantRange {
347            stages: ShaderStages::FRAGMENT,
348            range: 0..16,
349        });
350        let fragment = descriptor.fragment.as_mut().unwrap();
351        fragment.shader = self.shader.clone();
352        descriptor.primitive.polygon_mode = PolygonMode::Line;
353        descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
354        Ok(descriptor)
355    }
356}
357
358#[derive(Default)]
359struct Wireframe2dNode;
360impl ViewNode for Wireframe2dNode {
361    type ViewQuery = (
362        &'static ExtractedCamera,
363        &'static ExtractedView,
364        &'static ViewTarget,
365        &'static ViewDepthTexture,
366    );
367
368    fn run<'w>(
369        &self,
370        graph: &mut RenderGraphContext,
371        render_context: &mut RenderContext<'w>,
372        (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>,
373        world: &'w World,
374    ) -> Result<(), NodeRunError> {
375        let Some(wireframe_phase) =
376            world.get_resource::<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>()
377        else {
378            return Ok(());
379        };
380
381        let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else {
382            return Ok(());
383        };
384
385        let diagnostics = render_context.diagnostic_recorder();
386
387        let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
388            label: Some("wireframe_2d"),
389            color_attachments: &[Some(target.get_color_attachment())],
390            depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
391            timestamp_writes: None,
392            occlusion_query_set: None,
393        });
394        let pass_span = diagnostics.pass_span(&mut render_pass, "wireframe_2d");
395
396        if let Some(viewport) = camera.viewport.as_ref() {
397            render_pass.set_camera_viewport(viewport);
398        }
399
400        if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) {
401            error!("Error encountered while rendering the stencil phase {err:?}");
402            return Err(NodeRunError::DrawError(err));
403        }
404
405        pass_span.end(&mut render_pass);
406
407        Ok(())
408    }
409}
410
411/// Sets the color of the [`Wireframe2d`] of the entity it is attached to.
412///
413/// If this component is present but there's no [`Wireframe2d`] component,
414/// it will still affect the color of the wireframe when [`Wireframe2dConfig::global`] is set to true.
415///
416/// This overrides the [`Wireframe2dConfig::default_color`].
417#[derive(Component, Debug, Clone, Default, Reflect)]
418#[reflect(Component, Default, Debug)]
419pub struct Wireframe2dColor {
420    pub color: Color,
421}
422
423#[derive(Component, Debug, Clone, Default)]
424pub struct ExtractedWireframeColor {
425    pub color: [f32; 4],
426}
427
428/// Disables wireframe rendering for any entity it is attached to.
429/// It will ignore the [`Wireframe2dConfig`] global setting.
430///
431/// This requires the [`Wireframe2dPlugin`] to be enabled.
432#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
433#[reflect(Component, Default, Debug, PartialEq)]
434pub struct NoWireframe2d;
435
436#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)]
437#[reflect(Resource, Debug, Default)]
438pub struct Wireframe2dConfig {
439    /// Whether to show wireframes for all meshes.
440    /// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component.
441    pub global: bool,
442    /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have
443    /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe2d`],
444    /// but no [`Wireframe2dColor`].
445    pub default_color: Color,
446}
447
448#[derive(Asset, Reflect, Clone, Debug, Default)]
449#[reflect(Clone, Default)]
450pub struct Wireframe2dMaterial {
451    pub color: Color,
452}
453
454pub struct RenderWireframeMaterial {
455    pub color: [f32; 4],
456}
457
458#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)]
459#[reflect(Component, Default, Clone, PartialEq)]
460pub struct Mesh2dWireframe(pub Handle<Wireframe2dMaterial>);
461
462impl AsAssetId for Mesh2dWireframe {
463    type Asset = Wireframe2dMaterial;
464
465    fn as_asset_id(&self) -> AssetId<Self::Asset> {
466        self.0.id()
467    }
468}
469
470impl RenderAsset for RenderWireframeMaterial {
471    type SourceAsset = Wireframe2dMaterial;
472    type Param = ();
473
474    fn prepare_asset(
475        source_asset: Self::SourceAsset,
476        _asset_id: AssetId<Self::SourceAsset>,
477        _param: &mut SystemParamItem<Self::Param>,
478        _previous_asset: Option<&Self>,
479    ) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
480        Ok(RenderWireframeMaterial {
481            color: source_asset.color.to_linear().to_f32_array(),
482        })
483    }
484}
485
486#[derive(Resource, Deref, DerefMut, Default)]
487pub struct RenderWireframeInstances(MainEntityHashMap<AssetId<Wireframe2dMaterial>>);
488
489#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)]
490pub struct WireframeEntitiesNeedingSpecialization {
491    #[deref]
492    pub entities: Vec<Entity>,
493}
494
495#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)]
496pub struct WireframeEntitySpecializationTicks {
497    pub entities: MainEntityHashMap<Tick>,
498}
499
500/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view.
501#[derive(Resource, Deref, DerefMut, Default)]
502pub struct SpecializedWireframePipelineCache {
503    // view entity -> view pipeline cache
504    #[deref]
505    map: HashMap<RetainedViewEntity, SpecializedWireframeViewPipelineCache>,
506}
507
508/// Stores the cached render pipeline ID for each entity in a single view, as
509/// well as the last time it was changed.
510#[derive(Deref, DerefMut, Default)]
511pub struct SpecializedWireframeViewPipelineCache {
512    // material entity -> (tick, pipeline_id)
513    #[deref]
514    map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
515}
516
517#[derive(Resource)]
518struct GlobalWireframeMaterial {
519    // This handle will be reused when the global config is enabled
520    handle: Handle<Wireframe2dMaterial>,
521}
522
523pub fn extract_wireframe_materials(
524    mut material_instances: ResMut<RenderWireframeInstances>,
525    changed_meshes_query: Extract<
526        Query<
527            (Entity, &ViewVisibility, &Mesh2dWireframe),
528            Or<(Changed<ViewVisibility>, Changed<Mesh2dWireframe>)>,
529        >,
530    >,
531    mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
532    mut removed_materials_query: Extract<RemovedComponents<Mesh2dWireframe>>,
533) {
534    for (entity, view_visibility, material) in &changed_meshes_query {
535        if view_visibility.get() {
536            material_instances.insert(entity.into(), material.id());
537        } else {
538            material_instances.remove(&MainEntity::from(entity));
539        }
540    }
541
542    for entity in removed_visibilities_query
543        .read()
544        .chain(removed_materials_query.read())
545    {
546        // Only queue a mesh for removal if we didn't pick it up above.
547        // It's possible that a necessary component was removed and re-added in
548        // the same frame.
549        if !changed_meshes_query.contains(entity) {
550            material_instances.remove(&MainEntity::from(entity));
551        }
552    }
553}
554
555fn setup_global_wireframe_material(
556    mut commands: Commands,
557    mut materials: ResMut<Assets<Wireframe2dMaterial>>,
558    config: Res<Wireframe2dConfig>,
559) {
560    // Create the handle used for the global material
561    commands.insert_resource(GlobalWireframeMaterial {
562        handle: materials.add(Wireframe2dMaterial {
563            color: config.default_color,
564        }),
565    });
566}
567
568/// Updates the wireframe material of all entities without a [`Wireframe2dColor`] or without a [`Wireframe2d`] component
569fn global_color_changed(
570    config: Res<Wireframe2dConfig>,
571    mut materials: ResMut<Assets<Wireframe2dMaterial>>,
572    global_material: Res<GlobalWireframeMaterial>,
573) {
574    if let Some(global_material) = materials.get_mut(&global_material.handle) {
575        global_material.color = config.default_color;
576    }
577}
578
579/// Updates the wireframe material when the color in [`Wireframe2dColor`] changes
580fn wireframe_color_changed(
581    mut materials: ResMut<Assets<Wireframe2dMaterial>>,
582    mut colors_changed: Query<
583        (&mut Mesh2dWireframe, &Wireframe2dColor),
584        (With<Wireframe2d>, Changed<Wireframe2dColor>),
585    >,
586) {
587    for (mut handle, wireframe_color) in &mut colors_changed {
588        handle.0 = materials.add(Wireframe2dMaterial {
589            color: wireframe_color.color,
590        });
591    }
592}
593
594/// Applies or remove the wireframe material to any mesh with a [`Wireframe2d`] component, and removes it
595/// for any mesh with a [`NoWireframe2d`] component.
596fn apply_wireframe_material(
597    mut commands: Commands,
598    mut materials: ResMut<Assets<Wireframe2dMaterial>>,
599    wireframes: Query<
600        (Entity, Option<&Wireframe2dColor>),
601        (With<Wireframe2d>, Without<Mesh2dWireframe>),
602    >,
603    no_wireframes: Query<Entity, (With<NoWireframe2d>, With<Mesh2dWireframe>)>,
604    mut removed_wireframes: RemovedComponents<Wireframe2d>,
605    global_material: Res<GlobalWireframeMaterial>,
606) {
607    for e in removed_wireframes.read().chain(no_wireframes.iter()) {
608        if let Ok(mut commands) = commands.get_entity(e) {
609            commands.remove::<Mesh2dWireframe>();
610        }
611    }
612
613    let mut material_to_spawn = vec![];
614    for (e, maybe_color) in &wireframes {
615        let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
616        material_to_spawn.push((e, Mesh2dWireframe(material)));
617    }
618    commands.try_insert_batch(material_to_spawn);
619}
620
621type WireframeFilter = (With<Mesh2d>, Without<Wireframe2d>, Without<NoWireframe2d>);
622
623/// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component.
624fn apply_global_wireframe_material(
625    mut commands: Commands,
626    config: Res<Wireframe2dConfig>,
627    meshes_without_material: Query<
628        (Entity, Option<&Wireframe2dColor>),
629        (WireframeFilter, Without<Mesh2dWireframe>),
630    >,
631    meshes_with_global_material: Query<Entity, (WireframeFilter, With<Mesh2dWireframe>)>,
632    global_material: Res<GlobalWireframeMaterial>,
633    mut materials: ResMut<Assets<Wireframe2dMaterial>>,
634) {
635    if config.global {
636        let mut material_to_spawn = vec![];
637        for (e, maybe_color) in &meshes_without_material {
638            let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
639            // We only add the material handle but not the Wireframe component
640            // This makes it easy to detect which mesh is using the global material and which ones are user specified
641            material_to_spawn.push((e, Mesh2dWireframe(material)));
642        }
643        commands.try_insert_batch(material_to_spawn);
644    } else {
645        for e in &meshes_with_global_material {
646            commands.entity(e).remove::<Mesh2dWireframe>();
647        }
648    }
649}
650
651/// Gets a handle to a wireframe material with a fallback on the default material
652fn get_wireframe_material(
653    maybe_color: Option<&Wireframe2dColor>,
654    wireframe_materials: &mut Assets<Wireframe2dMaterial>,
655    global_material: &GlobalWireframeMaterial,
656) -> Handle<Wireframe2dMaterial> {
657    if let Some(wireframe_color) = maybe_color {
658        wireframe_materials.add(Wireframe2dMaterial {
659            color: wireframe_color.color,
660        })
661    } else {
662        // If there's no color specified we can use the global material since it's already set to use the default_color
663        global_material.handle.clone()
664    }
665}
666
667fn extract_wireframe_2d_camera(
668    mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
669    cameras: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
670    mut live_entities: Local<HashSet<RetainedViewEntity>>,
671) {
672    live_entities.clear();
673    for (main_entity, camera) in &cameras {
674        if !camera.is_active {
675            continue;
676        }
677        let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
678        wireframe_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
679        live_entities.insert(retained_view_entity);
680    }
681
682    // Clear out all dead views.
683    wireframe_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
684}
685
686pub fn extract_wireframe_entities_needing_specialization(
687    entities_needing_specialization: Extract<Res<WireframeEntitiesNeedingSpecialization>>,
688    mut entity_specialization_ticks: ResMut<WireframeEntitySpecializationTicks>,
689    views: Query<&ExtractedView>,
690    mut specialized_wireframe_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
691    mut removed_meshes_query: Extract<RemovedComponents<Mesh2d>>,
692    ticks: SystemChangeTick,
693) {
694    for entity in entities_needing_specialization.iter() {
695        // Update the entity's specialization tick with this run's tick
696        entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
697    }
698
699    for entity in removed_meshes_query.read() {
700        for view in &views {
701            if let Some(specialized_wireframe_pipeline_cache) =
702                specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity)
703            {
704                specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity));
705            }
706        }
707    }
708}
709
710pub fn check_wireframe_entities_needing_specialization(
711    needs_specialization: Query<
712        Entity,
713        Or<(
714            Changed<Mesh2d>,
715            AssetChanged<Mesh2d>,
716            Changed<Mesh2dWireframe>,
717            AssetChanged<Mesh2dWireframe>,
718        )>,
719    >,
720    mut entities_needing_specialization: ResMut<WireframeEntitiesNeedingSpecialization>,
721) {
722    entities_needing_specialization.clear();
723    for entity in &needs_specialization {
724        entities_needing_specialization.push(entity);
725    }
726}
727
728pub fn specialize_wireframes(
729    render_meshes: Res<RenderAssets<RenderMesh>>,
730    render_mesh_instances: Res<RenderMesh2dInstances>,
731    render_wireframe_instances: Res<RenderWireframeInstances>,
732    wireframe_phases: Res<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
733    views: Query<(&ExtractedView, &RenderVisibleEntities)>,
734    view_key_cache: Res<ViewKeyCache>,
735    entity_specialization_ticks: Res<WireframeEntitySpecializationTicks>,
736    view_specialization_ticks: Res<ViewSpecializationTicks>,
737    mut specialized_material_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
738    mut pipelines: ResMut<SpecializedMeshPipelines<Wireframe2dPipeline>>,
739    pipeline: Res<Wireframe2dPipeline>,
740    pipeline_cache: Res<PipelineCache>,
741    ticks: SystemChangeTick,
742) {
743    // Record the retained IDs of all views so that we can expire old
744    // pipeline IDs.
745    let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
746
747    for (view, visible_entities) in &views {
748        all_views.insert(view.retained_view_entity);
749
750        if !wireframe_phases.contains_key(&view.retained_view_entity) {
751            continue;
752        }
753
754        let Some(view_key) = view_key_cache.get(&view.retained_view_entity.main_entity) else {
755            continue;
756        };
757
758        let view_tick = view_specialization_ticks
759            .get(&view.retained_view_entity.main_entity)
760            .unwrap();
761        let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
762            .entry(view.retained_view_entity)
763            .or_default();
764
765        for (_, visible_entity) in visible_entities.iter::<Mesh2d>() {
766            if !render_wireframe_instances.contains_key(visible_entity) {
767                continue;
768            };
769            let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
770                continue;
771            };
772            let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
773            let last_specialized_tick = view_specialized_material_pipeline_cache
774                .get(visible_entity)
775                .map(|(tick, _)| *tick);
776            let needs_specialization = last_specialized_tick.is_none_or(|tick| {
777                view_tick.is_newer_than(tick, ticks.this_run())
778                    || entity_tick.is_newer_than(tick, ticks.this_run())
779            });
780            if !needs_specialization {
781                continue;
782            }
783            let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
784                continue;
785            };
786
787            let mut mesh_key = *view_key;
788            mesh_key |= Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology());
789
790            let pipeline_id =
791                pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout);
792            let pipeline_id = match pipeline_id {
793                Ok(id) => id,
794                Err(err) => {
795                    error!("{}", err);
796                    continue;
797                }
798            };
799
800            view_specialized_material_pipeline_cache
801                .insert(*visible_entity, (ticks.this_run(), pipeline_id));
802        }
803    }
804
805    // Delete specialized pipelines belonging to views that have expired.
806    specialized_material_pipeline_cache
807        .retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
808}
809
810fn queue_wireframes(
811    custom_draw_functions: Res<DrawFunctions<Wireframe2dPhaseItem>>,
812    render_mesh_instances: Res<RenderMesh2dInstances>,
813    mesh_allocator: Res<MeshAllocator>,
814    specialized_wireframe_pipeline_cache: Res<SpecializedWireframePipelineCache>,
815    render_wireframe_instances: Res<RenderWireframeInstances>,
816    mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
817    mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
818) {
819    for (view, visible_entities) in &mut views {
820        let Some(wireframe_phase) = wireframe_2d_phases.get_mut(&view.retained_view_entity) else {
821            continue;
822        };
823        let draw_wireframe = custom_draw_functions.read().id::<DrawWireframe2d>();
824
825        let Some(view_specialized_material_pipeline_cache) =
826            specialized_wireframe_pipeline_cache.get(&view.retained_view_entity)
827        else {
828            continue;
829        };
830
831        for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() {
832            let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else {
833                continue;
834            };
835            let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
836                .get(visible_entity)
837                .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
838            else {
839                continue;
840            };
841
842            // Skip the entity if it's cached in a bin and up to date.
843            if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) {
844                continue;
845            }
846            let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
847                continue;
848            };
849            let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
850            let bin_key = Wireframe2dBinKey {
851                asset_id: mesh_instance.mesh_asset_id.untyped(),
852            };
853            let batch_set_key = Wireframe2dBatchSetKey {
854                pipeline: pipeline_id,
855                asset_id: wireframe_instance.untyped(),
856                draw_function: draw_wireframe,
857                vertex_slab: vertex_slab.unwrap_or_default(),
858                index_slab,
859            };
860            wireframe_phase.add(
861                batch_set_key,
862                bin_key,
863                (*render_entity, *visible_entity),
864                InputUniformIndex::default(),
865                if mesh_instance.automatic_batching {
866                    BinnedRenderPhaseType::BatchableMesh
867                } else {
868                    BinnedRenderPhaseType::UnbatchableMesh
869                },
870                current_change_tick,
871            );
872        }
873    }
874}