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