Skip to main content

bevy_pbr/
wireframe.rs

1use crate::{
2    collect_meshes_for_gpu_building,
3    render::{PreprocessBindGroups, PreprocessPipelines},
4    set_mesh_motion_vector_flags, DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineSystems,
5    RenderLightmaps, RenderMeshInstanceFlags, RenderMeshInstances, SetMeshBindGroup,
6    SetMeshViewBindGroup, SetMeshViewBindingArrayBindGroup, ViewKeyCache,
7};
8use bevy_app::{App, Plugin, PostUpdate, Startup};
9use bevy_asset::{
10    embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp,
11    AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId,
12};
13use bevy_camera::{visibility::ViewVisibility, Camera, Camera3d};
14use bevy_color::{Color, ColorToComponents};
15use bevy_core_pipeline::schedule::{Core3d, Core3dSystems};
16use bevy_derive::{Deref, DerefMut};
17use bevy_ecs::{
18    prelude::*,
19    query::ROQueryItem,
20    system::{lifetimeless::SRes, SystemParamItem},
21};
22use bevy_mesh::{Mesh, Mesh3d, MeshVertexBufferLayoutRef};
23use bevy_platform::{
24    collections::{HashMap, HashSet},
25    hash::FixedHasher,
26};
27use bevy_reflect::{std_traits::ReflectDefault, Reflect};
28use bevy_render::{
29    batching::gpu_preprocessing::{
30        GpuPreprocessingMode, GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers,
31        IndirectParametersNonIndexed,
32    },
33    camera::{
34        DirtySpecializationSystems, DirtyWireframeSpecializations, ExtractedCamera, PendingQueues,
35    },
36    extract_resource::ExtractResource,
37    mesh::{
38        allocator::{MeshAllocator, MeshAllocatorSettings, MeshSlabs},
39        RenderMesh, RenderMeshBufferInfo,
40    },
41    render_asset::{
42        prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
43    },
44    render_phase::{
45        AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType,
46        CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem,
47        PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
48        SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases,
49    },
50    render_resource::{binding_types::*, *},
51    renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery},
52    sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet},
53    view::{
54        ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities,
55        RetainedViewEntity, ViewDepthStencilTexture, ViewTarget,
56    },
57    Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderDebugFlags,
58    RenderStartup, RenderSystems,
59};
60use bevy_shader::Shader;
61use bytemuck::{Pod, Zeroable};
62use core::{any::TypeId, hash::Hash, mem::size_of, ops::Range};
63use tracing::{error, warn};
64
65/// A [`Plugin`] that draws wireframes.
66///
67/// Wireframes currently do not work when using webgl or webgpu.
68/// Supported rendering backends:
69/// - DX12
70/// - Vulkan
71/// - Metal
72///
73/// This is a native only feature.
74#[derive(Debug, Default)]
75pub struct WireframePlugin {
76    /// Debugging flags that can optionally be set when constructing the renderer.
77    pub debug_flags: RenderDebugFlags,
78}
79
80impl WireframePlugin {
81    /// Creates a new [`WireframePlugin`] with the given debug flags.
82    pub fn new(debug_flags: RenderDebugFlags) -> Self {
83        Self { debug_flags }
84    }
85}
86
87impl Plugin for WireframePlugin {
88    fn build(&self, app: &mut App) {
89        embedded_asset!(app, "render/wireframe.wesl");
90
91        app.add_plugins((
92            BinnedRenderPhasePlugin::<Wireframe3d, MeshPipeline>::new(self.debug_flags),
93            RenderAssetPlugin::<RenderWireframeMaterial>::default(),
94        ))
95        .init_asset::<WireframeMaterial>()
96        .init_resource::<WireframeEntitiesNeedingSpecialization>()
97        .init_resource::<SpecializedMeshPipelines<Wireframe3dPipeline>>()
98        .init_resource::<WireframeConfig>()
99        .init_resource::<WireframeEntitiesNeedingSpecialization>()
100        .register_type::<WireframeLineWidth>()
101        .register_type::<WireframeTopology>()
102        .register_type::<WireframeXray>()
103        .add_systems(Startup, setup_global_wireframe_material)
104        .add_systems(
105            PostUpdate,
106            (
107                wireframe_config_changed.run_if(resource_changed::<WireframeConfig>),
108                wireframe_color_changed,
109                wireframe_line_width_changed,
110                wireframe_topology_changed,
111                wireframe_xray_changed,
112                // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global
113                // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed.
114                (apply_wireframe_material, apply_global_wireframe_material).chain(),
115            ),
116        )
117        .add_systems(
118            PostUpdate,
119            check_wireframe_entities_needing_specialization
120                .after(AssetEventSystems)
121                .run_if(resource_exists::<WireframeConfig>),
122        );
123    }
124
125    fn finish(&self, app: &mut App) {
126        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
127            return;
128        };
129
130        let required_features = WgpuFeatures::POLYGON_MODE_LINE | WgpuFeatures::IMMEDIATES;
131        let render_device = render_app.world().resource::<RenderDevice>();
132        if !render_device.features().contains(required_features) {
133            warn!(
134                "WireframePlugin not loaded. GPU lacks support for required features: {:?}.",
135                required_features
136            );
137            return;
138        }
139
140        // we need storage for vertex pulling in the wide wireframe path
141        render_app
142            .world_mut()
143            .resource_mut::<MeshAllocatorSettings>()
144            .extra_buffer_usages |= BufferUsages::STORAGE;
145
146        render_app
147            .init_gpu_resource::<SpecializedWireframePipelineCache>()
148            .init_resource::<DrawFunctions<Wireframe3d>>()
149            .add_render_command::<Wireframe3d, DrawWireframe3dThin>()
150            .add_render_command::<Wireframe3d, DrawWireframe3dWide>()
151            .init_gpu_resource::<RenderWireframeInstances>()
152            .init_gpu_resource::<WireframeWideBindGroups>()
153            .init_gpu_resource::<SpecializedMeshPipelines<Wireframe3dPipeline>>()
154            .init_gpu_resource::<PendingWireframeQueues>()
155            .add_systems(
156                RenderStartup,
157                init_wireframe_3d_pipeline.after(MeshPipelineSystems),
158            )
159            .add_systems(
160                Core3d,
161                wireframe_3d
162                    .after(Core3dSystems::MainPass)
163                    .before(Core3dSystems::EarlyPostProcess),
164            )
165            .add_systems(
166                ExtractSchedule,
167                (
168                    extract_wireframe_3d_camera,
169                    extract_wireframe_entities_needing_specialization
170                        .in_set(DirtySpecializationSystems::CheckForChanges),
171                    extract_wireframe_entities_that_need_specializations_removed
172                        .in_set(DirtySpecializationSystems::CheckForRemovals),
173                    extract_wireframe_materials,
174                ),
175            )
176            .add_systems(
177                Render,
178                (
179                    specialize_wireframes
180                        .in_set(RenderSystems::Specialize)
181                        .after(prepare_assets::<RenderWireframeMaterial>)
182                        .after(prepare_assets::<RenderMesh>)
183                        .after(collect_meshes_for_gpu_building)
184                        .after(set_mesh_motion_vector_flags),
185                    prepare_wireframe_wide_bind_groups
186                        .in_set(RenderSystems::PrepareBindGroups)
187                        .after(prepare_assets::<RenderWireframeMaterial>)
188                        .after(prepare_assets::<RenderMesh>),
189                    queue_wireframes
190                        .in_set(RenderSystems::QueueMeshes)
191                        .after(prepare_assets::<RenderWireframeMaterial>),
192                ),
193            );
194    }
195}
196
197/// Enables wireframe rendering for any entity it is attached to.
198/// It will ignore the [`WireframeConfig`] global setting.
199///
200/// This requires the [`WireframePlugin`] to be enabled.
201#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
202#[reflect(Component, Default, Debug, PartialEq)]
203pub struct Wireframe;
204
205pub struct Wireframe3d {
206    /// Determines which objects can be placed into a *batch set*.
207    ///
208    /// Objects in a single batch set can potentially be multi-drawn together,
209    /// if it's enabled and the current platform supports it.
210    pub batch_set_key: Wireframe3dBatchSetKey,
211    /// The key, which determines which can be batched.
212    pub bin_key: Wireframe3dBinKey,
213    /// An entity from which data will be fetched, including the mesh if
214    /// applicable.
215    pub representative_entity: (Entity, MainEntity),
216    /// The ranges of instances.
217    pub batch_range: Range<u32>,
218    /// An extra index, which is either a dynamic offset or an index in the
219    /// indirect parameters list.
220    pub extra_index: PhaseItemExtraIndex,
221}
222
223impl PhaseItem for Wireframe3d {
224    fn entity(&self) -> Entity {
225        self.representative_entity.0
226    }
227
228    fn main_entity(&self) -> MainEntity {
229        self.representative_entity.1
230    }
231
232    fn draw_function(&self) -> DrawFunctionId {
233        self.batch_set_key.draw_function
234    }
235
236    fn batch_range(&self) -> &Range<u32> {
237        &self.batch_range
238    }
239
240    fn batch_range_mut(&mut self) -> &mut Range<u32> {
241        &mut self.batch_range
242    }
243
244    fn extra_index(&self) -> PhaseItemExtraIndex {
245        self.extra_index.clone()
246    }
247
248    fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
249        (&mut self.batch_range, &mut self.extra_index)
250    }
251}
252
253impl CachedRenderPipelinePhaseItem for Wireframe3d {
254    fn cached_pipeline(&self) -> CachedRenderPipelineId {
255        self.batch_set_key.pipeline
256    }
257}
258
259impl BinnedPhaseItem for Wireframe3d {
260    type BinKey = Wireframe3dBinKey;
261    type BatchSetKey = Wireframe3dBatchSetKey;
262
263    fn new(
264        batch_set_key: Self::BatchSetKey,
265        bin_key: Self::BinKey,
266        representative_entity: (Entity, MainEntity),
267        batch_range: Range<u32>,
268        extra_index: PhaseItemExtraIndex,
269    ) -> Self {
270        Self {
271            batch_set_key,
272            bin_key,
273            representative_entity,
274            batch_range,
275            extra_index,
276        }
277    }
278}
279
280#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
281pub struct Wireframe3dBatchSetKey {
282    /// The identifier of the render pipeline.
283    pub pipeline: CachedRenderPipelineId,
284
285    /// The wireframe material asset ID.
286    pub asset_id: UntypedAssetId,
287
288    /// The function used to draw.
289    pub draw_function: DrawFunctionId,
290
291    /// The IDs of the slabs of GPU memory in the mesh allocator that contain
292    /// the mesh data.
293    ///
294    /// For non-mesh items, you can fill the [`MeshSlabs::vertex_slab_id`] with
295    /// 0 if your items can be multi-drawn, or with a unique value if they
296    /// can't.
297    pub slabs: MeshSlabs,
298
299    /// For the wide wireframe path, the mesh asset ID ensures all draws in one
300    /// batch set share the same vertex-pull params uniform. `None` for the thin
301    /// path, which doesn't need per-mesh bind groups.
302    pub mesh_asset_id: Option<UntypedAssetId>,
303}
304
305impl PhaseItemBatchSetKey for Wireframe3dBatchSetKey {
306    fn indexed(&self) -> bool {
307        self.slabs.index_slab_id.is_some()
308    }
309}
310
311/// Data that must be identical in order to *batch* phase items together.
312///
313/// Note that a *batch set* (if multi-draw is in use) contains multiple batches.
314#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
315pub struct Wireframe3dBinKey {
316    /// The wireframe mesh asset ID.
317    pub asset_id: UntypedAssetId,
318}
319
320pub struct SetWireframe3dThinImmediates;
321
322impl<P: PhaseItem> RenderCommand<P> for SetWireframe3dThinImmediates {
323    type Param = (
324        SRes<RenderWireframeInstances>,
325        SRes<RenderAssets<RenderWireframeMaterial>>,
326    );
327    type ViewQuery = ();
328    type ItemQuery = ();
329
330    #[inline]
331    fn render<'w>(
332        item: &P,
333        _view: (),
334        _item_query: Option<()>,
335        (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>,
336        pass: &mut TrackedRenderPass<'w>,
337    ) -> RenderCommandResult {
338        let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else {
339            return RenderCommandResult::Failure("No wireframe material found for entity");
340        };
341        let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else {
342            return RenderCommandResult::Failure("No wireframe material found for entity");
343        };
344
345        pass.set_immediates(0, bytemuck::bytes_of(&wireframe_material.color));
346        RenderCommandResult::Success
347    }
348}
349
350pub struct SetWireframe3dWideImmediates;
351
352#[derive(Clone, Copy, Pod, Zeroable)]
353#[repr(C)]
354struct WireframeWideImmediates {
355    color: [f32; 4],
356    line_width: f32,
357    smoothing: f32,
358    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
359    _padding: [f32; 2],
360}
361
362impl<P: PhaseItem> RenderCommand<P> for SetWireframe3dWideImmediates {
363    type Param = (
364        SRes<RenderWireframeInstances>,
365        SRes<RenderAssets<RenderWireframeMaterial>>,
366    );
367    type ViewQuery = ();
368    type ItemQuery = ();
369
370    #[inline]
371    fn render<'w>(
372        item: &P,
373        _view: (),
374        _item_query: Option<()>,
375        (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>,
376        pass: &mut TrackedRenderPass<'w>,
377    ) -> RenderCommandResult {
378        let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else {
379            return RenderCommandResult::Failure("No wireframe material found for entity");
380        };
381        let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else {
382            return RenderCommandResult::Failure("No wireframe material found for entity");
383        };
384
385        let push = WireframeWideImmediates {
386            color: wireframe_material.color,
387            line_width: wireframe_material.line_width,
388            smoothing: if wireframe_material.line_width <= 1.0 {
389                0.5
390            } else {
391                1.0
392            },
393            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
394            _padding: [0.0; 2],
395        };
396        pass.set_immediates(0, bytemuck::bytes_of(&push));
397        RenderCommandResult::Success
398    }
399}
400
401#[derive(Clone, Copy, ShaderType, Pod, Zeroable)]
402#[repr(C)]
403pub struct WireframeVertexPullParams {
404    pub index_offset: u32,
405    pub vertex_stride_u32s: u32,
406    pub position_offset_u32s: u32,
407}
408
409#[derive(Resource, Default)]
410pub struct WireframeWideBindGroups {
411    pub params: DynamicUniformBuffer<WireframeVertexPullParams>,
412    pub bind_groups: HashMap<AssetId<Mesh>, (BindGroup, u32)>,
413}
414
415pub fn prepare_wireframe_wide_bind_groups(
416    render_mesh_instances: Res<RenderMeshInstances>,
417    render_meshes: Res<RenderAssets<RenderMesh>>,
418    render_wireframe_instances: Res<RenderWireframeInstances>,
419    render_wireframe_assets: Res<RenderAssets<RenderWireframeMaterial>>,
420    mesh_allocator: Res<MeshAllocator>,
421    pipeline: Res<Wireframe3dPipeline>,
422    render_device: Res<RenderDevice>,
423    render_queue: Res<RenderQueue>,
424    mut wide_bind_groups: ResMut<WireframeWideBindGroups>,
425) {
426    wide_bind_groups.bind_groups.clear();
427
428    struct MeshInfo {
429        mesh_id: AssetId<Mesh>,
430        params: WireframeVertexPullParams,
431        vertex_buffer: Buffer,
432        index_buffer: Buffer,
433    }
434
435    let mut infos: Vec<MeshInfo> = Vec::new();
436    let mut seen: HashSet<AssetId<Mesh>, FixedHasher> = HashSet::default();
437
438    for (entity, wireframe_asset_id) in render_wireframe_instances.iter() {
439        let Some(material) = render_wireframe_assets.get(*wireframe_asset_id) else {
440            continue;
441        };
442        if material.line_width <= 1.0 && material.topology != WireframeTopology::Quads {
443            continue;
444        }
445
446        let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*entity) else {
447            continue;
448        };
449        let mesh_id = mesh_instance.mesh_asset_id();
450        if !seen.insert(mesh_id) {
451            continue;
452        }
453
454        let Some(mesh) = render_meshes.get(mesh_id) else {
455            continue;
456        };
457        let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh_id) else {
458            continue;
459        };
460        let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh_id) else {
461            continue;
462        };
463
464        let vertex_stride_bytes = mesh.layout.0.layout().array_stride as u32;
465        let position_offset_bytes = mesh
466            .layout
467            .0
468            .layout()
469            .attributes
470            .first()
471            .map(|a| a.offset as u32)
472            .unwrap_or(0);
473
474        infos.push(MeshInfo {
475            mesh_id,
476            params: WireframeVertexPullParams {
477                index_offset: index_slice.range.start,
478                vertex_stride_u32s: vertex_stride_bytes / 4,
479                position_offset_u32s: position_offset_bytes / 4,
480            },
481            vertex_buffer: vertex_slice.buffer.clone(),
482            index_buffer: index_slice.buffer.clone(),
483        });
484    }
485
486    if infos.is_empty() {
487        return;
488    }
489
490    let Some(mut writer) =
491        wide_bind_groups
492            .params
493            .get_writer(infos.len(), &render_device, &render_queue)
494    else {
495        return;
496    };
497
498    let offsets: Vec<u32> = infos
499        .iter()
500        .map(|info| writer.write(&info.params))
501        .collect();
502    drop(writer);
503
504    let WireframeWideBindGroups {
505        ref params,
506        ref mut bind_groups,
507    } = *wide_bind_groups;
508    let Some(params_binding) = params.binding() else {
509        return;
510    };
511
512    for (i, info) in infos.iter().enumerate() {
513        let bind_group = render_device.create_bind_group(
514            "wireframe_wide_bind_group",
515            &pipeline.wide_bind_group_layout,
516            &BindGroupEntries::sequential((
517                info.vertex_buffer.as_entire_buffer_binding(),
518                info.index_buffer.as_entire_buffer_binding(),
519                params_binding.clone(),
520            )),
521        );
522        bind_groups.insert(info.mesh_id, (bind_group, offsets[i]));
523    }
524}
525
526pub struct SetWireframe3dWideBindGroup;
527
528impl<P: PhaseItem> RenderCommand<P> for SetWireframe3dWideBindGroup {
529    type Param = (SRes<RenderMeshInstances>, SRes<WireframeWideBindGroups>);
530    type ViewQuery = ();
531    type ItemQuery = ();
532
533    #[inline]
534    fn render<'w>(
535        item: &P,
536        _view: (),
537        _item_query: Option<()>,
538        (render_mesh_instances, wide_bind_groups): SystemParamItem<'w, '_, Self::Param>,
539        pass: &mut TrackedRenderPass<'w>,
540    ) -> RenderCommandResult {
541        let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(item.main_entity())
542        else {
543            return RenderCommandResult::Skip;
544        };
545        let Some((bind_group, dynamic_offset)) = wide_bind_groups
546            .into_inner()
547            .bind_groups
548            .get(&mesh_instance.mesh_asset_id())
549        else {
550            return RenderCommandResult::Skip;
551        };
552
553        pass.set_bind_group(3, bind_group, &[*dynamic_offset]);
554        RenderCommandResult::Success
555    }
556}
557
558pub struct DrawWireframeMeshPulled;
559
560impl<P: PhaseItem> RenderCommand<P> for DrawWireframeMeshPulled {
561    type Param = (
562        SRes<RenderMeshInstances>,
563        SRes<RenderAssets<RenderMesh>>,
564        SRes<MeshAllocator>,
565        SRes<IndirectParametersBuffers>,
566        SRes<PipelineCache>,
567        Option<SRes<PreprocessPipelines>>,
568        SRes<GpuPreprocessingSupport>,
569    );
570    type ViewQuery = Has<PreprocessBindGroups>;
571    type ItemQuery = ();
572
573    #[inline]
574    fn render<'w>(
575        item: &P,
576        has_preprocess_bind_group: ROQueryItem<Self::ViewQuery>,
577        _item_query: Option<()>,
578        (
579            render_mesh_instances,
580            render_meshes,
581            mesh_allocator,
582            indirect_parameters_buffers,
583            pipeline_cache,
584            preprocess_pipelines,
585            preprocessing_support,
586        ): SystemParamItem<'w, '_, Self::Param>,
587        pass: &mut TrackedRenderPass<'w>,
588    ) -> RenderCommandResult {
589        if let Some(preprocess_pipelines) = preprocess_pipelines
590            && (!has_preprocess_bind_group
591                || !preprocess_pipelines
592                    .pipelines_are_loaded(&pipeline_cache, &preprocessing_support))
593        {
594            return RenderCommandResult::Skip;
595        }
596
597        let render_mesh_instances = render_mesh_instances.into_inner();
598        let render_meshes = render_meshes.into_inner();
599        let mesh_allocator = mesh_allocator.into_inner();
600        let indirect_parameters_buffers = indirect_parameters_buffers.into_inner();
601
602        let Some(mesh_asset_id) = render_mesh_instances.mesh_asset_id(item.main_entity()) else {
603            return RenderCommandResult::Skip;
604        };
605        let Some(gpu_mesh) = render_meshes.get(mesh_asset_id) else {
606            return RenderCommandResult::Skip;
607        };
608
609        let index_count = match &gpu_mesh.buffer_info {
610            RenderMeshBufferInfo::Indexed { count, .. } => *count,
611            RenderMeshBufferInfo::NonIndexed => gpu_mesh.vertex_count,
612        };
613
614        match item.extra_index() {
615            PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => {
616                // direct draw: use vertex range starting at first_vertex_index so
617                // the shader can recover draw_id via mesh[instance_index].first_vertex_index.
618                let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh_asset_id) else {
619                    return RenderCommandResult::Skip;
620                };
621                let first_vertex = vertex_slice.range.start;
622                pass.draw(
623                    first_vertex..(first_vertex + index_count),
624                    item.batch_range().clone(),
625                );
626            }
627            PhaseItemExtraIndex::IndirectParametersIndex {
628                range: indirect_parameters_range,
629                batch_set_index,
630            } => {
631                // no indexes - the preprocessor sets base_vertex = first_vertex_index.
632                let Some(phase_indirect) = indirect_parameters_buffers.get(&TypeId::of::<P>())
633                else {
634                    warn!("Wireframe wide: indirect parameters buffer missing for phase");
635                    return RenderCommandResult::Skip;
636                };
637                let (Some(indirect_buffer), Some(batch_sets_buffer)) = (
638                    phase_indirect.non_indexed.data_buffer(),
639                    phase_indirect.non_indexed.batch_sets_buffer(),
640                ) else {
641                    warn!("Wireframe wide: non-indexed indirect parameters buffer not ready");
642                    return RenderCommandResult::Skip;
643                };
644
645                let indirect_parameters_offset = indirect_parameters_range.start as u64
646                    * size_of::<IndirectParametersNonIndexed>() as u64;
647                let indirect_parameters_count =
648                    indirect_parameters_range.end - indirect_parameters_range.start;
649
650                match batch_set_index {
651                    Some(batch_set_index) => {
652                        let count_offset =
653                            u32::from(batch_set_index) * (size_of::<IndirectBatchSet>() as u32);
654                        pass.multi_draw_indirect_count(
655                            indirect_buffer,
656                            indirect_parameters_offset,
657                            batch_sets_buffer,
658                            count_offset as u64,
659                            indirect_parameters_count,
660                        );
661                    }
662                    None => {
663                        pass.multi_draw_indirect(
664                            indirect_buffer,
665                            indirect_parameters_offset,
666                            indirect_parameters_count,
667                        );
668                    }
669                }
670            }
671        }
672        RenderCommandResult::Success
673    }
674}
675
676/// Draw wireframes with `PolygonMode::Line`, i.e. the fast path.
677pub type DrawWireframe3dThin = (
678    SetItemPipeline,
679    SetMeshViewBindGroup<0>,
680    SetMeshViewBindingArrayBindGroup<1>,
681    SetMeshBindGroup<2>,
682    SetWireframe3dThinImmediates,
683    DrawMesh,
684);
685
686/// Draw wireframes using vertex pulling for wide lines or quad topology.
687pub type DrawWireframe3dWide = (
688    SetItemPipeline,
689    SetMeshViewBindGroup<0>,
690    SetMeshViewBindingArrayBindGroup<1>,
691    SetMeshBindGroup<2>,
692    SetWireframe3dWideBindGroup,
693    SetWireframe3dWideImmediates,
694    DrawWireframeMeshPulled,
695);
696
697#[derive(Resource, Clone)]
698pub struct Wireframe3dPipeline {
699    mesh_pipeline: MeshPipeline,
700    shader: Handle<Shader>,
701    pub wide_bind_group_layout: BindGroupLayout,
702    pub wide_bind_group_layout_descriptor: BindGroupLayoutDescriptor,
703}
704
705pub fn init_wireframe_3d_pipeline(
706    mut commands: Commands,
707    mesh_pipeline: Res<MeshPipeline>,
708    asset_server: Res<AssetServer>,
709    render_device: Res<RenderDevice>,
710) {
711    let wide_bgl_entries = BindGroupLayoutEntries::sequential(
712        ShaderStages::VERTEX,
713        (
714            storage_buffer_read_only::<u32>(false), // vertex data
715            storage_buffer_read_only::<u32>(false), // index data
716            uniform_buffer::<WireframeVertexPullParams>(true),
717        ),
718    );
719
720    let wide_bind_group_layout = render_device
721        .create_bind_group_layout("wireframe_wide_bind_group_layout", &wide_bgl_entries);
722
723    let wide_bind_group_layout_descriptor =
724        BindGroupLayoutDescriptor::new("wireframe_wide_bind_group_layout", &wide_bgl_entries);
725
726    commands.insert_resource(Wireframe3dPipeline {
727        mesh_pipeline: mesh_pipeline.clone(),
728        shader: load_embedded_asset!(asset_server.as_ref(), "render/wireframe.wesl"),
729        wide_bind_group_layout,
730        wide_bind_group_layout_descriptor,
731    });
732}
733
734#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
735pub struct WireframePipelineKey {
736    pub mesh_key: MeshPipelineKey,
737    pub wide: bool,
738    pub quads: bool,
739    pub line_mode: bool,
740    pub xray_mode: bool,
741}
742
743impl SpecializedMeshPipeline for Wireframe3dPipeline {
744    type Key = WireframePipelineKey;
745
746    fn specialize(
747        &self,
748        key: Self::Key,
749        layout: &MeshVertexBufferLayoutRef,
750    ) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
751        let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?;
752
753        if key.xray_mode {
754            descriptor.primitive.cull_mode = None;
755            let depth_stencil = descriptor.depth_stencil.as_mut().unwrap();
756            // An x-ray wireframe must not occlude or be occluded by anything
757            depth_stencil.depth_compare = Some(CompareFunction::Always);
758            depth_stencil.depth_write_enabled = Some(false);
759        }
760
761        if descriptor.primitive.topology.is_triangles() {
762            descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
763        }
764
765        if key.wide {
766            descriptor.label = Some("wireframe_3d_wide_pipeline".into());
767
768            descriptor.vertex.shader = self.shader.clone();
769            descriptor.vertex.shader_defs.push("WIREFRAME_WIDE".into());
770            if key.quads {
771                descriptor.vertex.shader_defs.push("WIREFRAME_QUADS".into());
772            }
773            descriptor.vertex.entry_point = Some("vertex".into());
774            descriptor.vertex.buffers = vec![]; // vertex pulling from storage
775
776            let fragment = descriptor.fragment.as_mut().unwrap();
777            fragment.shader = self.shader.clone();
778            fragment.shader_defs.push("WIREFRAME_WIDE".into());
779            fragment.entry_point = Some("fragment".into());
780
781            for state in fragment.targets.iter_mut().flatten() {
782                state.blend = Some(BlendState::ALPHA_BLENDING);
783            }
784
785            descriptor.primitive.polygon_mode = if key.line_mode {
786                PolygonMode::Line
787            } else {
788                PolygonMode::Fill
789            };
790            descriptor.immediate_size = 32; // color(16) + line_width(4) + smoothing(4) + pad(8)
791
792            descriptor
793                .layout
794                .push(self.wide_bind_group_layout_descriptor.clone());
795        } else {
796            descriptor.label = Some("wireframe_3d_pipeline".into());
797            descriptor.immediate_size = 16;
798            let fragment = descriptor.fragment.as_mut().unwrap();
799            fragment.shader = self.shader.clone();
800            descriptor.primitive.polygon_mode = PolygonMode::Line;
801        }
802
803        Ok(descriptor)
804    }
805}
806
807pub fn wireframe_3d(
808    world: &World,
809    view: ViewQuery<(
810        &ExtractedCamera,
811        &ExtractedView,
812        &ViewTarget,
813        &ViewDepthStencilTexture,
814    )>,
815    wireframe_phases: Res<ViewBinnedRenderPhases<Wireframe3d>>,
816    mut ctx: RenderContext,
817) {
818    let view_entity = view.entity();
819
820    let (camera, extracted_view, target, depth) = view.into_inner();
821
822    let Some(wireframe_phase) = wireframe_phases.get(&extracted_view.retained_view_entity) else {
823        return;
824    };
825
826    if wireframe_phase.is_empty() {
827        return;
828    }
829
830    let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
831        label: Some("wireframe_3d"),
832        color_attachments: &[Some(target.get_color_attachment())],
833        depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
834        timestamp_writes: None,
835        occlusion_query_set: None,
836        multiview_mask: None,
837    });
838
839    if let Some(viewport) = camera.viewport.as_ref() {
840        render_pass.set_camera_viewport(viewport);
841    }
842
843    if let Err(err) = wireframe_phase.render(&mut render_pass, world, view_entity) {
844        error!("Error encountered while rendering the wireframe phase {err:?}");
845    }
846}
847
848/// Sets the color of the [`Wireframe`] of the entity it is attached to.
849///
850/// If this component is present but there's no [`Wireframe`] component,
851/// it will still affect the color of the wireframe when [`WireframeConfig::global`] is set to true.
852///
853/// This overrides the [`WireframeConfig::default_color`].
854#[derive(Component, Debug, Clone, Default, Reflect)]
855#[reflect(Component, Default, Debug)]
856pub struct WireframeColor {
857    pub color: Color,
858}
859
860/// Sets the line width (in screen-space pixels) of the wireframe.
861///
862/// Overrides [`WireframeConfig::default_line_width`].
863#[derive(Component, Debug, Clone, Reflect)]
864#[reflect(Component, Default, Debug)]
865pub struct WireframeLineWidth {
866    pub width: f32,
867}
868
869impl Default for WireframeLineWidth {
870    fn default() -> Self {
871        Self { width: 1.0 }
872    }
873}
874
875/// Disables wireframe rendering for any entity it is attached to.
876/// It will ignore the [`WireframeConfig`] global setting.
877///
878/// This requires the [`WireframePlugin`] to be enabled.
879#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
880#[reflect(Component, Default, Debug, PartialEq)]
881pub struct NoWireframe;
882
883/// Controls whether wireframe edges follow triangle or quad topology.
884#[derive(Component, Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Reflect)]
885#[reflect(Component, Default, Debug)]
886pub enum WireframeTopology {
887    #[default]
888    Triangles,
889    /// Does a best-effort attempt to detect quads from a triangle mesh. No guarantee of accuracy is made,
890    /// that is, there may be both false positives and false negatives in the rendered output.
891    Quads,
892}
893
894/// Controls whether wireframe edges render as an x-ray overlay.
895///
896/// Overrides [`WireframeConfig::xray_mode`].
897#[derive(Component, Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Reflect)]
898#[reflect(Component, Default, Debug)]
899pub struct WireframeXray(pub bool);
900
901#[derive(Resource, Debug, Clone, ExtractResource, Reflect)]
902#[reflect(Resource, Debug, Default)]
903#[extract_app(RenderApp)]
904pub struct WireframeConfig {
905    /// Whether to show wireframes for all meshes.
906    /// Can be overridden for individual meshes by adding a [`Wireframe`] or [`NoWireframe`] component.
907    pub global: bool,
908    /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe`] component attached to it will have
909    /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe`],
910    /// but no [`WireframeColor`].
911    pub default_color: Color,
912    /// Default line width in screen-space pixels.
913    pub default_line_width: f32,
914    /// Default edge topology.
915    pub default_topology: WireframeTopology,
916    /// Whether all wireframe pipelines render as x-ray overlays. When enabled,
917    /// both sides are rendered without depth testing.
918    pub xray_mode: bool,
919}
920
921impl Default for WireframeConfig {
922    fn default() -> Self {
923        Self {
924            global: false,
925            default_color: Color::default(),
926            default_line_width: 1.0,
927            default_topology: WireframeTopology::default(),
928            xray_mode: false,
929        }
930    }
931}
932
933#[derive(Asset, Reflect, Clone, Debug)]
934#[reflect(Clone, Default)]
935pub struct WireframeMaterial {
936    pub color: Color,
937    pub line_width: f32,
938    pub topology: WireframeTopology,
939    pub xray_mode: bool,
940}
941
942impl Default for WireframeMaterial {
943    fn default() -> Self {
944        Self {
945            color: Color::default(),
946            line_width: 1.0,
947            topology: WireframeTopology::default(),
948            xray_mode: false,
949        }
950    }
951}
952
953pub struct RenderWireframeMaterial {
954    pub color: [f32; 4],
955    pub line_width: f32,
956    pub topology: WireframeTopology,
957    pub xray_mode: bool,
958}
959
960#[derive(
961    Component, FromTemplate, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq,
962)]
963#[reflect(Component, Default, Clone, PartialEq)]
964pub struct Mesh3dWireframe(pub Handle<WireframeMaterial>);
965
966impl AsAssetId for Mesh3dWireframe {
967    type Asset = WireframeMaterial;
968
969    fn as_asset_id(&self) -> AssetId<Self::Asset> {
970        self.0.id()
971    }
972}
973
974impl RenderAsset for RenderWireframeMaterial {
975    type SourceAsset = WireframeMaterial;
976    type Param = ();
977
978    fn prepare_asset(
979        source_asset: Self::SourceAsset,
980        _asset_id: AssetId<Self::SourceAsset>,
981        _param: &mut SystemParamItem<Self::Param>,
982        _previous_asset: Option<&Self>,
983    ) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
984        Ok(RenderWireframeMaterial {
985            color: source_asset.color.to_linear().to_f32_array(),
986            line_width: source_asset.line_width,
987            topology: source_asset.topology,
988            xray_mode: source_asset.xray_mode,
989        })
990    }
991}
992
993#[derive(Resource, Deref, DerefMut, Default)]
994pub struct RenderWireframeInstances(MainEntityHashMap<AssetId<WireframeMaterial>>);
995
996/// Temporarily stores entities that were determined to either need their
997/// specialized pipelines for wireframes updated or to have their specialized
998/// pipelines for wireframes removed.
999#[derive(Clone, Resource, Debug, Default)]
1000pub struct WireframeEntitiesNeedingSpecialization {
1001    /// Entities that need to have their pipelines updated.
1002    pub changed: Vec<Entity>,
1003    /// Entities that need to have their pipelines removed.
1004    pub removed: Vec<Entity>,
1005}
1006
1007#[derive(Resource, Default)]
1008pub struct SpecializedWireframePipelineCache {
1009    views: HashMap<RetainedViewEntity, SpecializedWireframeViewPipelineCache>,
1010    wide: HashMap<
1011        (MeshPipelineKey, MeshVertexBufferLayoutRef, bool, bool, bool),
1012        CachedRenderPipelineId,
1013    >,
1014}
1015
1016#[derive(Deref, DerefMut, Default)]
1017pub struct SpecializedWireframeViewPipelineCache {
1018    // material entity -> (tick, pipeline_id)
1019    #[deref]
1020    map: MainEntityHashMap<CachedRenderPipelineId>,
1021}
1022
1023#[derive(Resource)]
1024struct GlobalWireframeMaterial {
1025    // This handle will be reused when the global config is enabled
1026    handle: Handle<WireframeMaterial>,
1027}
1028
1029pub fn extract_wireframe_materials(
1030    mut material_instances: ResMut<RenderWireframeInstances>,
1031    changed_meshes_query: Extract<
1032        Query<
1033            (Entity, &ViewVisibility, &Mesh3dWireframe),
1034            Or<(Changed<ViewVisibility>, Changed<Mesh3dWireframe>)>,
1035        >,
1036    >,
1037    mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
1038    mut removed_materials_query: Extract<RemovedComponents<Mesh3dWireframe>>,
1039) {
1040    for (entity, view_visibility, material) in &changed_meshes_query {
1041        if view_visibility.get() {
1042            material_instances.insert(entity.into(), material.id());
1043        } else {
1044            material_instances.remove(&MainEntity::from(entity));
1045        }
1046    }
1047
1048    for entity in removed_visibilities_query
1049        .read()
1050        .chain(removed_materials_query.read())
1051    {
1052        // Only queue a mesh for removal if we didn't pick it up above.
1053        // It's possible that a necessary component was removed and re-added in
1054        // the same frame.
1055        if !changed_meshes_query.contains(entity) {
1056            material_instances.remove(&MainEntity::from(entity));
1057        }
1058    }
1059}
1060
1061fn setup_global_wireframe_material(
1062    mut commands: Commands,
1063    mut materials: ResMut<Assets<WireframeMaterial>>,
1064    config: Res<WireframeConfig>,
1065) {
1066    commands.insert_resource(GlobalWireframeMaterial {
1067        handle: materials.add(WireframeMaterial {
1068            color: config.default_color,
1069            line_width: config.default_line_width,
1070            topology: config.default_topology,
1071            xray_mode: config.xray_mode,
1072        }),
1073    });
1074}
1075
1076fn wireframe_config_changed(
1077    config: Res<WireframeConfig>,
1078    mut materials: ResMut<Assets<WireframeMaterial>>,
1079    global_material: Res<GlobalWireframeMaterial>,
1080    mut per_entity_wireframes: Query<
1081        (
1082            &mut Mesh3dWireframe,
1083            Option<&WireframeColor>,
1084            Option<&WireframeLineWidth>,
1085            Option<&WireframeTopology>,
1086            Option<&WireframeXray>,
1087        ),
1088        With<Wireframe>,
1089    >,
1090) {
1091    if let Some(mut mat) = materials.get_mut(&global_material.handle) {
1092        mat.color = config.default_color;
1093        mat.line_width = config.default_line_width;
1094        mat.topology = config.default_topology;
1095        mat.xray_mode = config.xray_mode;
1096    }
1097
1098    for (mut handle, maybe_color, maybe_width, maybe_topology, maybe_xray) in
1099        &mut per_entity_wireframes
1100    {
1101        if handle.0 == global_material.handle {
1102            continue;
1103        }
1104        handle.0 = materials.add(WireframeMaterial {
1105            color: maybe_color.map(|c| c.color).unwrap_or(config.default_color),
1106            line_width: maybe_width
1107                .map(|w| w.width)
1108                .unwrap_or(config.default_line_width),
1109            topology: maybe_topology.copied().unwrap_or(config.default_topology),
1110            xray_mode: maybe_xray.map(|xray| xray.0).unwrap_or(config.xray_mode),
1111        });
1112    }
1113}
1114
1115fn wireframe_color_changed(
1116    mut materials: ResMut<Assets<WireframeMaterial>>,
1117    mut colors_changed: Query<
1118        (
1119            &mut Mesh3dWireframe,
1120            &WireframeColor,
1121            Option<&WireframeLineWidth>,
1122            Option<&WireframeTopology>,
1123            Option<&WireframeXray>,
1124        ),
1125        (With<Wireframe>, Changed<WireframeColor>),
1126    >,
1127    config: Res<WireframeConfig>,
1128) {
1129    for (mut handle, wireframe_color, maybe_width, maybe_topology, maybe_xray) in
1130        &mut colors_changed
1131    {
1132        handle.0 = materials.add(WireframeMaterial {
1133            color: wireframe_color.color,
1134            line_width: maybe_width
1135                .map(|w| w.width)
1136                .unwrap_or(config.default_line_width),
1137            topology: maybe_topology.copied().unwrap_or(config.default_topology),
1138            xray_mode: maybe_xray.map(|xray| xray.0).unwrap_or(config.xray_mode),
1139        });
1140    }
1141}
1142
1143fn wireframe_line_width_changed(
1144    mut materials: ResMut<Assets<WireframeMaterial>>,
1145    mut widths_changed: Query<
1146        (
1147            &mut Mesh3dWireframe,
1148            &WireframeLineWidth,
1149            Option<&WireframeColor>,
1150            Option<&WireframeTopology>,
1151            Option<&WireframeXray>,
1152        ),
1153        (With<Wireframe>, Changed<WireframeLineWidth>),
1154    >,
1155    config: Res<WireframeConfig>,
1156) {
1157    for (mut handle, wireframe_width, maybe_color, maybe_topology, maybe_xray) in
1158        &mut widths_changed
1159    {
1160        handle.0 = materials.add(WireframeMaterial {
1161            color: maybe_color.map(|c| c.color).unwrap_or(config.default_color),
1162            line_width: wireframe_width.width,
1163            topology: maybe_topology.copied().unwrap_or(config.default_topology),
1164            xray_mode: maybe_xray.map(|xray| xray.0).unwrap_or(config.xray_mode),
1165        });
1166    }
1167}
1168
1169fn wireframe_topology_changed(
1170    mut materials: ResMut<Assets<WireframeMaterial>>,
1171    mut topology_changed: Query<
1172        (
1173            &mut Mesh3dWireframe,
1174            &WireframeTopology,
1175            Option<&WireframeColor>,
1176            Option<&WireframeLineWidth>,
1177            Option<&WireframeXray>,
1178        ),
1179        (With<Wireframe>, Changed<WireframeTopology>),
1180    >,
1181    config: Res<WireframeConfig>,
1182) {
1183    for (mut handle, topology, maybe_color, maybe_width, maybe_xray) in &mut topology_changed {
1184        handle.0 = materials.add(WireframeMaterial {
1185            color: maybe_color.map(|c| c.color).unwrap_or(config.default_color),
1186            line_width: maybe_width
1187                .map(|w| w.width)
1188                .unwrap_or(config.default_line_width),
1189            topology: *topology,
1190            xray_mode: maybe_xray.map(|xray| xray.0).unwrap_or(config.xray_mode),
1191        });
1192    }
1193}
1194
1195fn wireframe_xray_changed(
1196    mut materials: ResMut<Assets<WireframeMaterial>>,
1197    mut xray_changed: Query<
1198        (
1199            &mut Mesh3dWireframe,
1200            &WireframeXray,
1201            Option<&WireframeColor>,
1202            Option<&WireframeLineWidth>,
1203            Option<&WireframeTopology>,
1204        ),
1205        (With<Wireframe>, Changed<WireframeXray>),
1206    >,
1207    config: Res<WireframeConfig>,
1208) {
1209    for (mut handle, xray, maybe_color, maybe_width, maybe_topology) in &mut xray_changed {
1210        handle.0 = materials.add(WireframeMaterial {
1211            color: maybe_color.map(|c| c.color).unwrap_or(config.default_color),
1212            line_width: maybe_width
1213                .map(|w| w.width)
1214                .unwrap_or(config.default_line_width),
1215            topology: maybe_topology.copied().unwrap_or(config.default_topology),
1216            xray_mode: xray.0,
1217        });
1218    }
1219}
1220
1221/// Applies or remove the wireframe material to any mesh with a [`Wireframe`] component, and removes it
1222/// for any mesh with a [`NoWireframe`] component.
1223fn apply_wireframe_material(
1224    mut commands: Commands,
1225    mut materials: ResMut<Assets<WireframeMaterial>>,
1226    wireframes: Query<
1227        (
1228            Entity,
1229            Option<&WireframeColor>,
1230            Option<&WireframeLineWidth>,
1231            Option<&WireframeTopology>,
1232            Option<&WireframeXray>,
1233        ),
1234        (With<Wireframe>, Without<Mesh3dWireframe>),
1235    >,
1236    no_wireframes: Query<Entity, (With<NoWireframe>, With<Mesh3dWireframe>)>,
1237    mut removed_wireframes: RemovedComponents<Wireframe>,
1238    global_material: Res<GlobalWireframeMaterial>,
1239    config: Res<WireframeConfig>,
1240) {
1241    for e in removed_wireframes.read().chain(no_wireframes.iter()) {
1242        if let Ok(mut commands) = commands.get_entity(e) {
1243            commands.remove::<Mesh3dWireframe>();
1244        }
1245    }
1246
1247    let mut material_to_spawn = vec![];
1248    for (e, maybe_color, maybe_width, maybe_topology, maybe_xray) in &wireframes {
1249        let material = get_wireframe_material(
1250            maybe_color,
1251            maybe_width,
1252            maybe_topology,
1253            maybe_xray,
1254            &mut materials,
1255            &global_material,
1256            &config,
1257        );
1258        material_to_spawn.push((e, Mesh3dWireframe(material)));
1259    }
1260    commands.try_insert_batch(material_to_spawn);
1261}
1262
1263type WireframeFilter = (With<Mesh3d>, Without<Wireframe>, Without<NoWireframe>);
1264
1265/// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component.
1266fn apply_global_wireframe_material(
1267    mut commands: Commands,
1268    config: Res<WireframeConfig>,
1269    meshes_without_material: Query<
1270        (
1271            Entity,
1272            Option<&WireframeColor>,
1273            Option<&WireframeLineWidth>,
1274            Option<&WireframeTopology>,
1275            Option<&WireframeXray>,
1276        ),
1277        (WireframeFilter, Without<Mesh3dWireframe>),
1278    >,
1279    meshes_with_global_material: Query<Entity, (WireframeFilter, With<Mesh3dWireframe>)>,
1280    global_material: Res<GlobalWireframeMaterial>,
1281    mut materials: ResMut<Assets<WireframeMaterial>>,
1282) {
1283    if config.global {
1284        let mut material_to_spawn = vec![];
1285        for (e, maybe_color, maybe_width, maybe_topology, maybe_xray) in &meshes_without_material {
1286            let material = get_wireframe_material(
1287                maybe_color,
1288                maybe_width,
1289                maybe_topology,
1290                maybe_xray,
1291                &mut materials,
1292                &global_material,
1293                &config,
1294            );
1295            // We only add the material handle but not the Wireframe component
1296            // This makes it easy to detect which mesh is using the global material and which ones are user specified
1297            material_to_spawn.push((e, Mesh3dWireframe(material)));
1298        }
1299        commands.try_insert_batch(material_to_spawn);
1300    } else {
1301        for e in &meshes_with_global_material {
1302            commands.entity(e).remove::<Mesh3dWireframe>();
1303        }
1304    }
1305}
1306
1307/// Gets a handle to a wireframe material with a fallback on the default material
1308fn get_wireframe_material(
1309    maybe_color: Option<&WireframeColor>,
1310    maybe_width: Option<&WireframeLineWidth>,
1311    maybe_topology: Option<&WireframeTopology>,
1312    maybe_xray: Option<&WireframeXray>,
1313    wireframe_materials: &mut Assets<WireframeMaterial>,
1314    global_material: &GlobalWireframeMaterial,
1315    config: &WireframeConfig,
1316) -> Handle<WireframeMaterial> {
1317    if maybe_color.is_some()
1318        || maybe_width.is_some()
1319        || maybe_topology.is_some()
1320        || maybe_xray.is_some()
1321    {
1322        wireframe_materials.add(WireframeMaterial {
1323            color: maybe_color.map(|c| c.color).unwrap_or(config.default_color),
1324            line_width: maybe_width
1325                .map(|w| w.width)
1326                .unwrap_or(config.default_line_width),
1327            topology: maybe_topology.copied().unwrap_or(config.default_topology),
1328            xray_mode: maybe_xray.map(|xray| xray.0).unwrap_or(config.xray_mode),
1329        })
1330    } else {
1331        // If there's no color specified we can use the global material since it's already set to use the default_color
1332        global_material.handle.clone()
1333    }
1334}
1335
1336fn extract_wireframe_3d_camera(
1337    mut wireframe_3d_phases: ResMut<ViewBinnedRenderPhases<Wireframe3d>>,
1338    cameras: Extract<Query<(Entity, &Camera, Has<NoIndirectDrawing>), With<Camera3d>>>,
1339    mut live_entities: Local<HashSet<RetainedViewEntity>>,
1340    gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
1341) {
1342    live_entities.clear();
1343    for (main_entity, camera, no_indirect_drawing) in &cameras {
1344        if !camera.is_active {
1345            continue;
1346        }
1347        let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing {
1348            GpuPreprocessingMode::Culling
1349        } else {
1350            GpuPreprocessingMode::PreprocessingOnly
1351        });
1352
1353        let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
1354        wireframe_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
1355        live_entities.insert(retained_view_entity);
1356    }
1357
1358    // Clear out all dead views.
1359    wireframe_3d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
1360}
1361
1362pub fn extract_wireframe_entities_needing_specialization(
1363    entities_needing_specialization: Extract<Res<WireframeEntitiesNeedingSpecialization>>,
1364    mut dirty_wireframe_specializations: ResMut<DirtyWireframeSpecializations>,
1365) {
1366    // Drain the list of entities needing specialization from the main world
1367    // into the render-world `DirtySpecializations` table.
1368    for entity in entities_needing_specialization.changed.iter() {
1369        dirty_wireframe_specializations
1370            .changed_renderables
1371            .insert(MainEntity::from(*entity));
1372    }
1373}
1374
1375/// A system that adds entities that were judged to need their wireframe
1376/// specializations removed to the appropriate table in
1377/// [`DirtyWireframeSpecializations`].
1378pub fn extract_wireframe_entities_that_need_specializations_removed(
1379    entities_needing_specialization: Extract<Res<WireframeEntitiesNeedingSpecialization>>,
1380    mut dirty_wireframe_specializations: ResMut<DirtyWireframeSpecializations>,
1381) {
1382    for entity in entities_needing_specialization.removed.iter() {
1383        dirty_wireframe_specializations
1384            .removed_renderables
1385            .insert(MainEntity::from(*entity));
1386    }
1387}
1388
1389/// Finds 3D wireframe entities that have changed in such a way as to
1390/// potentially require specialization and adds them to the
1391/// [`WireframeEntitiesNeedingSpecialization`] list.
1392pub fn check_wireframe_entities_needing_specialization(
1393    needs_specialization: Query<
1394        Entity,
1395        Or<(
1396            Changed<Mesh3d>,
1397            AssetChanged<Mesh3d>,
1398            Changed<Mesh3dWireframe>,
1399            AssetChanged<Mesh3dWireframe>,
1400            Changed<WireframeLineWidth>,
1401            Changed<WireframeTopology>,
1402            Changed<WireframeXray>,
1403        )>,
1404    >,
1405    wireframe_entities: Query<Entity, With<Mesh3dWireframe>>,
1406    config: Res<WireframeConfig>,
1407    mut entities_needing_specialization: ResMut<WireframeEntitiesNeedingSpecialization>,
1408    mut removed_mesh_3d_components: RemovedComponents<Mesh3d>,
1409    mut removed_mesh_3d_wireframe_components: RemovedComponents<Mesh3dWireframe>,
1410) {
1411    entities_needing_specialization.changed.clear();
1412    entities_needing_specialization.removed.clear();
1413
1414    // Gather all entities that need their specializations regenerated.
1415    for entity in &needs_specialization {
1416        entities_needing_specialization.changed.push(entity);
1417    }
1418
1419    if config.is_changed() {
1420        entities_needing_specialization
1421            .changed
1422            .extend(wireframe_entities.iter());
1423    }
1424
1425    // All entities that removed their `Mesh3d` or `Mesh3dWireframe` components
1426    // need to have their specializations removed as well.
1427    //
1428    // It's possible that `Mesh3d` was removed and re-added in the same frame,
1429    // but we don't have to handle that situation specially here, because
1430    // `specialize_wireframes` processes specialization removals before
1431    // additions. So, if the pipeline specialization gets spuriously removed,
1432    // it'll just be immediately re-added again, which is harmless.
1433    for entity in removed_mesh_3d_components
1434        .read()
1435        .chain(removed_mesh_3d_wireframe_components.read())
1436    {
1437        entities_needing_specialization.removed.push(entity);
1438    }
1439}
1440
1441#[derive(Default, Deref, DerefMut, Resource)]
1442pub struct PendingWireframeQueues(pub PendingQueues);
1443
1444pub fn specialize_wireframes(
1445    render_meshes: Res<RenderAssets<RenderMesh>>,
1446    render_mesh_instances: Res<RenderMeshInstances>,
1447    render_wireframe_instances: Res<RenderWireframeInstances>,
1448    render_wireframe_assets: Res<RenderAssets<RenderWireframeMaterial>>,
1449    render_visibility_ranges: Res<RenderVisibilityRanges>,
1450    wireframe_phases: Res<ViewBinnedRenderPhases<Wireframe3d>>,
1451    views: Query<(&ExtractedView, &RenderVisibleEntities)>,
1452    view_key_cache: Res<ViewKeyCache>,
1453    dirty_wireframe_specializations: Res<DirtyWireframeSpecializations>,
1454    mut specialized_material_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
1455    mut pipelines: ResMut<SpecializedMeshPipelines<Wireframe3dPipeline>>,
1456    mut pending_wireframe_queues: ResMut<PendingWireframeQueues>,
1457    pipeline: Res<Wireframe3dPipeline>,
1458    pipeline_cache: Res<PipelineCache>,
1459    render_lightmaps: Res<RenderLightmaps>,
1460) {
1461    let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
1462
1463    let SpecializedWireframePipelineCache {
1464        views: ref mut views_pipeline_cache,
1465        wide: ref mut wide_pipeline_cache,
1466    } = *specialized_material_pipeline_cache;
1467
1468    for (view, visible_entities) in &views {
1469        all_views.insert(view.retained_view_entity);
1470
1471        if !wireframe_phases.contains_key(&view.retained_view_entity) {
1472            continue;
1473        }
1474
1475        let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else {
1476            continue;
1477        };
1478
1479        let view_specialized_material_pipeline_cache = views_pipeline_cache
1480            .entry(view.retained_view_entity)
1481            .or_default();
1482
1483        let Some(render_visible_mesh_entities) = visible_entities.get::<Mesh3d>() else {
1484            continue;
1485        };
1486
1487        // Initialize the pending queues.
1488        let view_pending_wireframe_queues =
1489            pending_wireframe_queues.prepare_for_new_frame(view.retained_view_entity);
1490
1491        // Remove cached pipeline IDs corresponding to entities that
1492        // either have been removed or need to be respecialized.
1493        if dirty_wireframe_specializations
1494            .must_wipe_specializations_for_view(view.retained_view_entity)
1495        {
1496            view_specialized_material_pipeline_cache.clear();
1497        } else {
1498            for &renderable_entity in dirty_wireframe_specializations.iter_to_despecialize() {
1499                view_specialized_material_pipeline_cache.remove(&renderable_entity);
1500            }
1501        }
1502
1503        // Now process all wireframe meshes that need to be re-specialized.
1504        for (render_entity, visible_entity) in dirty_wireframe_specializations.iter_to_specialize(
1505            view.retained_view_entity,
1506            render_visible_mesh_entities,
1507            &view_pending_wireframe_queues.prev_frame,
1508        ) {
1509            if view_specialized_material_pipeline_cache.contains_key(visible_entity) {
1510                continue;
1511            }
1512
1513            if !render_wireframe_instances.contains_key(visible_entity) {
1514                continue;
1515            };
1516            let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
1517            else {
1518                // We couldn't fetch the mesh, probably because it hasn't loaded
1519                // yet. Add the entity to the list of pending wireframes and
1520                // bail.
1521                view_pending_wireframe_queues
1522                    .current_frame
1523                    .insert((*render_entity, *visible_entity));
1524                continue;
1525            };
1526            let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else {
1527                continue;
1528            };
1529
1530            let mut mesh_key = *view_key;
1531            mesh_key |= MeshPipelineKey::from_primitive_topology_and_strip_index(
1532                mesh.primitive_topology(),
1533                mesh.index_format(),
1534            );
1535
1536            if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) {
1537                mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER;
1538            }
1539
1540            if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
1541                // If the previous frame have skins or morph targets, note that.
1542                if mesh_instance
1543                    .flags()
1544                    .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN)
1545                {
1546                    mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN;
1547                }
1548                if mesh_instance
1549                    .flags()
1550                    .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH)
1551                {
1552                    mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH;
1553                }
1554            }
1555
1556            // Even though we don't use the lightmap in the wireframe, the
1557            // `SetMeshBindGroup` render command will bind the data for it. So
1558            // we need to include the appropriate flag in the mesh pipeline key
1559            // to ensure that the necessary bind group layout entries are
1560            // present.
1561            if render_lightmaps
1562                .render_lightmaps
1563                .contains_key(visible_entity)
1564            {
1565                mesh_key |= MeshPipelineKey::LIGHTMAPPED;
1566            }
1567
1568            let mat = render_wireframe_instances
1569                .get(visible_entity)
1570                .and_then(|asset_id| render_wireframe_assets.get(*asset_id));
1571            let quads = mat
1572                .map(|m| m.topology == WireframeTopology::Quads)
1573                .unwrap_or(false);
1574            let thick = mat.map(|m| m.line_width > 1.0).unwrap_or(false);
1575            let xray_mode = mat.map(|m| m.xray_mode).unwrap_or(false);
1576            let wide = thick || quads;
1577            let line_mode = wide && !thick;
1578
1579            let pipeline_id = if wide {
1580                let cache_key = (mesh_key, mesh.layout.clone(), quads, line_mode, xray_mode);
1581                *wide_pipeline_cache.entry(cache_key).or_insert_with(|| {
1582                    let wireframe_key = WireframePipelineKey {
1583                        mesh_key,
1584                        wide: true,
1585                        quads,
1586                        line_mode,
1587                        xray_mode,
1588                    };
1589                    match pipeline.specialize(wireframe_key, &mesh.layout) {
1590                        Ok(descriptor) => pipeline_cache.queue_render_pipeline(descriptor),
1591                        Err(err) => {
1592                            error!("{}", err);
1593                            CachedRenderPipelineId::INVALID
1594                        }
1595                    }
1596                })
1597            } else {
1598                let wireframe_key = WireframePipelineKey {
1599                    mesh_key,
1600                    wide: false,
1601                    quads: false,
1602                    line_mode: false,
1603                    xray_mode,
1604                };
1605                match pipelines.specialize(&pipeline_cache, &pipeline, wireframe_key, &mesh.layout)
1606                {
1607                    Ok(id) => id,
1608                    Err(err) => {
1609                        error!("{}", err);
1610                        continue;
1611                    }
1612                }
1613            };
1614
1615            view_specialized_material_pipeline_cache.insert(*visible_entity, pipeline_id);
1616        }
1617    }
1618
1619    pending_wireframe_queues.expire_stale_views(&all_views);
1620
1621    // Delete specialized pipelines belonging to views that have expired.
1622    views_pipeline_cache.retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
1623}
1624
1625fn queue_wireframes(
1626    custom_draw_functions: Res<DrawFunctions<Wireframe3d>>,
1627    render_mesh_instances: Res<RenderMeshInstances>,
1628    gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
1629    mesh_allocator: Res<MeshAllocator>,
1630    specialized_wireframe_pipeline_cache: Res<SpecializedWireframePipelineCache>,
1631    render_wireframe_instances: Res<RenderWireframeInstances>,
1632    dirty_wireframe_specializations: Res<DirtyWireframeSpecializations>,
1633    render_wireframe_assets: Res<RenderAssets<RenderWireframeMaterial>>,
1634    mut wireframe_3d_phases: ResMut<ViewBinnedRenderPhases<Wireframe3d>>,
1635    mut pending_wireframe_queues: ResMut<PendingWireframeQueues>,
1636    mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
1637    mut mesh_instances_queued_this_iteration_scratch_space: Local<MainEntityHashSet>,
1638) {
1639    for (view, visible_entities) in &mut views {
1640        let Some(wireframe_phase) = wireframe_3d_phases.get_mut(&view.retained_view_entity) else {
1641            continue;
1642        };
1643        let draw_functions = custom_draw_functions.read();
1644        let draw_thin = draw_functions.id::<DrawWireframe3dThin>();
1645        let draw_wide = draw_functions.id::<DrawWireframe3dWide>();
1646
1647        let Some(view_specialized_material_pipeline_cache) = specialized_wireframe_pipeline_cache
1648            .views
1649            .get(&view.retained_view_entity)
1650        else {
1651            continue;
1652        };
1653
1654        let Some(render_mesh_visible_entities) = visible_entities.get::<Mesh3d>() else {
1655            continue;
1656        };
1657
1658        let view_pending_wireframe_queues = pending_wireframe_queues
1659            .get_mut(&view.retained_view_entity)
1660            .expect(
1661                "View pending wireframe queues should have been created in `specialize_wireframes`",
1662            );
1663
1664        // First, remove meshes that need to be respecialized, and those that were removed, from the bins.
1665        for &main_entity in dirty_wireframe_specializations
1666            .iter_to_dequeue(view.retained_view_entity, render_mesh_visible_entities)
1667        {
1668            wireframe_phase.remove(main_entity);
1669        }
1670
1671        // Now iterate through all newly-visible entities and those needing respecialization.
1672        for (render_entity, visible_entity) in dirty_wireframe_specializations.iter_to_queue(
1673            view.retained_view_entity,
1674            render_mesh_visible_entities,
1675            &view_pending_wireframe_queues.prev_frame,
1676            &mut mesh_instances_queued_this_iteration_scratch_space,
1677        ) {
1678            let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else {
1679                continue;
1680            };
1681            let Some(pipeline_id) = view_specialized_material_pipeline_cache
1682                .get(visible_entity)
1683                .copied()
1684            else {
1685                continue;
1686            };
1687
1688            let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
1689            else {
1690                // We couldn't fetch the mesh, probably because it hasn't loaded
1691                // yet. Add the entity to the list of pending wireframes and
1692                // bail.
1693                view_pending_wireframe_queues
1694                    .current_frame
1695                    .insert((*render_entity, *visible_entity));
1696                continue;
1697            };
1698
1699            let is_wide = render_wireframe_assets
1700                .get(*wireframe_instance)
1701                .map(|mat| mat.line_width > 1.0 || mat.topology == WireframeTopology::Quads)
1702                .unwrap_or(false);
1703            let draw_function = if is_wide { draw_wide } else { draw_thin };
1704
1705            let Some(MeshSlabs {
1706                vertex_slab_id: vertex_slab,
1707                index_slab_id: index_slab,
1708                metadata_slab_id: metadata_slab,
1709                morph_target_slab_id: morph_target_slab,
1710            }) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id())
1711            else {
1712                continue;
1713            };
1714            let bin_key = Wireframe3dBinKey {
1715                asset_id: mesh_instance.mesh_asset_id().untyped(),
1716            };
1717            let batch_set_key = Wireframe3dBatchSetKey {
1718                pipeline: pipeline_id,
1719                asset_id: wireframe_instance.untyped(),
1720                draw_function,
1721                slabs: MeshSlabs {
1722                    vertex_slab_id: vertex_slab,
1723                    morph_target_slab_id: morph_target_slab,
1724                    metadata_slab_id: metadata_slab,
1725                    // wide wireframes use non-indexed draws (vertex pulling
1726                    // from storage), so set index_slab to None to make the
1727                    // preprocessor emit IndirectParametersNonIndexed instead of
1728                    // IndirectParametersIndexed.
1729                    index_slab_id: if is_wide { None } else { index_slab },
1730                },
1731                mesh_asset_id: if is_wide {
1732                    Some(mesh_instance.mesh_asset_id().untyped())
1733                } else {
1734                    None
1735                },
1736            };
1737            wireframe_phase.add(
1738                batch_set_key,
1739                bin_key,
1740                (*render_entity, *visible_entity),
1741                mesh_instance.current_uniform_index,
1742                BinnedRenderPhaseType::mesh(
1743                    mesh_instance.should_batch(),
1744                    &gpu_preprocessing_support,
1745                ),
1746            );
1747        }
1748    }
1749}