bevy_pbr/
wireframe.rs

1use crate::{Material, MaterialPipeline, MaterialPipelineKey, MaterialPlugin, MeshMaterial3d};
2use bevy_app::{Plugin, Startup, Update};
3use bevy_asset::{load_internal_asset, Asset, Assets, Handle};
4use bevy_color::{Color, LinearRgba};
5use bevy_ecs::prelude::*;
6use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
7use bevy_render::{
8    extract_resource::ExtractResource,
9    mesh::{Mesh3d, MeshVertexBufferLayoutRef},
10    prelude::*,
11    render_resource::*,
12};
13
14pub const WIREFRAME_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(192598014480025766);
15
16/// A [`Plugin`] that draws wireframes.
17///
18/// Wireframes currently do not work when using webgl or webgpu.
19/// Supported rendering backends:
20/// - DX12
21/// - Vulkan
22/// - Metal
23///
24/// This is a native only feature.
25#[derive(Debug, Default)]
26pub struct WireframePlugin;
27impl Plugin for WireframePlugin {
28    fn build(&self, app: &mut bevy_app::App) {
29        load_internal_asset!(
30            app,
31            WIREFRAME_SHADER_HANDLE,
32            "render/wireframe.wgsl",
33            Shader::from_wgsl
34        );
35
36        app.register_type::<Wireframe>()
37            .register_type::<NoWireframe>()
38            .register_type::<WireframeConfig>()
39            .register_type::<WireframeColor>()
40            .init_resource::<WireframeConfig>()
41            .add_plugins(MaterialPlugin::<WireframeMaterial>::default())
42            .add_systems(Startup, setup_global_wireframe_material)
43            .add_systems(
44                Update,
45                (
46                    global_color_changed.run_if(resource_changed::<WireframeConfig>),
47                    wireframe_color_changed,
48                    // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global
49                    // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed.
50                    (apply_wireframe_material, apply_global_wireframe_material).chain(),
51                ),
52            );
53    }
54}
55
56/// Enables wireframe rendering for any entity it is attached to.
57/// It will ignore the [`WireframeConfig`] global setting.
58///
59/// This requires the [`WireframePlugin`] to be enabled.
60#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
61#[reflect(Component, Default, Debug, PartialEq)]
62pub struct Wireframe;
63
64/// Sets the color of the [`Wireframe`] of the entity it is attached to.
65///
66/// If this component is present but there's no [`Wireframe`] component,
67/// it will still affect the color of the wireframe when [`WireframeConfig::global`] is set to true.
68///
69/// This overrides the [`WireframeConfig::default_color`].
70// TODO: consider caching materials based on this color.
71// This could blow up in size if people use random colored wireframes for each mesh.
72// It will also be important to remove unused materials from the cache.
73#[derive(Component, Debug, Clone, Default, Reflect)]
74#[reflect(Component, Default, Debug)]
75pub struct WireframeColor {
76    pub color: Color,
77}
78
79/// Disables wireframe rendering for any entity it is attached to.
80/// It will ignore the [`WireframeConfig`] global setting.
81///
82/// This requires the [`WireframePlugin`] to be enabled.
83#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
84#[reflect(Component, Default, Debug, PartialEq)]
85pub struct NoWireframe;
86
87#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)]
88#[reflect(Resource, Debug, Default)]
89pub struct WireframeConfig {
90    /// Whether to show wireframes for all meshes.
91    /// Can be overridden for individual meshes by adding a [`Wireframe`] or [`NoWireframe`] component.
92    pub global: bool,
93    /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe`] component attached to it will have
94    /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe`],
95    /// but no [`WireframeColor`].
96    pub default_color: Color,
97}
98
99#[derive(Resource)]
100struct GlobalWireframeMaterial {
101    // This handle will be reused when the global config is enabled
102    handle: Handle<WireframeMaterial>,
103}
104
105fn setup_global_wireframe_material(
106    mut commands: Commands,
107    mut materials: ResMut<Assets<WireframeMaterial>>,
108    config: Res<WireframeConfig>,
109) {
110    // Create the handle used for the global material
111    commands.insert_resource(GlobalWireframeMaterial {
112        handle: materials.add(WireframeMaterial {
113            color: config.default_color.into(),
114        }),
115    });
116}
117
118/// Updates the wireframe material of all entities without a [`WireframeColor`] or without a [`Wireframe`] component
119fn global_color_changed(
120    config: Res<WireframeConfig>,
121    mut materials: ResMut<Assets<WireframeMaterial>>,
122    global_material: Res<GlobalWireframeMaterial>,
123) {
124    if let Some(global_material) = materials.get_mut(&global_material.handle) {
125        global_material.color = config.default_color.into();
126    }
127}
128
129/// Updates the wireframe material when the color in [`WireframeColor`] changes
130#[allow(clippy::type_complexity)]
131fn wireframe_color_changed(
132    mut materials: ResMut<Assets<WireframeMaterial>>,
133    mut colors_changed: Query<
134        (&mut MeshMaterial3d<WireframeMaterial>, &WireframeColor),
135        (With<Wireframe>, Changed<WireframeColor>),
136    >,
137) {
138    for (mut handle, wireframe_color) in &mut colors_changed {
139        handle.0 = materials.add(WireframeMaterial {
140            color: wireframe_color.color.into(),
141        });
142    }
143}
144
145/// Applies or remove the wireframe material to any mesh with a [`Wireframe`] component, and removes it
146/// for any mesh with a [`NoWireframe`] component.
147fn apply_wireframe_material(
148    mut commands: Commands,
149    mut materials: ResMut<Assets<WireframeMaterial>>,
150    wireframes: Query<
151        (Entity, Option<&WireframeColor>),
152        (With<Wireframe>, Without<MeshMaterial3d<WireframeMaterial>>),
153    >,
154    no_wireframes: Query<Entity, (With<NoWireframe>, With<MeshMaterial3d<WireframeMaterial>>)>,
155    mut removed_wireframes: RemovedComponents<Wireframe>,
156    global_material: Res<GlobalWireframeMaterial>,
157) {
158    for e in removed_wireframes.read().chain(no_wireframes.iter()) {
159        if let Some(mut commands) = commands.get_entity(e) {
160            commands.remove::<MeshMaterial3d<WireframeMaterial>>();
161        }
162    }
163
164    let mut material_to_spawn = vec![];
165    for (e, maybe_color) in &wireframes {
166        let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
167        material_to_spawn.push((e, MeshMaterial3d(material)));
168    }
169    commands.insert_or_spawn_batch(material_to_spawn);
170}
171
172type WireframeFilter = (With<Mesh3d>, Without<Wireframe>, Without<NoWireframe>);
173
174/// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component.
175fn apply_global_wireframe_material(
176    mut commands: Commands,
177    config: Res<WireframeConfig>,
178    meshes_without_material: Query<
179        (Entity, Option<&WireframeColor>),
180        (WireframeFilter, Without<MeshMaterial3d<WireframeMaterial>>),
181    >,
182    meshes_with_global_material: Query<
183        Entity,
184        (WireframeFilter, With<MeshMaterial3d<WireframeMaterial>>),
185    >,
186    global_material: Res<GlobalWireframeMaterial>,
187    mut materials: ResMut<Assets<WireframeMaterial>>,
188) {
189    if config.global {
190        let mut material_to_spawn = vec![];
191        for (e, maybe_color) in &meshes_without_material {
192            let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
193            // We only add the material handle but not the Wireframe component
194            // This makes it easy to detect which mesh is using the global material and which ones are user specified
195            material_to_spawn.push((e, MeshMaterial3d(material)));
196        }
197        commands.insert_or_spawn_batch(material_to_spawn);
198    } else {
199        for e in &meshes_with_global_material {
200            commands
201                .entity(e)
202                .remove::<MeshMaterial3d<WireframeMaterial>>();
203        }
204    }
205}
206
207/// Gets an handle to a wireframe material with a fallback on the default material
208fn get_wireframe_material(
209    maybe_color: Option<&WireframeColor>,
210    wireframe_materials: &mut Assets<WireframeMaterial>,
211    global_material: &GlobalWireframeMaterial,
212) -> Handle<WireframeMaterial> {
213    if let Some(wireframe_color) = maybe_color {
214        wireframe_materials.add(WireframeMaterial {
215            color: wireframe_color.color.into(),
216        })
217    } else {
218        // If there's no color specified we can use the global material since it's already set to use the default_color
219        global_material.handle.clone()
220    }
221}
222
223#[derive(Default, AsBindGroup, TypePath, Debug, Clone, Asset)]
224pub struct WireframeMaterial {
225    #[uniform(0)]
226    pub color: LinearRgba,
227}
228
229impl Material for WireframeMaterial {
230    fn fragment_shader() -> ShaderRef {
231        WIREFRAME_SHADER_HANDLE.into()
232    }
233
234    fn specialize(
235        _pipeline: &MaterialPipeline<Self>,
236        descriptor: &mut RenderPipelineDescriptor,
237        _layout: &MeshVertexBufferLayoutRef,
238        _key: MaterialPipelineKey<Self>,
239    ) -> Result<(), SpecializedMeshPipelineError> {
240        descriptor.primitive.polygon_mode = PolygonMode::Line;
241        if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() {
242            depth_stencil.bias.slope_scale = 1.0;
243        }
244        Ok(())
245    }
246}