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.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 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 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 if contact.penetration <= Scalar::EPSILON {
307 continue;
308 }
309
310 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 if let Some(color) = config.contact_normal_color {
324 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
346pub trait DebugRenderConstraint<const N: usize>: EntityConstraint<N> {
350 type Context: SystemParam;
352
353 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
364pub 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 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 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 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 island.body_count == 0 {
474 continue;
475 }
476
477 let mut body = island.head_body;
478 let mut aabb: Option<ColliderAabb> = None;
479
480 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 #[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}