1#![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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
60#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
61#[cfg_attr(feature = "2d", doc = " Collider::circle(0.5),")]
87#[cfg_attr(feature = "3d", doc = " Collider::sphere(0.5),")]
88#[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 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 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 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 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 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 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 if contact.penetration <= f32::EPSILON {
378 continue;
379 }
380
381 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 if let Some(color) = config.contact_normal_color {
395 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
416pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
420 type Context: SystemParam;
422
423 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
434pub 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 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 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 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 island.body_count == 0 {
544 continue;
545 }
546
547 let mut body = island.head_body;
548 let mut aabb: Option<ColliderAabb> = None;
549
550 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 #[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
592fn 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 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 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}