Skip to main content

avian3d/debug_render/
mod.rs

1//! Renders physics objects and properties for debugging purposes.
2//!
3//! See [`PhysicsDebugPlugin`].
4
5#![allow(clippy::unnecessary_cast)]
6
7mod configuration;
8mod gizmos;
9mod tracked_spatial_query;
10
11pub use configuration::*;
12pub use gizmos::*;
13pub(crate) use tracked_spatial_query::*;
14
15use crate::{
16    collider_tree::ColliderTrees,
17    dynamics::{
18        joints::EntityConstraint,
19        solver::{
20            islands::{BodyIslandNode, PhysicsIslands},
21            solver_body::{SolverBodies, SolverBodyFlags, SolverBodyIndex},
22        },
23    },
24    prelude::*,
25};
26use bevy::{
27    camera::visibility::VisibilitySystems,
28    ecs::{
29        query::Has,
30        system::{StaticSystemParam, SystemParam, SystemParamItem},
31    },
32    prelude::*,
33    shape::Aabb3d,
34};
35
36/// A plugin that renders physics objects and properties for debugging purposes.
37/// It is not enabled by default and must be added manually.
38///
39/// Currently, the following are supported for debug rendering:
40///
41/// - The axes and center of mass of [rigid bodies](RigidBody)
42/// - [AABBs](ColliderAabb)
43/// - [Collider] wireframes
44/// - Using different colors for [sleeping](Sleeping) bodies
45/// - [Contacts](ContactPair)
46/// - [Joints](dynamics::joints)
47/// - [Spatial queries](spatial_query)
48/// - [Simulation islands](dynamics::solver::islands)
49/// - [Collider tree](crate::collider_tree) nodes
50/// - Changing the visibility of entities to only show debug rendering
51///
52/// By default, [AABBs](ColliderAabb) and [contacts](ContactPair) are not debug rendered.
53/// You can configure the [`PhysicsGizmos`] retrieved from `GizmoConfigStore` for the global configuration
54/// and the [`DebugRender`] component for entity-level configuration.
55///
56/// # Example
57///
58/// ```no_run
59#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
60#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
61/// use bevy::prelude::*;
62///
63/// fn main() {
64///     App::new()
65///         .add_plugins((
66///             DefaultPlugins,
67///             PhysicsPlugins::default(),
68///             // Enables debug rendering
69///             PhysicsDebugPlugin,
70///         ))
71///         // Overwrite default debug rendering configuration (optional)
72///         .insert_gizmo_config(
73///             PhysicsGizmos {
74///                 aabb_color: Some(Color::WHITE),
75///                 ..default()
76///             },
77///             GizmoConfig::default(),
78///         )
79///         .run();
80/// }
81///
82/// fn setup(mut commands: Commands) {
83///     // This rigid body and its collider and AABB will get rendered
84///     commands.spawn((
85///         RigidBody::Dynamic,
86#[cfg_attr(feature = "2d", doc = "        Collider::circle(0.5),")]
87#[cfg_attr(feature = "3d", doc = "        Collider::sphere(0.5),")]
88///         // Overwrite default collider color (optional)
89///         DebugRender::default().with_collider_color(Color::srgb(1.0, 0.0, 0.0)),
90///     ));
91/// }
92/// ```
93#[derive(Default)]
94pub struct PhysicsDebugPlugin;
95
96impl Plugin for PhysicsDebugPlugin {
97    fn build(&self, app: &mut App) {
98        app.init_gizmo_group::<PhysicsGizmos>();
99
100        let mut store = app.world_mut().resource_mut::<GizmoConfigStore>();
101        let config = store.config_mut::<PhysicsGizmos>().0;
102        #[cfg(feature = "2d")]
103        {
104            config.line.width = 2.0;
105        }
106        #[cfg(feature = "3d")]
107        {
108            config.line.width = 1.5;
109        }
110
111        app.add_systems(
112            PostUpdate,
113            (
114                debug_render_axes,
115                debug_render_aabbs,
116                debug_render_bvh,
117                (
118                    debug_render_tracked_spatial_queries,
119                    #[cfg(all(
120                        feature = "default-collider",
121                        any(feature = "parry-f32", feature = "parry-f64")
122                    ))]
123                    debug_render_colliders,
124                )
125                    .chain(),
126                debug_render_contacts,
127                // TODO: Refactor joints to allow iterating over all of them without generics
128                debug_render_constraint::<FixedJoint, 2>,
129                debug_render_constraint::<PrismaticJoint, 2>,
130                debug_render_constraint::<DistanceJoint, 2>,
131                debug_render_constraint::<RevoluteJoint, 2>,
132                #[cfg(feature = "3d")]
133                debug_render_constraint::<SphericalJoint, 2>,
134                debug_render_raycasts,
135                #[cfg(all(
136                    feature = "default-collider",
137                    any(feature = "parry-f32", feature = "parry-f64")
138                ))]
139                debug_render_shapecasts,
140                debug_render_islands.run_if(resource_exists::<PhysicsIslands>),
141            )
142                .after(TransformSystems::Propagate)
143                .run_if(|store: Res<GizmoConfigStore>| store.config::<PhysicsGizmos>().0.enabled),
144        )
145        .add_systems(
146            PostUpdate,
147            change_mesh_visibility.before(VisibilitySystems::CalculateBounds),
148        )
149        .add_systems(First, sync_tracked_spatial_queries)
150        .init_resource::<TrackedSpatialQueries>()
151        .init_resource::<TrackedShapeIntersections>();
152    }
153}
154
155#[allow(clippy::type_complexity)]
156fn debug_render_axes(
157    bodies: Query<(
158        &GlobalTransform,
159        &ComputedCenterOfMass,
160        Has<Sleeping>,
161        Option<&DebugRender>,
162    )>,
163    mut gizmos: Gizmos<PhysicsGizmos>,
164    store: Res<GizmoConfigStore>,
165    length_unit: Res<PhysicsLengthUnit>,
166) {
167    let config = store.config::<PhysicsGizmos>().1;
168    for (transform, local_com, sleeping, render_config) in &bodies {
169        let pos = Position::from(transform);
170        let rot = Rotation::from(transform);
171
172        // If the body is sleeping, the colors will be multiplied by the sleeping color multiplier
173        if let Some(mut lengths) = render_config.map_or(config.axis_lengths, |c| c.axis_lengths) {
174            lengths *= length_unit.0;
175
176            let mul = if sleeping {
177                render_config
178                    .map_or(config.sleeping_color_multiplier, |c| {
179                        c.sleeping_color_multiplier
180                    })
181                    .unwrap_or([1.0; 4])
182            } else {
183                [1.0; 4]
184            };
185            let [x_color, y_color, _z_color] = [
186                Color::hsla(0.0, 1.0 * mul[1], 0.5 * mul[2], 1.0 * mul[3]),
187                Color::hsla(120.0 * mul[0], 1.0 * mul[1], 0.4 * mul[2], 1.0 * mul[3]),
188                Color::hsla(220.0 * mul[0], 1.0 * mul[1], 0.6 * mul[2], 1.0 * mul[3]),
189            ];
190            let global_com = pos.0 + (rot * local_com.0).real();
191
192            let x = (rot * (Vector::X * lengths.x)).real();
193            gizmos.draw_line(global_com - x, global_com + x, x_color);
194
195            let y = (rot * (Vector::Y * lengths.y)).real();
196            gizmos.draw_line(global_com - y, global_com + y, y_color);
197
198            #[cfg(feature = "3d")]
199            {
200                let z = (rot * (Vector::Z * lengths.z)).real();
201                gizmos.draw_line(global_com - z, global_com + z, _z_color);
202            }
203        }
204    }
205}
206
207fn debug_render_aabbs(
208    aabbs: Query<(
209        Entity,
210        &ColliderAabb,
211        Option<&ColliderOf>,
212        Option<&DebugRender>,
213    )>,
214    sleeping: Query<(), With<Sleeping>>,
215    mut gizmos: Gizmos<PhysicsGizmos>,
216    store: Res<GizmoConfigStore>,
217) {
218    let config = store.config::<PhysicsGizmos>().1;
219    #[cfg(feature = "2d")]
220    for (entity, aabb, collider_rb, render_config) in &aabbs {
221        if let Some(mut color) = render_config.map_or(config.aabb_color, |c| c.aabb_color) {
222            let collider_rb = collider_rb.map_or(entity, |c| c.body);
223
224            // If the body is sleeping, multiply the color by the sleeping color multiplier
225            if sleeping.contains(collider_rb) {
226                let hsla = Hsla::from(color).to_vec4();
227                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
228                    c.sleeping_color_multiplier
229                }) {
230                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
231                }
232            }
233
234            gizmos.rect_2d(aabb.center().f32(), aabb.size(), color);
235        }
236    }
237
238    #[cfg(feature = "3d")]
239    for (entity, aabb, collider_rb, render_config) in &aabbs {
240        if let Some(mut color) = render_config.map_or(config.aabb_color, |c| c.aabb_color) {
241            use bevy_shape::Aabb3d;
242
243            let collider_rb = collider_rb.map_or(entity, |c| c.body);
244
245            // If the body is sleeping, multiply the color by the sleeping color multiplier
246            if sleeping.contains(collider_rb) {
247                let hsla = Hsla::from(color).to_vec4();
248                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
249                    c.sleeping_color_multiplier
250                }) {
251                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
252                }
253            }
254
255            gizmos.aabb_3d(
256                Aabb3d {
257                    min: Vec3A::from(aabb.min),
258                    max: Vec3A::from(aabb.max),
259                },
260                Transform::IDENTITY,
261                color,
262            );
263        }
264    }
265}
266
267fn debug_render_bvh(
268    bvh: Res<ColliderTrees>,
269    mut gizmos: Gizmos<PhysicsGizmos>,
270    store: Res<GizmoConfigStore>,
271) {
272    let config = store.config::<PhysicsGizmos>().1;
273
274    let Some(collider_tree_color) = config.collider_tree_color else {
275        return;
276    };
277
278    for node in bvh.iter_trees().flat_map(|tree| tree.bvh.nodes.iter()) {
279        if node.prim_count == 0 && node.aabb.valid() {
280            gizmos.aabb_3d(
281                Aabb3d::from_min_max(node.aabb.min.to_array(), node.aabb.max.to_array()),
282                Transform::IDENTITY,
283                collider_tree_color,
284            );
285        }
286    }
287}
288
289#[cfg(all(
290    feature = "default-collider",
291    any(feature = "parry-f32", feature = "parry-f64")
292))]
293#[allow(clippy::type_complexity)]
294fn debug_render_colliders(
295    mut colliders: Query<(
296        Entity,
297        &Collider,
298        &GlobalTransform,
299        Option<&ColliderOf>,
300        Option<&DebugRender>,
301    )>,
302    sleeping: Query<(), With<Sleeping>>,
303    body_indices: Query<&SolverBodyIndex>,
304    solver_bodies: Res<SolverBodies>,
305    shape_intersection_tests: Res<TrackedShapeIntersections>,
306    mut gizmos: Gizmos<PhysicsGizmos>,
307    store: Res<GizmoConfigStore>,
308) {
309    let config = store.config::<PhysicsGizmos>().1;
310    for (entity, collider, transform, collider_rb, render_config) in &mut colliders {
311        let position = Position::from(transform);
312        let rotation = Rotation::from(transform);
313
314        // Colliders hit by a tracked shape intersection test are drawn using the highlight color.
315        let intersection_color = shape_intersection_tests
316            .contains(&entity)
317            .then_some(config.shape_intersection_color)
318            .flatten();
319
320        if let Some(mut color) = intersection_color
321            .or_else(|| render_config.map_or(config.collider_color, |c| c.collider_color))
322        {
323            let collider_rb = collider_rb.map_or(entity, |c| c.body);
324
325            let ccd_color = (render_config.is_none() && intersection_color.is_none())
326                .then(|| body_indices.get(collider_rb).ok().copied())
327                .flatten()
328                .and_then(|index| solver_bodies.get(index))
329                .and_then(|solver_body| {
330                    if solver_body
331                        .flags
332                        .contains(SolverBodyFlags::HAD_TIME_OF_IMPACT)
333                    {
334                        config.time_of_impact_color
335                    } else if solver_body.flags.contains(SolverBodyFlags::IS_FAST) {
336                        config.fast_body_color
337                    } else {
338                        None
339                    }
340                });
341
342            if let Some(ccd_color) = ccd_color {
343                color = ccd_color;
344            } else if intersection_color.is_none() && sleeping.contains(collider_rb) {
345                // If the body is sleeping, multiply the color by the sleeping color multiplier.
346                let hsla = Hsla::from(color).to_vec4();
347                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
348                    c.sleeping_color_multiplier
349                }) {
350                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
351                }
352            }
353
354            gizmos.draw_collider(collider, position.0, rotation, color);
355        }
356    }
357}
358
359fn debug_render_contacts(
360    collisions: Collisions,
361    mut gizmos: Gizmos<PhysicsGizmos>,
362    store: Res<GizmoConfigStore>,
363    time: Res<Time<Substeps>>,
364    length_unit: Res<PhysicsLengthUnit>,
365) {
366    let delta_secs = time.delta_secs();
367    let config = store.config::<PhysicsGizmos>().1;
368
369    if config.contact_point_color.is_none() && config.contact_normal_color.is_none() {
370        return;
371    }
372
373    for contacts in collisions.iter() {
374        for manifold in contacts.manifolds.iter() {
375            for contact in manifold.points.iter() {
376                // Don't render contacts that aren't penetrating
377                if contact.penetration <= f32::EPSILON {
378                    continue;
379                }
380
381                // Draw contact points
382                if let Some(color) = config.contact_point_color {
383                    #[cfg(feature = "2d")]
384                    {
385                        gizmos.circle_2d(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
386                    }
387                    #[cfg(feature = "3d")]
388                    {
389                        gizmos.sphere(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
390                    }
391                }
392
393                // Draw contact normals
394                if let Some(color) = config.contact_normal_color {
395                    // The length of the normal arrows
396                    let length = length_unit.0
397                        * match config.contact_normal_scale {
398                            ContactGizmoScale::Constant(length) => length,
399                            ContactGizmoScale::Scaled(scale) => {
400                                scale * contact.normal_impulse / delta_secs
401                            }
402                        };
403
404                    gizmos.draw_arrow(
405                        contact.point,
406                        contact.point + (manifold.normal * length).real(),
407                        0.1 * length_unit.0,
408                        color,
409                    );
410                }
411            }
412        }
413    }
414}
415
416/// A trait for rendering debug information about constraints.
417///
418/// Add the [`debug_render_constraint`] system to render all constraints that implement this trait.
419pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
420    /// A [`SystemParam`] type for any additional ECS access required for rendering the constraint.
421    type Context: SystemParam;
422
423    /// Renders the debug information for the constraint.
424    fn debug_render(
425        &self,
426        positions: [RVector; N],
427        rotations: [Rotation; N],
428        context: &mut SystemParamItem<Self::Context>,
429        gizmos: &mut Gizmos<PhysicsGizmos>,
430        config: &PhysicsGizmos,
431    );
432}
433
434/// A system that renders all constraints that implement the [`DebugRenderConstraint`] trait.
435pub fn debug_render_constraint<T: Component + DebugRenderConstraint<N>, const N: usize>(
436    bodies: Query<&GlobalTransform>,
437    constraints: Query<&T>,
438    mut gizmos: Gizmos<PhysicsGizmos>,
439    store: Res<GizmoConfigStore>,
440    mut context: StaticSystemParam<T::Context>,
441) {
442    let config = store.config::<PhysicsGizmos>().1;
443    for constraint in &constraints {
444        if let Ok(bodies) = bodies.get_many(constraint.entities()) {
445            let positions: [RVector; N] = bodies
446                .iter()
447                .map(|transform| Position::from(**transform).0)
448                .collect::<Vec<_>>()
449                .try_into()
450                .unwrap();
451            let rotations: [Rotation; N] = bodies
452                .iter()
453                .map(|transform| Rotation::from(**transform))
454                .collect::<Vec<_>>()
455                .try_into()
456                .unwrap();
457
458            constraint.debug_render(positions, rotations, &mut context, &mut gizmos, config);
459        }
460    }
461}
462
463fn debug_render_raycasts(
464    query: Query<(&RayCaster, &RayHits)>,
465    mut gizmos: Gizmos<PhysicsGizmos>,
466    store: Res<GizmoConfigStore>,
467    length_unit: Res<PhysicsLengthUnit>,
468) {
469    let config = store.config::<PhysicsGizmos>().1;
470    for (ray, hits) in &query {
471        let ray_color = config.raycast_color.unwrap_or(Color::NONE);
472        let point_color = config.raycast_point_color.unwrap_or(Color::NONE);
473        let normal_color = config.raycast_normal_color.unwrap_or(Color::NONE);
474
475        gizmos.draw_raycast(
476            ray.global_origin(),
477            ray.global_direction(),
478            // f32::MAX renders nothing, but this number seems to be fine :P
479            ray.max_distance.min(1_000_000_000_000_000_000.0),
480            hits.as_slice(),
481            ray_color,
482            point_color,
483            normal_color,
484            length_unit.0,
485        );
486    }
487}
488
489#[cfg(all(
490    feature = "default-collider",
491    any(feature = "parry-f32", feature = "parry-f64")
492))]
493fn debug_render_shapecasts(
494    query: Query<(&ShapeCaster, &ShapeHits)>,
495    mut gizmos: Gizmos<PhysicsGizmos>,
496    store: Res<GizmoConfigStore>,
497    length_unit: Res<PhysicsLengthUnit>,
498) {
499    let config = store.config::<PhysicsGizmos>().1;
500    for (shape_caster, hits) in &query {
501        let ray_color = config.shapecast_color.unwrap_or(Color::NONE);
502        let shape_color = config.shapecast_shape_color.unwrap_or(Color::NONE);
503        let point_color = config.shapecast_point_color.unwrap_or(Color::NONE);
504        let normal_color = config.shapecast_normal_color.unwrap_or(Color::NONE);
505
506        gizmos.draw_shapecast(
507            &shape_caster.shape,
508            shape_caster.global_origin(),
509            shape_caster.global_shape_rotation(),
510            shape_caster.global_direction(),
511            // f32::MAX renders nothing, but this number seems to be fine :P
512            shape_caster.max_distance.min(1_000_000_000_000_000.0),
513            hits.as_slice(),
514            ray_color,
515            shape_color,
516            point_color,
517            normal_color,
518            length_unit.0,
519        );
520    }
521}
522
523fn debug_render_islands(
524    islands: Res<PhysicsIslands>,
525    bodies: Query<(&RigidBodyColliders, &BodyIslandNode)>,
526    aabbs: Query<&ColliderAabb>,
527    mut gizmos: Gizmos<PhysicsGizmos>,
528    store: Res<GizmoConfigStore>,
529) {
530    let config = store.config::<PhysicsGizmos>().1;
531
532    for island in islands.iter() {
533        if let Some(mut color) = config.island_color {
534            // If the island is sleeping, multiply the color by the sleeping color multiplier
535            if island.is_sleeping {
536                let hsla = Hsla::from(color).to_vec4();
537                if let Some(mul) = config.sleeping_color_multiplier {
538                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
539                }
540            }
541
542            // If the island is empty, skip rendering
543            if island.body_count == 0 {
544                continue;
545            }
546
547            let mut body = island.head_body;
548            let mut aabb: Option<ColliderAabb> = None;
549
550            // Compute the island's AABB by merging the AABBs of all bodies in the island.
551            while let Some(next_body) = body {
552                if let Ok((colliders, body_island)) = bodies.get(next_body) {
553                    for collider in colliders.iter() {
554                        if let Ok(collider_aabb) = aabbs.get(collider) {
555                            aabb = Some(
556                                aabb.map_or(*collider_aabb, |aabb| aabb.merged(*collider_aabb)),
557                            );
558                        }
559                    }
560                    body = body_island.next;
561                } else {
562                    break;
563                }
564            }
565
566            let Some(aabb) = aabb else {
567                continue;
568            };
569
570            // Render the island's AABB.
571            #[cfg(feature = "2d")]
572            {
573                gizmos.rect_2d(aabb.center().f32(), aabb.size(), color);
574            }
575            #[cfg(feature = "3d")]
576            {
577                use bevy_shape::Aabb3d;
578
579                gizmos.aabb_3d(
580                    Aabb3d {
581                        min: Vec3A::from(aabb.min),
582                        max: Vec3A::from(aabb.max),
583                    },
584                    Transform::IDENTITY,
585                    color,
586                );
587            }
588        }
589    }
590}
591
592/// Caches whether physics gizmos are enabled on [`TrackedSpatialQueries`].
593fn sync_tracked_spatial_queries(
594    store: Res<GizmoConfigStore>,
595    mut queries: ResMut<TrackedSpatialQueries>,
596) {
597    let enabled = store.config::<PhysicsGizmos>().0.enabled;
598
599    if !enabled && queries.enabled {
600        queries.clear();
601    }
602
603    queries.enabled = enabled;
604}
605
606fn debug_render_tracked_spatial_queries(
607    mut gizmos: Gizmos<PhysicsGizmos>,
608    store: Res<GizmoConfigStore>,
609    mut queries: ResMut<TrackedSpatialQueries>,
610    mut shape_intersections: ResMut<TrackedShapeIntersections>,
611    length_unit: Res<PhysicsLengthUnit>,
612) {
613    let config = store.config::<PhysicsGizmos>().1;
614
615    shape_intersections.clear();
616
617    for query in queries.drain() {
618        match query {
619            TrackedSpatialQuery::Raycast {
620                origin,
621                direction,
622                max_distance,
623                hits,
624            } => {
625                let arrow_color = config.raycast_color.unwrap_or(Color::NONE);
626                let point_color = config.raycast_point_color.unwrap_or(Color::NONE);
627                let normal_color = config.raycast_normal_color.unwrap_or(Color::NONE);
628
629                gizmos.draw_raycast(
630                    origin,
631                    direction,
632                    // f32::MAX renders nothing, but this number seems to be fine :P
633                    max_distance.min(1_000_000_000_000_000_000.0),
634                    &hits,
635                    arrow_color,
636                    point_color,
637                    normal_color,
638                    **length_unit,
639                );
640            }
641            TrackedSpatialQuery::Shapecast {
642                shape,
643                origin,
644                rotation,
645                direction,
646                max_distance,
647                hits,
648            } => {
649                let arrow_color = config.shapecast_color.unwrap_or(Color::NONE);
650                let shape_color = config.shapecast_shape_color.unwrap_or(Color::NONE);
651                let point_color = config.shapecast_point_color.unwrap_or(Color::NONE);
652                let normal_color = config.shapecast_normal_color.unwrap_or(Color::NONE);
653
654                gizmos.draw_shapecast(
655                    &shape,
656                    origin,
657                    rotation,
658                    direction,
659                    // f32::MAX renders nothing, but this number seems to be fine :P
660                    max_distance.min(1_000_000_000_000_000.0),
661                    &hits,
662                    arrow_color,
663                    shape_color,
664                    point_color,
665                    normal_color,
666                    **length_unit,
667                );
668            }
669            TrackedSpatialQuery::PointProjection { point, projection } => {
670                let line_color = config.point_projection_color.unwrap_or(Color::NONE);
671                let origin_color = config.point_projection_origin_color.unwrap_or(Color::NONE);
672                let projection_color = config.point_projection_point_color.unwrap_or(Color::NONE);
673
674                gizmos.draw_line(point, projection, line_color);
675
676                #[cfg(feature = "2d")]
677                {
678                    gizmos.circle_2d(
679                        projection.f32(),
680                        0.1 * **length_unit as f32,
681                        projection_color,
682                    );
683                    gizmos.circle_2d(point.f32(), 0.1 * **length_unit as f32, origin_color);
684                }
685
686                #[cfg(feature = "3d")]
687                {
688                    gizmos.sphere(
689                        projection.f32(),
690                        0.1 * **length_unit as f32,
691                        projection_color,
692                    );
693                    gizmos.sphere(point.f32(), 0.1 * **length_unit as f32, origin_color);
694                }
695            }
696            TrackedSpatialQuery::ShapeIntersections {
697                shape,
698                position,
699                rotation,
700                hits,
701            } => {
702                let shape_color = config.shape_intersection_shape_color.unwrap_or(Color::NONE);
703
704                gizmos.draw_collider(&shape, position, rotation, shape_color);
705
706                if config.shape_intersection_color.is_some() {
707                    shape_intersections.extend(hits);
708                }
709            }
710        }
711    }
712}
713
714type MeshVisibilityQueryFilter = (
715    With<RigidBody>,
716    Or<(Changed<DebugRender>, Without<DebugRender>)>,
717);
718
719fn change_mesh_visibility(
720    mut meshes: Query<(&mut Visibility, Option<&DebugRender>), MeshVisibilityQueryFilter>,
721    store: Res<GizmoConfigStore>,
722) {
723    let config = store.config::<PhysicsGizmos>();
724    if store.is_changed() {
725        for (mut visibility, render_config) in &mut meshes {
726            let hide_mesh =
727                config.0.enabled && render_config.map_or(config.1.hide_meshes, |c| c.hide_mesh);
728            if hide_mesh {
729                *visibility = Visibility::Hidden;
730            } else {
731                *visibility = Visibility::Inherited;
732            }
733        }
734    }
735}