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;
9
10pub use configuration::*;
11pub use gizmos::*;
12
13use crate::{
14    dynamics::{
15        joints::EntityConstraint,
16        solver::islands::{BodyIslandNode, PhysicsIslands},
17    },
18    prelude::*,
19};
20use bevy::{
21    camera::visibility::VisibilitySystems,
22    ecs::{
23        query::Has,
24        system::{StaticSystemParam, SystemParam, SystemParamItem},
25    },
26    prelude::*,
27};
28
29/// A plugin that renders physics objects and properties for debugging purposes.
30/// It is not enabled by default and must be added manually.
31///
32/// Currently, the following are supported for debug rendering:
33///
34/// - The axes and center of mass of [rigid bodies](RigidBody)
35/// - [AABBs](ColliderAabb)
36/// - [Collider] wireframes
37/// - Using different colors for [sleeping](Sleeping) bodies
38/// - [Contacts](ContactPair)
39/// - [Joints](dynamics::joints)
40/// - [`RayCaster`]
41/// - [`ShapeCaster`]
42/// - [Simulation islands](dynamics::solver::islands)
43/// - Changing the visibility of entities to only show debug rendering
44///
45/// By default, [AABBs](ColliderAabb) and [contacts](ContactPair) are not debug rendered.
46/// You can configure the [`PhysicsGizmos`] retrieved from `GizmoConfigStore` for the global configuration
47/// and the [`DebugRender`] component for entity-level configuration.
48///
49/// # Example
50///
51/// ```no_run
52#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
53#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
54/// use bevy::prelude::*;
55///
56/// fn main() {
57///     App::new()
58///         .add_plugins((
59///             DefaultPlugins,
60///             PhysicsPlugins::default(),
61///             // Enables debug rendering
62///             PhysicsDebugPlugin,
63///         ))
64///         // Overwrite default debug rendering configuration (optional)
65///         .insert_gizmo_config(
66///             PhysicsGizmos {
67///                 aabb_color: Some(Color::WHITE),
68///                 ..default()
69///             },
70///             GizmoConfig::default(),
71///         )
72///         .run();
73/// }
74///
75/// fn setup(mut commands: Commands) {
76///     // This rigid body and its collider and AABB will get rendered
77///     commands.spawn((
78///         RigidBody::Dynamic,
79#[cfg_attr(feature = "2d", doc = "        Collider::circle(0.5),")]
80#[cfg_attr(feature = "3d", doc = "        Collider::sphere(0.5),")]
81///         // Overwrite default collider color (optional)
82///         DebugRender::default().with_collider_color(Color::srgb(1.0, 0.0, 0.0)),
83///     ));
84/// }
85/// ```
86#[derive(Default)]
87pub struct PhysicsDebugPlugin;
88
89impl Plugin for PhysicsDebugPlugin {
90    fn build(&self, app: &mut App) {
91        app.init_gizmo_group::<PhysicsGizmos>();
92
93        let mut store = app.world_mut().resource_mut::<GizmoConfigStore>();
94        let config = store.config_mut::<PhysicsGizmos>().0;
95        #[cfg(feature = "2d")]
96        {
97            config.line.width = 2.0;
98        }
99        #[cfg(feature = "3d")]
100        {
101            config.line.width = 1.5;
102        }
103
104        app.add_systems(
105            PostUpdate,
106            (
107                debug_render_axes,
108                debug_render_aabbs,
109                #[cfg(all(
110                    feature = "default-collider",
111                    any(feature = "parry-f32", feature = "parry-f64")
112                ))]
113                debug_render_colliders,
114                debug_render_contacts,
115                // TODO: Refactor joints to allow iterating over all of them without generics
116                debug_render_constraint::<FixedJoint, 2>,
117                debug_render_constraint::<PrismaticJoint, 2>,
118                debug_render_constraint::<DistanceJoint, 2>,
119                debug_render_constraint::<RevoluteJoint, 2>,
120                #[cfg(feature = "3d")]
121                debug_render_constraint::<SphericalJoint, 2>,
122                debug_render_raycasts,
123                #[cfg(all(
124                    feature = "default-collider",
125                    any(feature = "parry-f32", feature = "parry-f64")
126                ))]
127                debug_render_shapecasts,
128                debug_render_islands.run_if(resource_exists::<PhysicsIslands>),
129            )
130                .after(TransformSystems::Propagate)
131                .run_if(|store: Res<GizmoConfigStore>| store.config::<PhysicsGizmos>().0.enabled),
132        )
133        .add_systems(
134            PostUpdate,
135            change_mesh_visibility.before(VisibilitySystems::CalculateBounds),
136        );
137    }
138}
139
140#[allow(clippy::type_complexity)]
141fn debug_render_axes(
142    bodies: Query<(
143        &GlobalTransform,
144        &ComputedCenterOfMass,
145        Has<Sleeping>,
146        Option<&DebugRender>,
147    )>,
148    mut gizmos: Gizmos<PhysicsGizmos>,
149    store: Res<GizmoConfigStore>,
150    length_unit: Res<PhysicsLengthUnit>,
151) {
152    let config = store.config::<PhysicsGizmos>().1;
153    for (transform, local_com, sleeping, render_config) in &bodies {
154        let pos = Position::from(transform);
155        let rot = Rotation::from(transform);
156
157        // If the body is sleeping, the colors will be multiplied by the sleeping color multiplier
158        if let Some(mut lengths) = render_config.map_or(config.axis_lengths, |c| c.axis_lengths) {
159            lengths *= length_unit.0;
160
161            let mul = if sleeping {
162                render_config
163                    .map_or(config.sleeping_color_multiplier, |c| {
164                        c.sleeping_color_multiplier
165                    })
166                    .unwrap_or([1.0; 4])
167            } else {
168                [1.0; 4]
169            };
170            let [x_color, y_color, _z_color] = [
171                Color::hsla(0.0, 1.0 * mul[1], 0.5 * mul[2], 1.0 * mul[3]),
172                Color::hsla(120.0 * mul[0], 1.0 * mul[1], 0.4 * mul[2], 1.0 * mul[3]),
173                Color::hsla(220.0 * mul[0], 1.0 * mul[1], 0.6 * mul[2], 1.0 * mul[3]),
174            ];
175            let global_com = pos.0 + rot * local_com.0;
176
177            let x = rot * (Vector::X * lengths.x);
178            gizmos.draw_line(global_com - x, global_com + x, x_color);
179
180            let y = rot * (Vector::Y * lengths.y);
181            gizmos.draw_line(global_com - y, global_com + y, y_color);
182
183            #[cfg(feature = "3d")]
184            {
185                let z = rot * (Vector::Z * lengths.z);
186                gizmos.draw_line(global_com - z, global_com + z, _z_color);
187            }
188        }
189    }
190}
191
192fn debug_render_aabbs(
193    aabbs: Query<(
194        Entity,
195        &ColliderAabb,
196        Option<&ColliderOf>,
197        Option<&DebugRender>,
198    )>,
199    sleeping: Query<(), With<Sleeping>>,
200    mut gizmos: Gizmos<PhysicsGizmos>,
201    store: Res<GizmoConfigStore>,
202) {
203    let config = store.config::<PhysicsGizmos>().1;
204    #[cfg(feature = "2d")]
205    for (entity, aabb, collider_rb, render_config) in &aabbs {
206        if let Some(mut color) = render_config.map_or(config.aabb_color, |c| c.aabb_color) {
207            let collider_rb = collider_rb.map_or(entity, |c| c.body);
208
209            // If the body is sleeping, multiply the color by the sleeping color multiplier
210            if sleeping.contains(collider_rb) {
211                let hsla = Hsla::from(color).to_vec4();
212                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
213                    c.sleeping_color_multiplier
214                }) {
215                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
216                }
217            }
218
219            gizmos.rect_2d(aabb.center().f32(), aabb.size().f32(), color);
220        }
221    }
222
223    #[cfg(feature = "3d")]
224    for (entity, aabb, collider_rb, render_config) in &aabbs {
225        if let Some(mut color) = render_config.map_or(config.aabb_color, |c| c.aabb_color) {
226            use bevy_math::bounding::Aabb3d;
227
228            let collider_rb = collider_rb.map_or(entity, |c| c.body);
229
230            // If the body is sleeping, multiply the color by the sleeping color multiplier
231            if sleeping.contains(collider_rb) {
232                let hsla = Hsla::from(color).to_vec4();
233                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
234                    c.sleeping_color_multiplier
235                }) {
236                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
237                }
238            }
239
240            gizmos.aabb_3d(
241                Aabb3d {
242                    min: Vec3A::from(aabb.min.f32()),
243                    max: Vec3A::from(aabb.max.f32()),
244                },
245                Transform::IDENTITY,
246                color,
247            );
248        }
249    }
250}
251
252#[cfg(all(
253    feature = "default-collider",
254    any(feature = "parry-f32", feature = "parry-f64")
255))]
256#[allow(clippy::type_complexity)]
257fn debug_render_colliders(
258    mut colliders: Query<(
259        Entity,
260        &Collider,
261        &GlobalTransform,
262        Option<&ColliderOf>,
263        Option<&DebugRender>,
264    )>,
265    sleeping: Query<(), With<Sleeping>>,
266    mut gizmos: Gizmos<PhysicsGizmos>,
267    store: Res<GizmoConfigStore>,
268) {
269    let config = store.config::<PhysicsGizmos>().1;
270    for (entity, collider, transform, collider_rb, render_config) in &mut colliders {
271        let position = Position::from(transform);
272        let rotation = Rotation::from(transform);
273        if let Some(mut color) = render_config.map_or(config.collider_color, |c| c.collider_color) {
274            let collider_rb = collider_rb.map_or(entity, |c| c.body);
275
276            // If the body is sleeping, multiply the color by the sleeping color multiplier
277            if sleeping.contains(collider_rb) {
278                let hsla = Hsla::from(color).to_vec4();
279                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
280                    c.sleeping_color_multiplier
281                }) {
282                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
283                }
284            }
285            gizmos.draw_collider(collider, position, rotation, color);
286        }
287    }
288}
289
290fn debug_render_contacts(
291    collisions: Collisions,
292    mut gizmos: Gizmos<PhysicsGizmos>,
293    store: Res<GizmoConfigStore>,
294    time: Res<Time<Substeps>>,
295    length_unit: Res<PhysicsLengthUnit>,
296) {
297    let config = store.config::<PhysicsGizmos>().1;
298
299    if config.contact_point_color.is_none() && config.contact_normal_color.is_none() {
300        return;
301    }
302
303    for contacts in collisions.iter() {
304        for manifold in contacts.manifolds.iter() {
305            for contact in manifold.points.iter() {
306                // Don't render contacts that aren't penetrating
307                if contact.penetration <= Scalar::EPSILON {
308                    continue;
309                }
310
311                // Draw contact points
312                if let Some(color) = config.contact_point_color {
313                    #[cfg(feature = "2d")]
314                    {
315                        gizmos.circle_2d(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
316                    }
317                    #[cfg(feature = "3d")]
318                    {
319                        gizmos.sphere(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
320                    }
321                }
322
323                // Draw contact normals
324                if let Some(color) = config.contact_normal_color {
325                    // The length of the normal arrows
326                    let length = length_unit.0
327                        * match config.contact_normal_scale {
328                            ContactGizmoScale::Constant(length) => length,
329                            ContactGizmoScale::Scaled(scale) => {
330                                scale * contact.normal_impulse
331                                    / time.delta_secs_f64().adjust_precision()
332                            }
333                        };
334
335                    gizmos.draw_arrow(
336                        contact.point,
337                        contact.point + manifold.normal * length,
338                        0.1 * length_unit.0,
339                        color,
340                    );
341                }
342            }
343        }
344    }
345}
346
347/// A trait for rendering debug information about constraints.
348///
349/// Add the [`debug_render_constraint`] system to render all constraints that implement this trait.
350pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
351    /// A [`SystemParam`] type for any additional ECS access required for rendering the constraint.
352    type Context: SystemParam;
353
354    /// Renders the debug information for the constraint.
355    fn debug_render(
356        &self,
357        positions: [Vector; N],
358        rotations: [Rotation; N],
359        context: &mut SystemParamItem<Self::Context>,
360        gizmos: &mut Gizmos<PhysicsGizmos>,
361        config: &PhysicsGizmos,
362    );
363}
364
365/// A system that renders all constraints that implement the [`DebugRenderConstraint`] trait.
366pub fn debug_render_constraint<T: Component + DebugRenderConstraint<N>, const N: usize>(
367    bodies: Query<&GlobalTransform>,
368    constraints: Query<&T>,
369    mut gizmos: Gizmos<PhysicsGizmos>,
370    store: Res<GizmoConfigStore>,
371    mut context: StaticSystemParam<T::Context>,
372) {
373    let config = store.config::<PhysicsGizmos>().1;
374    for constraint in &constraints {
375        if let Ok(bodies) = bodies.get_many(constraint.entities()) {
376            let positions: [Vector; N] = bodies
377                .iter()
378                .map(|transform| Position::from(**transform).0)
379                .collect::<Vec<_>>()
380                .try_into()
381                .unwrap();
382            let rotations: [Rotation; N] = bodies
383                .iter()
384                .map(|transform| Rotation::from(**transform))
385                .collect::<Vec<_>>()
386                .try_into()
387                .unwrap();
388
389            constraint.debug_render(positions, rotations, &mut context, &mut gizmos, config);
390        }
391    }
392}
393
394fn debug_render_raycasts(
395    query: Query<(&RayCaster, &RayHits)>,
396    mut gizmos: Gizmos<PhysicsGizmos>,
397    store: Res<GizmoConfigStore>,
398    length_unit: Res<PhysicsLengthUnit>,
399) {
400    let config = store.config::<PhysicsGizmos>().1;
401    for (ray, hits) in &query {
402        let ray_color = config.raycast_color.unwrap_or(Color::NONE);
403        let point_color = config.raycast_point_color.unwrap_or(Color::NONE);
404        let normal_color = config.raycast_normal_color.unwrap_or(Color::NONE);
405
406        gizmos.draw_raycast(
407            ray.global_origin(),
408            ray.global_direction(),
409            // f32::MAX renders nothing, but this number seems to be fine :P
410            ray.max_distance.min(1_000_000_000_000_000_000.0),
411            hits.as_slice(),
412            ray_color,
413            point_color,
414            normal_color,
415            length_unit.0,
416        );
417    }
418}
419
420#[cfg(all(
421    feature = "default-collider",
422    any(feature = "parry-f32", feature = "parry-f64")
423))]
424fn debug_render_shapecasts(
425    query: Query<(&ShapeCaster, &ShapeHits)>,
426    mut gizmos: Gizmos<PhysicsGizmos>,
427    store: Res<GizmoConfigStore>,
428    length_unit: Res<PhysicsLengthUnit>,
429) {
430    let config = store.config::<PhysicsGizmos>().1;
431    for (shape_caster, hits) in &query {
432        let ray_color = config.shapecast_color.unwrap_or(Color::NONE);
433        let shape_color = config.shapecast_shape_color.unwrap_or(Color::NONE);
434        let point_color = config.shapecast_point_color.unwrap_or(Color::NONE);
435        let normal_color = config.shapecast_normal_color.unwrap_or(Color::NONE);
436
437        gizmos.draw_shapecast(
438            &shape_caster.shape,
439            shape_caster.global_origin(),
440            shape_caster.global_shape_rotation(),
441            shape_caster.global_direction(),
442            // f32::MAX renders nothing, but this number seems to be fine :P
443            shape_caster.max_distance.min(1_000_000_000_000_000.0),
444            hits.as_slice(),
445            ray_color,
446            shape_color,
447            point_color,
448            normal_color,
449            length_unit.0,
450        );
451    }
452}
453
454fn debug_render_islands(
455    islands: Res<PhysicsIslands>,
456    bodies: Query<(&RigidBodyColliders, &BodyIslandNode)>,
457    aabbs: Query<&ColliderAabb>,
458    mut gizmos: Gizmos<PhysicsGizmos>,
459    store: Res<GizmoConfigStore>,
460) {
461    let config = store.config::<PhysicsGizmos>().1;
462
463    for island in islands.iter() {
464        if let Some(mut color) = config.island_color {
465            // If the island is sleeping, multiply the color by the sleeping color multiplier
466            if island.is_sleeping {
467                let hsla = Hsla::from(color).to_vec4();
468                if let Some(mul) = config.sleeping_color_multiplier {
469                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
470                }
471            }
472
473            // If the island is empty, skip rendering
474            if island.body_count == 0 {
475                continue;
476            }
477
478            let mut body = island.head_body;
479            let mut aabb: Option<ColliderAabb> = None;
480
481            // Compute the island's AABB by merging the AABBs of all bodies in the island.
482            while let Some(next_body) = body {
483                if let Ok((colliders, body_island)) = bodies.get(next_body) {
484                    for collider in colliders.iter() {
485                        if let Ok(collider_aabb) = aabbs.get(collider) {
486                            aabb = Some(
487                                aabb.map_or(*collider_aabb, |aabb| aabb.merged(*collider_aabb)),
488                            );
489                        }
490                    }
491                    body = body_island.next;
492                } else {
493                    break;
494                }
495            }
496
497            let Some(aabb) = aabb else {
498                continue;
499            };
500
501            // Render the island's AABB.
502            #[cfg(feature = "2d")]
503            {
504                gizmos.rect_2d(aabb.center().f32(), aabb.size().f32(), color);
505            }
506            #[cfg(feature = "3d")]
507            {
508                use bevy_math::bounding::Aabb3d;
509
510                gizmos.aabb_3d(
511                    Aabb3d {
512                        min: Vec3A::from(aabb.min.f32()),
513                        max: Vec3A::from(aabb.max.f32()),
514                    },
515                    Transform::IDENTITY,
516                    color,
517                );
518            }
519        }
520    }
521}
522
523type MeshVisibilityQueryFilter = (
524    With<RigidBody>,
525    Or<(Changed<DebugRender>, Without<DebugRender>)>,
526);
527
528fn change_mesh_visibility(
529    mut meshes: Query<(&mut Visibility, Option<&DebugRender>), MeshVisibilityQueryFilter>,
530    store: Res<GizmoConfigStore>,
531) {
532    let config = store.config::<PhysicsGizmos>();
533    if store.is_changed() {
534        for (mut visibility, render_config) in &mut meshes {
535            let hide_mesh =
536                config.0.enabled && render_config.map_or(config.1.hide_meshes, |c| c.hide_mesh);
537            if hide_mesh {
538                *visibility = Visibility::Hidden;
539            } else {
540                *visibility = Visibility::Visible;
541            }
542        }
543    }
544}