1#![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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
56#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
57#[cfg_attr(feature = "2d", doc = " Collider::circle(0.5),")]
83#[cfg_attr(feature = "3d", doc = " Collider::sphere(0.5),")]
84#[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 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 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 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 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 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 if contact.penetration <= Scalar::EPSILON {
334 continue;
335 }
336
337 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 if let Some(color) = config.contact_normal_color {
351 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
373pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
377 type Context: SystemParam;
379
380 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
391pub 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 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 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 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 island.body_count == 0 {
501 continue;
502 }
503
504 let mut body = island.head_body;
505 let mut aabb: Option<ColliderAabb> = None;
506
507 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 #[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}