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
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.cuboid(
220                Transform::from_scale(Vector::from(aabb.size()).extend(0.0).f32())
221                    .with_translation(Vector::from(aabb.center()).extend(0.0).f32()),
222                color,
223            );
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            let collider_rb = collider_rb.map_or(entity, |c| c.body);
231
232            // If the body is sleeping, multiply the color by the sleeping color multiplier
233            if sleeping.contains(collider_rb) {
234                let hsla = Hsla::from(color).to_vec4();
235                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
236                    c.sleeping_color_multiplier
237                }) {
238                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
239                }
240            }
241
242            gizmos.cuboid(
243                Transform::from_scale(Vector::from(aabb.size()).f32())
244                    .with_translation(Vector::from(aabb.center()).f32()),
245                color,
246            );
247        }
248    }
249}
250
251#[cfg(all(
252    feature = "default-collider",
253    any(feature = "parry-f32", feature = "parry-f64")
254))]
255#[allow(clippy::type_complexity)]
256fn debug_render_colliders(
257    mut colliders: Query<(
258        Entity,
259        &Collider,
260        &GlobalTransform,
261        Option<&ColliderOf>,
262        Option<&DebugRender>,
263    )>,
264    sleeping: Query<(), With<Sleeping>>,
265    mut gizmos: Gizmos<PhysicsGizmos>,
266    store: Res<GizmoConfigStore>,
267) {
268    let config = store.config::<PhysicsGizmos>().1;
269    for (entity, collider, transform, collider_rb, render_config) in &mut colliders {
270        let position = Position::from(transform);
271        let rotation = Rotation::from(transform);
272        if let Some(mut color) = render_config.map_or(config.collider_color, |c| c.collider_color) {
273            let collider_rb = collider_rb.map_or(entity, |c| c.body);
274
275            // If the body is sleeping, multiply the color by the sleeping color multiplier
276            if sleeping.contains(collider_rb) {
277                let hsla = Hsla::from(color).to_vec4();
278                if let Some(mul) = render_config.map_or(config.sleeping_color_multiplier, |c| {
279                    c.sleeping_color_multiplier
280                }) {
281                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
282                }
283            }
284            gizmos.draw_collider(collider, position, rotation, color);
285        }
286    }
287}
288
289fn debug_render_contacts(
290    collisions: Collisions,
291    mut gizmos: Gizmos<PhysicsGizmos>,
292    store: Res<GizmoConfigStore>,
293    time: Res<Time<Substeps>>,
294    length_unit: Res<PhysicsLengthUnit>,
295) {
296    let config = store.config::<PhysicsGizmos>().1;
297
298    if config.contact_point_color.is_none() && config.contact_normal_color.is_none() {
299        return;
300    }
301
302    for contacts in collisions.iter() {
303        for manifold in contacts.manifolds.iter() {
304            for contact in manifold.points.iter() {
305                // Don't render contacts that aren't penetrating
306                if contact.penetration <= Scalar::EPSILON {
307                    continue;
308                }
309
310                // Draw contact points
311                if let Some(color) = config.contact_point_color {
312                    #[cfg(feature = "2d")]
313                    {
314                        gizmos.circle_2d(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
315                    }
316                    #[cfg(feature = "3d")]
317                    {
318                        gizmos.sphere(contact.point.f32(), 0.1 * length_unit.0 as f32, color);
319                    }
320                }
321
322                // Draw contact normals
323                if let Some(color) = config.contact_normal_color {
324                    // The length of the normal arrows
325                    let length = length_unit.0
326                        * match config.contact_normal_scale {
327                            ContactGizmoScale::Constant(length) => length,
328                            ContactGizmoScale::Scaled(scale) => {
329                                scale * contact.normal_impulse
330                                    / time.delta_secs_f64().adjust_precision()
331                            }
332                        };
333
334                    gizmos.draw_arrow(
335                        contact.point,
336                        contact.point + manifold.normal * length,
337                        0.1 * length_unit.0,
338                        color,
339                    );
340                }
341            }
342        }
343    }
344}
345
346/// A trait for rendering debug information about constraints.
347///
348/// Add the [`debug_render_constraint`] system to render all constraints that implement this trait.
349pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
350    /// A [`SystemParam`] type for any additional ECS access required for rendering the constraint.
351    type Context: SystemParam;
352
353    /// Renders the debug information for the constraint.
354    fn debug_render(
355        &self,
356        positions: [Vector; N],
357        rotations: [Rotation; N],
358        context: &mut SystemParamItem<Self::Context>,
359        gizmos: &mut Gizmos<PhysicsGizmos>,
360        config: &PhysicsGizmos,
361    );
362}
363
364/// A system that renders all constraints that implement the [`DebugRenderConstraint`] trait.
365pub fn debug_render_constraint<T: Component + DebugRenderConstraint<N>, const N: usize>(
366    bodies: Query<&GlobalTransform>,
367    constraints: Query<&T>,
368    mut gizmos: Gizmos<PhysicsGizmos>,
369    store: Res<GizmoConfigStore>,
370    mut context: StaticSystemParam<T::Context>,
371) {
372    let config = store.config::<PhysicsGizmos>().1;
373    for constraint in &constraints {
374        if let Ok(bodies) = bodies.get_many(constraint.entities()) {
375            let positions: [Vector; N] = bodies
376                .iter()
377                .map(|transform| Position::from(**transform).0)
378                .collect::<Vec<_>>()
379                .try_into()
380                .unwrap();
381            let rotations: [Rotation; N] = bodies
382                .iter()
383                .map(|transform| Rotation::from(**transform))
384                .collect::<Vec<_>>()
385                .try_into()
386                .unwrap();
387
388            constraint.debug_render(positions, rotations, &mut context, &mut gizmos, config);
389        }
390    }
391}
392
393fn debug_render_raycasts(
394    query: Query<(&RayCaster, &RayHits)>,
395    mut gizmos: Gizmos<PhysicsGizmos>,
396    store: Res<GizmoConfigStore>,
397    length_unit: Res<PhysicsLengthUnit>,
398) {
399    let config = store.config::<PhysicsGizmos>().1;
400    for (ray, hits) in &query {
401        let ray_color = config.raycast_color.unwrap_or(Color::NONE);
402        let point_color = config.raycast_point_color.unwrap_or(Color::NONE);
403        let normal_color = config.raycast_normal_color.unwrap_or(Color::NONE);
404
405        gizmos.draw_raycast(
406            ray.global_origin(),
407            ray.global_direction(),
408            // f32::MAX renders nothing, but this number seems to be fine :P
409            ray.max_distance.min(1_000_000_000_000_000_000.0),
410            hits.as_slice(),
411            ray_color,
412            point_color,
413            normal_color,
414            length_unit.0,
415        );
416    }
417}
418
419#[cfg(all(
420    feature = "default-collider",
421    any(feature = "parry-f32", feature = "parry-f64")
422))]
423fn debug_render_shapecasts(
424    query: Query<(&ShapeCaster, &ShapeHits)>,
425    mut gizmos: Gizmos<PhysicsGizmos>,
426    store: Res<GizmoConfigStore>,
427    length_unit: Res<PhysicsLengthUnit>,
428) {
429    let config = store.config::<PhysicsGizmos>().1;
430    for (shape_caster, hits) in &query {
431        let ray_color = config.shapecast_color.unwrap_or(Color::NONE);
432        let shape_color = config.shapecast_shape_color.unwrap_or(Color::NONE);
433        let point_color = config.shapecast_point_color.unwrap_or(Color::NONE);
434        let normal_color = config.shapecast_normal_color.unwrap_or(Color::NONE);
435
436        gizmos.draw_shapecast(
437            &shape_caster.shape,
438            shape_caster.global_origin(),
439            shape_caster.global_shape_rotation(),
440            shape_caster.global_direction(),
441            // f32::MAX renders nothing, but this number seems to be fine :P
442            shape_caster.max_distance.min(1_000_000_000_000_000.0),
443            hits.as_slice(),
444            ray_color,
445            shape_color,
446            point_color,
447            normal_color,
448            length_unit.0,
449        );
450    }
451}
452
453fn debug_render_islands(
454    islands: Res<PhysicsIslands>,
455    bodies: Query<(&RigidBodyColliders, &BodyIslandNode)>,
456    aabbs: Query<&ColliderAabb>,
457    mut gizmos: Gizmos<PhysicsGizmos>,
458    store: Res<GizmoConfigStore>,
459) {
460    let config = store.config::<PhysicsGizmos>().1;
461
462    for island in islands.iter() {
463        if let Some(mut color) = config.island_color {
464            // If the island is sleeping, multiply the color by the sleeping color multiplier
465            if island.is_sleeping {
466                let hsla = Hsla::from(color).to_vec4();
467                if let Some(mul) = config.sleeping_color_multiplier {
468                    color = Hsla::from_vec4(hsla * Vec4::from_array(mul)).into();
469                }
470            }
471
472            // If the island is empty, skip rendering
473            if island.body_count == 0 {
474                continue;
475            }
476
477            let mut body = island.head_body;
478            let mut aabb: Option<ColliderAabb> = None;
479
480            // Compute the island's AABB by merging the AABBs of all bodies in the island.
481            while let Some(next_body) = body {
482                if let Ok((colliders, body_island)) = bodies.get(next_body) {
483                    for collider in colliders.iter() {
484                        if let Ok(collider_aabb) = aabbs.get(collider) {
485                            aabb = Some(
486                                aabb.map_or(*collider_aabb, |aabb| aabb.merged(*collider_aabb)),
487                            );
488                        }
489                    }
490                    body = body_island.next;
491                } else {
492                    break;
493                }
494            }
495
496            let Some(aabb) = aabb else {
497                continue;
498            };
499
500            // Render the island's AABB.
501            #[cfg(feature = "2d")]
502            {
503                gizmos.cuboid(
504                    Transform::from_scale(Vector::from(aabb.size()).extend(0.0).f32())
505                        .with_translation(Vector::from(aabb.center()).extend(0.0).f32()),
506                    color,
507                );
508            }
509            #[cfg(feature = "3d")]
510            {
511                gizmos.cuboid(
512                    Transform::from_scale(Vector::from(aabb.size()).f32())
513                        .with_translation(Vector::from(aabb.center()).f32()),
514                    color,
515                );
516            }
517        }
518    }
519}
520
521type MeshVisibilityQueryFilter = (
522    With<RigidBody>,
523    Or<(Changed<DebugRender>, Without<DebugRender>)>,
524);
525
526fn change_mesh_visibility(
527    mut meshes: Query<(&mut Visibility, Option<&DebugRender>), MeshVisibilityQueryFilter>,
528    store: Res<GizmoConfigStore>,
529) {
530    let config = store.config::<PhysicsGizmos>();
531    if store.is_changed() {
532        for (mut visibility, render_config) in &mut meshes {
533            let hide_mesh =
534                config.0.enabled && render_config.map_or(config.1.hide_meshes, |c| c.hide_mesh);
535            if hide_mesh {
536                *visibility = Visibility::Hidden;
537            } else {
538                *visibility = Visibility::Visible;
539            }
540        }
541    }
542}