Skip to main content

bevy_gizmos_render/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc(
3    html_logo_url = "https://bevy.org/assets/icon.png",
4    html_favicon_url = "https://bevy.org/assets/icon.png"
5)]
6
7//! This crate renders `bevy_gizmos` with `bevy_render`.
8
9/// System set label for the systems handling the rendering of gizmos.
10#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)]
11pub enum GizmoRenderSystems {
12    /// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase
13    #[cfg(feature = "bevy_sprite_render")]
14    QueueLineGizmos2d,
15    /// Adds gizmos to the [`Transparent3d`](bevy_core_pipeline::core_3d::Transparent3d) render phase
16    #[cfg(feature = "bevy_pbr")]
17    QueueLineGizmos3d,
18}
19
20pub mod retained;
21
22#[cfg(feature = "bevy_sprite_render")]
23mod pipeline_2d;
24#[cfg(feature = "bevy_pbr")]
25mod pipeline_3d;
26
27use bevy_app::{App, Plugin};
28use bevy_ecs::{
29    resource::Resource,
30    schedule::{IntoScheduleConfigs, SystemSet},
31    system::Res,
32};
33
34use {bevy_gizmos::config::GizmoMeshConfig, bevy_mesh::VertexBufferLayout};
35
36use {
37    crate::retained::extract_linegizmos,
38    bevy_asset::AssetId,
39    bevy_ecs::{
40        component::Component,
41        entity::Entity,
42        query::ROQueryItem,
43        system::{
44            lifetimeless::{Read, SRes},
45            Commands, SystemParamItem,
46        },
47    },
48    bevy_math::{Affine3, Affine3A, Vec4},
49    bevy_render::{
50        extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
51        render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
52        render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
53        render_resource::{
54            binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayoutEntries,
55            Buffer, BufferInitDescriptor, BufferUsages, ShaderStages, ShaderType, VertexFormat,
56        },
57        renderer::RenderDevice,
58        sync_world::{MainEntity, TemporaryRenderEntity},
59        Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems,
60    },
61    bytemuck::cast_slice,
62};
63
64use bevy_render::render_resource::{
65    BindGroupLayoutDescriptor, PipelineCache, VertexAttribute, VertexStepMode,
66};
67
68use bevy_gizmos::{
69    config::{GizmoConfigStore, GizmoLineJoint},
70    GizmoAsset, GizmoHandles,
71};
72
73/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
74///
75/// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpriteRenderPlugin`](bevy_sprite_render::SpriteRenderPlugin).
76#[derive(Default)]
77pub struct GizmoRenderPlugin;
78
79impl Plugin for GizmoRenderPlugin {
80    fn build(&self, app: &mut App) {
81        {
82            use bevy_asset::embedded_asset;
83            embedded_asset!(app, "lines.wgsl");
84            embedded_asset!(app, "line_joints.wgsl");
85        }
86
87        app.add_plugins(UniformComponentPlugin::<LineGizmoUniform>::default())
88            .add_plugins(RenderAssetPlugin::<GpuLineGizmo>::default());
89
90        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
91            render_app.add_systems(RenderStartup, init_line_gizmo_uniform_bind_group_layout);
92
93            render_app.add_systems(
94                Render,
95                prepare_line_gizmo_bind_group.in_set(RenderSystems::PrepareBindGroups),
96            );
97
98            render_app.add_systems(ExtractSchedule, (extract_gizmo_data, extract_linegizmos));
99
100            #[cfg(feature = "bevy_sprite_render")]
101            if app.is_plugin_added::<bevy_sprite_render::SpriteRenderPlugin>() {
102                app.add_plugins(pipeline_2d::LineGizmo2dPlugin);
103            } else {
104                tracing::warn!("bevy_sprite_render feature is enabled but bevy_sprite_render::SpriteRenderPlugin was not detected. Are you sure you loaded GizmoPlugin after SpriteRenderPlugin?");
105            }
106            #[cfg(feature = "bevy_pbr")]
107            if app.is_plugin_added::<bevy_pbr::PbrPlugin>() {
108                app.add_plugins(pipeline_3d::LineGizmo3dPlugin);
109            } else {
110                tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?");
111            }
112        } else {
113            tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?");
114        }
115    }
116}
117
118fn init_line_gizmo_uniform_bind_group_layout(mut commands: Commands) {
119    let line_layout = BindGroupLayoutDescriptor::new(
120        "LineGizmoUniform layout",
121        &BindGroupLayoutEntries::single(
122            ShaderStages::VERTEX,
123            uniform_buffer::<LineGizmoUniform>(true),
124        ),
125    );
126
127    commands.insert_resource(LineGizmoUniformBindgroupLayout {
128        layout: line_layout,
129    });
130}
131
132fn extract_gizmo_data(
133    mut commands: Commands,
134    handles: Extract<Res<GizmoHandles>>,
135    config: Extract<Res<GizmoConfigStore>>,
136) {
137    use bevy_gizmos::config::GizmoLineStyle;
138    use bevy_utils::once;
139    use tracing::warn;
140
141    for (group_type_id, handle) in handles.handles() {
142        let Some((config, _)) = config.get_config_dyn(group_type_id) else {
143            continue;
144        };
145
146        if !config.enabled {
147            continue;
148        }
149
150        #[cfg_attr(
151            not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
152            expect(
153                unused_variables,
154                reason = "`handle` is unused when bevy_pbr and bevy_sprite_render are both disabled."
155            )
156        )]
157        let Some(handle) = handle
158        else {
159            continue;
160        };
161
162        let joints_resolution = if let GizmoLineJoint::Round(resolution) = config.line.joints {
163            resolution
164        } else {
165            0
166        };
167
168        let (gap_scale, line_scale) = if let GizmoLineStyle::Dashed {
169            gap_scale,
170            line_scale,
171        } = config.line.style
172        {
173            if gap_scale <= 0.0 {
174                once!(warn!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero."));
175            }
176            if line_scale <= 0.0 {
177                once!(warn!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero."));
178            }
179            (gap_scale, line_scale)
180        } else {
181            (1.0, 1.0)
182        };
183
184        commands.spawn((
185            LineGizmoUniform {
186                world_from_local: Affine3::from(&Affine3A::IDENTITY).to_transpose(),
187                line_width: config.line.width,
188                depth_bias: config.depth_bias,
189                joints_resolution,
190                gap_scale,
191                line_scale,
192                #[cfg(feature = "webgl")]
193                _padding: Default::default(),
194            },
195            #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite_render"))]
196            GizmoMeshConfig {
197                line_perspective: config.line.perspective,
198                line_style: config.line.style,
199                line_joints: config.line.joints,
200                render_layers: config.render_layers.clone(),
201                handle: handle.clone(),
202            },
203            // The immediate mode API does not have a main world entity to refer to,
204            // but we do need MainEntity on this render entity for the systems to find it.
205            MainEntity::from(Entity::PLACEHOLDER),
206            TemporaryRenderEntity,
207        ));
208    }
209}
210
211#[derive(Component, ShaderType, Clone, Copy)]
212struct LineGizmoUniform {
213    world_from_local: [Vec4; 3],
214    line_width: f32,
215    depth_bias: f32,
216    // Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)`
217    joints_resolution: u32,
218    // Only used if the current configs `line_style` is set to `GizmoLineStyle::Dashed{_}`
219    gap_scale: f32,
220    line_scale: f32,
221    /// WebGL2 structs must be 16 byte aligned.
222    #[cfg(feature = "webgl")]
223    _padding: bevy_math::Vec3,
224}
225
226#[cfg_attr(
227    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
228    expect(
229        dead_code,
230        reason = "fields are unused when bevy_pbr and bevy_sprite_render are both disabled."
231    )
232)]
233#[derive(Debug, Clone)]
234struct GpuLineGizmo {
235    list_position_buffer: Buffer,
236    list_color_buffer: Buffer,
237    list_vertex_count: u32,
238    strip_position_buffer: Buffer,
239    strip_color_buffer: Buffer,
240    strip_vertex_count: u32,
241}
242
243impl RenderAsset for GpuLineGizmo {
244    type SourceAsset = GizmoAsset;
245    type Param = SRes<RenderDevice>;
246
247    fn prepare_asset(
248        gizmo: Self::SourceAsset,
249        _: AssetId<Self::SourceAsset>,
250        render_device: &mut SystemParamItem<Self::Param>,
251        _: Option<&Self>,
252    ) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
253        let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
254            usage: BufferUsages::VERTEX,
255            label: Some("LineGizmo Position Buffer"),
256            contents: cast_slice(&gizmo.buffer().list_positions),
257        });
258
259        let list_color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
260            usage: BufferUsages::VERTEX,
261            label: Some("LineGizmo Color Buffer"),
262            contents: cast_slice(&gizmo.buffer().list_colors),
263        });
264
265        let strip_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
266            usage: BufferUsages::VERTEX,
267            label: Some("LineGizmo Strip Position Buffer"),
268            contents: cast_slice(&gizmo.buffer().strip_positions),
269        });
270
271        let strip_color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
272            usage: BufferUsages::VERTEX,
273            label: Some("LineGizmo Strip Color Buffer"),
274            contents: cast_slice(&gizmo.buffer().strip_colors),
275        });
276
277        Ok(GpuLineGizmo {
278            list_position_buffer,
279            list_color_buffer,
280            list_vertex_count: gizmo.buffer().list_positions.len() as u32,
281            strip_position_buffer,
282            strip_color_buffer,
283            strip_vertex_count: gizmo.buffer().strip_positions.len() as u32,
284        })
285    }
286}
287
288#[derive(Resource)]
289struct LineGizmoUniformBindgroupLayout {
290    layout: BindGroupLayoutDescriptor,
291}
292
293#[cfg_attr(
294    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
295    expect(
296        dead_code,
297        reason = "fields are unused when bevy_pbr and bevy_sprite_render are both disabled."
298    )
299)]
300#[derive(Resource)]
301struct LineGizmoUniformBindgroup {
302    bindgroup: BindGroup,
303}
304
305fn prepare_line_gizmo_bind_group(
306    mut commands: Commands,
307    line_gizmo_uniform_layout: Res<LineGizmoUniformBindgroupLayout>,
308    render_device: Res<RenderDevice>,
309    pipeline_cache: Res<PipelineCache>,
310    line_gizmo_uniforms: Res<ComponentUniforms<LineGizmoUniform>>,
311) {
312    if let Some(binding) = line_gizmo_uniforms.uniforms().binding() {
313        commands.insert_resource(LineGizmoUniformBindgroup {
314            bindgroup: render_device.create_bind_group(
315                "LineGizmoUniform bindgroup",
316                &pipeline_cache.get_bind_group_layout(&line_gizmo_uniform_layout.layout),
317                &BindGroupEntries::single(binding),
318            ),
319        });
320    }
321}
322
323#[cfg_attr(
324    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
325    expect(
326        dead_code,
327        reason = "struct is not constructed when bevy_pbr and bevy_sprite_render are both disabled."
328    )
329)]
330struct SetLineGizmoBindGroup<const I: usize>;
331
332impl<const I: usize, P: PhaseItem> RenderCommand<P> for SetLineGizmoBindGroup<I> {
333    type Param = SRes<LineGizmoUniformBindgroup>;
334    type ViewQuery = ();
335    type ItemQuery = Read<DynamicUniformIndex<LineGizmoUniform>>;
336
337    #[inline]
338    fn render<'w>(
339        _item: &P,
340        _view: ROQueryItem<'w, '_, Self::ViewQuery>,
341        uniform_index: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
342        bind_group: SystemParamItem<'w, '_, Self::Param>,
343        pass: &mut TrackedRenderPass<'w>,
344    ) -> RenderCommandResult {
345        let Some(uniform_index) = uniform_index else {
346            return RenderCommandResult::Skip;
347        };
348        pass.set_bind_group(
349            I,
350            &bind_group.into_inner().bindgroup,
351            &[uniform_index.index()],
352        );
353        RenderCommandResult::Success
354    }
355}
356
357#[cfg_attr(
358    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
359    expect(
360        dead_code,
361        reason = "struct is not constructed when bevy_pbr and bevy_sprite_render are both disabled."
362    )
363)]
364struct DrawLineGizmo<const STRIP: bool>;
365
366impl<P: PhaseItem, const STRIP: bool> RenderCommand<P> for DrawLineGizmo<STRIP> {
367    type Param = SRes<RenderAssets<GpuLineGizmo>>;
368    type ViewQuery = ();
369    type ItemQuery = Read<GizmoMeshConfig>;
370
371    #[inline]
372    fn render<'w>(
373        _item: &P,
374        _view: ROQueryItem<'w, '_, Self::ViewQuery>,
375        config: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
376        line_gizmos: SystemParamItem<'w, '_, Self::Param>,
377        pass: &mut TrackedRenderPass<'w>,
378    ) -> RenderCommandResult {
379        let Some(config) = config else {
380            return RenderCommandResult::Skip;
381        };
382        let Some(line_gizmo) = line_gizmos.into_inner().get(&config.handle) else {
383            return RenderCommandResult::Skip;
384        };
385
386        let vertex_count = if STRIP {
387            line_gizmo.strip_vertex_count
388        } else {
389            line_gizmo.list_vertex_count
390        };
391
392        if vertex_count < 2 {
393            return RenderCommandResult::Success;
394        }
395
396        let instances = if STRIP {
397            let item_size = VertexFormat::Float32x3.size();
398            let buffer_size = line_gizmo.strip_position_buffer.size() - item_size;
399
400            pass.set_vertex_buffer(0, line_gizmo.strip_position_buffer.slice(..buffer_size));
401            pass.set_vertex_buffer(1, line_gizmo.strip_position_buffer.slice(item_size..));
402
403            let item_size = VertexFormat::Float32x4.size();
404            let buffer_size = line_gizmo.strip_color_buffer.size() - item_size;
405
406            pass.set_vertex_buffer(2, line_gizmo.strip_color_buffer.slice(..buffer_size));
407            pass.set_vertex_buffer(3, line_gizmo.strip_color_buffer.slice(item_size..));
408
409            vertex_count - 1
410        } else {
411            pass.set_vertex_buffer(0, line_gizmo.list_position_buffer.slice(..));
412            pass.set_vertex_buffer(1, line_gizmo.list_color_buffer.slice(..));
413
414            vertex_count / 2
415        };
416
417        pass.draw(0..6, 0..instances);
418
419        RenderCommandResult::Success
420    }
421}
422
423#[cfg_attr(
424    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
425    expect(
426        dead_code,
427        reason = "struct is not constructed when bevy_pbr and bevy_sprite_render are both disabled."
428    )
429)]
430struct DrawLineJointGizmo;
431
432impl<P: PhaseItem> RenderCommand<P> for DrawLineJointGizmo {
433    type Param = SRes<RenderAssets<GpuLineGizmo>>;
434    type ViewQuery = ();
435    type ItemQuery = Read<GizmoMeshConfig>;
436
437    #[inline]
438    fn render<'w>(
439        _item: &P,
440        _view: ROQueryItem<'w, '_, Self::ViewQuery>,
441        config: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
442        line_gizmos: SystemParamItem<'w, '_, Self::Param>,
443        pass: &mut TrackedRenderPass<'w>,
444    ) -> RenderCommandResult {
445        let Some(config) = config else {
446            return RenderCommandResult::Skip;
447        };
448        let Some(line_gizmo) = line_gizmos.into_inner().get(&config.handle) else {
449            return RenderCommandResult::Skip;
450        };
451
452        if line_gizmo.strip_vertex_count <= 2 {
453            return RenderCommandResult::Success;
454        };
455
456        if config.line_joints == GizmoLineJoint::None {
457            return RenderCommandResult::Success;
458        };
459
460        let instances = {
461            let item_size = VertexFormat::Float32x3.size();
462            // position_a
463            let buffer_size_a = line_gizmo.strip_position_buffer.size() - item_size * 2;
464            pass.set_vertex_buffer(0, line_gizmo.strip_position_buffer.slice(..buffer_size_a));
465            // position_b
466            let buffer_size_b = line_gizmo.strip_position_buffer.size() - item_size;
467            pass.set_vertex_buffer(
468                1,
469                line_gizmo
470                    .strip_position_buffer
471                    .slice(item_size..buffer_size_b),
472            );
473            // position_c
474            pass.set_vertex_buffer(2, line_gizmo.strip_position_buffer.slice(item_size * 2..));
475
476            // color
477            let item_size = VertexFormat::Float32x4.size();
478            let buffer_size = line_gizmo.strip_color_buffer.size() - item_size;
479            // This corresponds to the color of position_b, hence starts from `item_size`
480            pass.set_vertex_buffer(
481                3,
482                line_gizmo.strip_color_buffer.slice(item_size..buffer_size),
483            );
484
485            line_gizmo.strip_vertex_count - 2
486        };
487
488        let vertices = match config.line_joints {
489            GizmoLineJoint::None => unreachable!(),
490            GizmoLineJoint::Miter => 6,
491            GizmoLineJoint::Round(resolution) => resolution * 3,
492            GizmoLineJoint::Bevel => 3,
493        };
494
495        pass.draw(0..vertices, 0..instances);
496
497        RenderCommandResult::Success
498    }
499}
500
501#[cfg_attr(
502    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
503    expect(
504        dead_code,
505        reason = "function is unused when bevy_pbr and bevy_sprite_render are both disabled."
506    )
507)]
508fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
509    use VertexFormat::*;
510    let mut position_layout = VertexBufferLayout {
511        array_stride: Float32x3.size(),
512        step_mode: VertexStepMode::Instance,
513        attributes: vec![VertexAttribute {
514            format: Float32x3,
515            offset: 0,
516            shader_location: 0,
517        }],
518    };
519
520    let mut color_layout = VertexBufferLayout {
521        array_stride: Float32x4.size(),
522        step_mode: VertexStepMode::Instance,
523        attributes: vec![VertexAttribute {
524            format: Float32x4,
525            offset: 0,
526            shader_location: 2,
527        }],
528    };
529
530    if strip {
531        vec![
532            position_layout.clone(),
533            {
534                position_layout.attributes[0].shader_location = 1;
535                position_layout
536            },
537            color_layout.clone(),
538            {
539                color_layout.attributes[0].shader_location = 3;
540                color_layout
541            },
542        ]
543    } else {
544        position_layout.array_stride *= 2;
545        position_layout.attributes.push(VertexAttribute {
546            format: Float32x3,
547            offset: Float32x3.size(),
548            shader_location: 1,
549        });
550
551        color_layout.array_stride *= 2;
552        color_layout.attributes.push(VertexAttribute {
553            format: Float32x4,
554            offset: Float32x4.size(),
555            shader_location: 3,
556        });
557
558        vec![position_layout, color_layout]
559    }
560}
561
562#[cfg_attr(
563    not(any(feature = "bevy_pbr", feature = "bevy_sprite_render")),
564    expect(
565        dead_code,
566        reason = "function is unused when bevy_pbr and bevy_sprite_render are both disabled."
567    )
568)]
569fn line_joint_gizmo_vertex_buffer_layouts() -> Vec<VertexBufferLayout> {
570    use VertexFormat::*;
571    let mut position_layout = VertexBufferLayout {
572        array_stride: Float32x3.size(),
573        step_mode: VertexStepMode::Instance,
574        attributes: vec![VertexAttribute {
575            format: Float32x3,
576            offset: 0,
577            shader_location: 0,
578        }],
579    };
580
581    let color_layout = VertexBufferLayout {
582        array_stride: Float32x4.size(),
583        step_mode: VertexStepMode::Instance,
584        attributes: vec![VertexAttribute {
585            format: Float32x4,
586            offset: 0,
587            shader_location: 3,
588        }],
589    };
590
591    vec![
592        position_layout.clone(),
593        {
594            position_layout.attributes[0].shader_location = 1;
595            position_layout.clone()
596        },
597        {
598            position_layout.attributes[0].shader_location = 2;
599            position_layout
600        },
601        color_layout.clone(),
602    ]
603}