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