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