bevy_pbr/
wireframe.rs

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