1#![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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
42#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
43#[cfg_attr(feature = "2d", doc = " Collider::circle(0.5),")]
69#[cfg_attr(feature = "3d", doc = " Collider::sphere(0.5),")]
70pub struct PhysicsDebugPlugin {
76 schedule: Interned<dyn ScheduleLabel>,
77}
78
79impl PhysicsDebugPlugin {
80 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 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 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 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 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 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 if contact.penetration <= Scalar::EPSILON {
325 continue;
326 }
327
328 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 if let Some(color) = config.contact_normal_color {
344 let color_dim = color.mix(&Color::BLACK, 0.5);
346
347 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 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 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 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 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}