1#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
42#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
43#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
60#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
61#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
70#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
71#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
80#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
81#![cfg_attr(
86 feature = "2d",
87 doc = "// Total center of mass: (10.0 * [0.0, -0.5] + 5.0 * [0.0, 4.0]) / (10.0 + 5.0) = [0.0, 1.0]"
88)]
89#![cfg_attr(
90 feature = "3d",
91 doc = "// Total center of mass: (10.0 * [0.0, -0.5, 0.0] + 5.0 * [0.0, 4.0, 0.0]) / (10.0 + 5.0) = [0.0, 1.0, 0.0]"
92)]
93#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
98#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
99#![cfg_attr(feature = "2d", doc = " Collider::circle(1.0),")]
103#![cfg_attr(feature = "3d", doc = " Collider::sphere(1.0),")]
104#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
115#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
116#![cfg_attr(feature = "2d", doc = "// Total center of mass: [0.0, -0.5]")]
121#![cfg_attr(feature = "3d", doc = "// Total center of mass: [0.0, -0.5, 0.0]")]
122#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
127#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
128#![cfg_attr(feature = "2d", doc = " Collider::circle(1.0),")]
134#![cfg_attr(feature = "3d", doc = " Collider::sphere(1.0),")]
135#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
148#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
149#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
168#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
169#![cfg_attr(feature = "2d", doc = "let shape = Collider::circle(0.5);")]
174#![cfg_attr(feature = "3d", doc = "let shape = Collider::sphere(0.5);")]
175#![cfg_attr(feature = "2d", doc = "let shape = Circle::new(0.5);")]
184#![cfg_attr(feature = "3d", doc = "let shape = Sphere::new(0.5);")]
185use crate::physics_transform::PhysicsTransformSystems;
200use crate::prelude::*;
201use bevy::{
202 ecs::{intern::Interned, schedule::ScheduleLabel},
203 prelude::*,
204};
205
206pub mod components;
207use components::RecomputeMassProperties;
208
209mod system_param;
210pub use system_param::MassPropertyHelper;
211
212pub use bevy_heavy;
214
215#[cfg(feature = "2d")]
216pub(crate) use bevy_heavy::{
217 ComputeMassProperties2d as ComputeMassProperties, MassProperties2d as MassProperties,
218};
219#[cfg(feature = "3d")]
220pub(crate) use bevy_heavy::{
221 ComputeMassProperties3d as ComputeMassProperties, MassProperties3d as MassProperties,
222};
223
224pub trait MassPropertiesExt {
226 fn to_bundle(&self) -> MassPropertiesBundle;
229}
230
231impl MassPropertiesExt for MassProperties {
232 fn to_bundle(&self) -> MassPropertiesBundle {
233 #[cfg(feature = "2d")]
234 let angular_inertia = AngularInertia(self.angular_inertia);
235 #[cfg(feature = "3d")]
236 let angular_inertia = AngularInertia::new_with_local_frame(
237 self.principal_angular_inertia.f32(),
238 self.local_inertial_frame.f32(),
239 );
240
241 MassPropertiesBundle {
242 mass: Mass(self.mass),
243 angular_inertia,
244 center_of_mass: CenterOfMass(self.center_of_mass),
245 }
246 }
247}
248
249pub struct MassPropertyPlugin {
257 schedule: Interned<dyn ScheduleLabel>,
258}
259
260impl MassPropertyPlugin {
261 pub fn new(schedule: impl ScheduleLabel) -> Self {
265 Self {
266 schedule: schedule.intern(),
267 }
268 }
269}
270
271impl Default for MassPropertyPlugin {
272 fn default() -> Self {
273 Self::new(FixedPostUpdate)
274 }
275}
276
277impl Plugin for MassPropertyPlugin {
278 fn build(&self, app: &mut App) {
279 app.register_required_components::<RigidBody, RecomputeMassProperties>();
282
283 app.add_observer(
285 |trigger: On<Add, RigidBody>, mut mass_helper: MassPropertyHelper| {
286 mass_helper.update_mass_properties(trigger.entity);
287 },
288 );
289
290 app.add_observer(
293 |trigger: On<Insert, RigidBodyColliders>, mut mass_helper: MassPropertyHelper| {
294 mass_helper.update_mass_properties(trigger.entity);
295 },
296 );
297
298 app.configure_sets(
300 self.schedule,
301 (
302 MassPropertySystems::UpdateColliderMassProperties,
303 MassPropertySystems::QueueRecomputation,
304 MassPropertySystems::UpdateComputedMassProperties,
305 )
306 .chain()
307 .in_set(PhysicsSystems::Prepare)
308 .after(PhysicsTransformSystems::TransformToPosition),
309 );
310
311 app.add_systems(
313 self.schedule,
314 (
315 queue_mass_recomputation_on_mass_change,
316 queue_mass_recomputation_on_collider_mass_change,
317 )
318 .in_set(MassPropertySystems::QueueRecomputation),
319 );
320
321 app.add_systems(
323 self.schedule,
324 (update_mass_properties, warn_invalid_mass)
325 .chain()
326 .in_set(MassPropertySystems::UpdateComputedMassProperties),
327 );
328 }
329}
330
331#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
333pub enum MassPropertySystems {
334 UpdateColliderMassProperties,
336 QueueRecomputation,
338 UpdateComputedMassProperties,
341}
342
343pub type WithComputedMassProperty = Or<(
345 With<ComputedMass>,
346 With<ComputedAngularInertia>,
347 With<ComputedCenterOfMass>,
348)>;
349
350pub type MassPropertyChanged = Or<(
352 Changed<Mass>,
353 Changed<AngularInertia>,
354 Changed<CenterOfMass>,
355)>;
356
357fn queue_mass_recomputation_on_mass_change(
363 mut commands: Commands,
364 mut query: Query<
365 Entity,
366 (
367 WithComputedMassProperty,
368 Without<ColliderOf>,
369 MassPropertyChanged,
370 ),
371 >,
372) {
373 for entity in &mut query {
374 commands.entity(entity).insert(RecomputeMassProperties);
375 }
376}
377
378fn queue_mass_recomputation_on_collider_mass_change(
381 mut commands: Commands,
382 mut query: Query<
383 &ColliderOf,
384 Or<(
385 Changed<ColliderMassProperties>,
386 Changed<ColliderTransform>,
387 MassPropertyChanged,
388 )>,
389 >,
390) {
391 for &ColliderOf { body } in &mut query {
392 if let Ok(mut entity_commands) = commands.get_entity(body) {
393 entity_commands.insert(RecomputeMassProperties);
394 }
395 }
396}
397
398fn update_mass_properties(
399 mut commands: Commands,
400 query: Query<Entity, With<RecomputeMassProperties>>,
401 mut mass_helper: MassPropertyHelper,
402) {
403 for entity in query.iter() {
405 mass_helper.update_mass_properties(entity);
406 commands.entity(entity).remove::<RecomputeMassProperties>();
407 }
408}
409
410#[cfg(feature = "default-collider")]
411type ShouldWarn = (
412 Without<ColliderConstructor>,
413 Without<ColliderConstructorHierarchy>,
414);
415
416#[cfg(not(feature = "default-collider"))]
417type ShouldWarn = ();
418
419fn warn_invalid_mass(
421 mut bodies: Query<
422 (
423 Entity,
424 &RigidBody,
425 Ref<ComputedMass>,
426 Ref<ComputedAngularInertia>,
427 ),
428 (
429 Or<(Changed<ComputedMass>, Changed<ComputedAngularInertia>)>,
430 ShouldWarn,
431 ),
432 >,
433) {
434 for (entity, rb, mass, inertia) in &mut bodies {
435 let is_mass_valid = mass.is_finite();
436 #[cfg(feature = "2d")]
437 let is_inertia_valid = inertia.is_finite();
438 #[cfg(feature = "3d")]
439 let is_inertia_valid = inertia.is_finite();
440
441 if rb.is_dynamic() && !(is_mass_valid && is_inertia_valid) {
443 warn!(
444 "Dynamic rigid body {:?} has no mass or inertia. This can cause NaN values. Consider adding a `MassPropertiesBundle` or a `Collider` with mass.",
445 entity
446 );
447 }
448 }
449}
450
451#[cfg(test)]
452#[cfg(all(feature = "2d", feature = "default-collider"))]
453#[allow(clippy::unnecessary_cast)]
454mod tests {
455 use approx::assert_relative_eq;
456
457 use super::*;
458
459 fn create_app() -> App {
460 let mut app = App::new();
461 app.add_plugins((MinimalPlugins, PhysicsPlugins::default(), TransformPlugin));
462 app
463 }
464
465 fn get_computed_mass_properties(
466 world: &mut World,
467 entity: Entity,
468 ) -> (
469 &ComputedMass,
470 &ComputedAngularInertia,
471 &ComputedCenterOfMass,
472 ) {
473 let mut query = world.query::<(
474 &ComputedMass,
475 &ComputedAngularInertia,
476 &ComputedCenterOfMass,
477 )>();
478 query.get(world, entity).unwrap()
479 }
480
481 #[test]
482 fn mass_properties_zero_by_default() {
483 let mut app = create_app();
486
487 let body_entity = app.world_mut().spawn(RigidBody::Dynamic).id();
488
489 app.world_mut().run_schedule(FixedPostUpdate);
490
491 let (mass, angular_inertia, center_of_mass) =
492 get_computed_mass_properties(app.world_mut(), body_entity);
493
494 assert_eq!(*mass, ComputedMass::default());
495 assert_eq!(*angular_inertia, ComputedAngularInertia::default());
496 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
497 }
498
499 #[test]
500 fn mass_properties_rb_collider() {
501 let mut app = create_app();
504
505 let collider = Collider::circle(1.0);
506 let collider_mass_props = collider.mass_properties(1.0);
507
508 let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
509
510 app.world_mut().run_schedule(FixedPostUpdate);
511
512 let (mass, angular_inertia, center_of_mass) =
513 get_computed_mass_properties(app.world_mut(), body_entity);
514
515 assert_eq!(mass.value() as f32, collider_mass_props.mass);
516 assert_eq!(
517 angular_inertia.value() as f32,
518 collider_mass_props.angular_inertia
519 );
520 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
521 }
522
523 #[test]
524 fn mass_properties_rb_collider_with_set_mass() {
525 let mut app = create_app();
528
529 let collider = Collider::circle(1.0);
530 let collider_mass_props = collider.mass_properties(1.0);
531
532 let body_entity = app
533 .world_mut()
534 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
535 .id();
536
537 app.world_mut().run_schedule(FixedPostUpdate);
538
539 let (mass, angular_inertia, center_of_mass) =
540 get_computed_mass_properties(app.world_mut(), body_entity);
541
542 assert_eq!(mass.value() as f32, 5.0);
543 assert_eq!(
544 angular_inertia.value() as f32,
545 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
546 );
547 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
548 }
549
550 #[test]
551 fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
552 let mut app = create_app();
555
556 let collider = Collider::circle(1.0);
557
558 let body_entity = app
559 .world_mut()
560 .spawn((
561 RigidBody::Dynamic,
562 collider,
563 Mass(5.0),
564 AngularInertia(10.0),
565 ))
566 .id();
567
568 app.world_mut().run_schedule(FixedPostUpdate);
569
570 let (mass, angular_inertia, center_of_mass) =
571 get_computed_mass_properties(app.world_mut(), body_entity);
572
573 assert_eq!(mass.value() as f32, 5.0);
574 assert_eq!(angular_inertia.value() as f32, 10.0);
575 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
576 }
577
578 #[test]
579 fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
580 let mut app = create_app();
584
585 let collider = Collider::circle(1.0);
586 let collider_mass_props = collider.mass_properties(2.0);
587
588 let body_entity = app
589 .world_mut()
590 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
591 .with_child((collider, ColliderDensity(2.0)))
592 .id();
593
594 app.world_mut().run_schedule(FixedPostUpdate);
595
596 let (mass, angular_inertia, center_of_mass) =
597 get_computed_mass_properties(app.world_mut(), body_entity);
598
599 assert_eq!(mass.value() as f32, 5.0 + collider_mass_props.mass);
600 assert_eq!(
601 angular_inertia.value() as f32,
602 5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
603 );
604 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
605 }
606
607 #[test]
608 fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
609 let mut app = create_app();
613
614 let collider = Collider::circle(1.0);
615 let collider_mass_props = collider.mass_properties(1.0);
616
617 let body_entity = app
618 .world_mut()
619 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
620 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
621 .id();
622
623 app.world_mut().run_schedule(FixedPostUpdate);
624
625 let (mass, angular_inertia, center_of_mass) =
626 get_computed_mass_properties(app.world_mut(), body_entity);
627
628 assert_eq!(mass.value() as f32, 5.0 + 10.0);
629 assert_eq!(
630 angular_inertia.value() as f32,
631 5.0 * collider_mass_props.unit_angular_inertia()
632 + 10.0 * collider_mass_props.unit_angular_inertia()
633 );
634 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
635 }
636
637 #[test]
638 fn mass_properties_no_auto_mass_collider_no_set_mass() {
639 let mut app = create_app();
644
645 let body_entity = app
646 .world_mut()
647 .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
648 .id();
649
650 app.world_mut().run_schedule(FixedPostUpdate);
651
652 let (mass, angular_inertia, center_of_mass) =
653 get_computed_mass_properties(app.world_mut(), body_entity);
654
655 assert_eq!(*mass, ComputedMass::default());
656 assert_eq!(*angular_inertia, ComputedAngularInertia::default());
657 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
658 }
659
660 #[test]
661 fn mass_properties_no_auto_mass_hierarchy() {
662 let mut app = create_app();
666
667 let collider = Collider::circle(1.0);
668 let collider_mass_props = collider.mass_properties(1.0);
669
670 let body_entity = app
671 .world_mut()
672 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
673 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
674 .id();
675
676 app.world_mut().run_schedule(FixedPostUpdate);
677
678 let (mass, angular_inertia, center_of_mass) =
679 get_computed_mass_properties(app.world_mut(), body_entity);
680
681 assert_eq!(mass.value() as f32, 5.0);
682 assert_eq!(
683 angular_inertia.value() as f32,
684 mass.value() as f32
685 * (5.0 * collider_mass_props.unit_angular_inertia()
686 + 10.0 * collider_mass_props.unit_angular_inertia())
687 / 15.0
688 );
689 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
690 }
691
692 #[test]
693 fn mass_properties_add_remove_collider() {
694 let mut app = create_app();
703
704 let collider = Collider::circle(1.0);
705 let collider_mass_props = collider.mass_properties(1.0);
706
707 let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
708
709 app.world_mut().run_schedule(FixedPostUpdate);
710
711 let (mass, angular_inertia, center_of_mass) =
712 get_computed_mass_properties(app.world_mut(), body_entity);
713
714 assert_eq!(mass.value() as f32, collider_mass_props.mass);
715 assert_eq!(
716 angular_inertia.value() as f32,
717 collider_mass_props.angular_inertia
718 );
719 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
720
721 let child_collider = Collider::circle(2.0);
723 let child_collider_mass_props = child_collider.mass_properties(1.0);
724
725 let child_entity = app
726 .world_mut()
727 .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
728 .id();
729
730 app.world_mut().run_schedule(FixedPostUpdate);
731
732 let (mass, angular_inertia, center_of_mass) =
733 get_computed_mass_properties(app.world_mut(), body_entity);
734
735 assert_eq!(
736 mass.value() as f32,
737 collider_mass_props.mass + child_collider_mass_props.mass
738 );
739 assert_eq!(
740 angular_inertia.value() as f32,
741 collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
742 );
743 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
744
745 app.world_mut().entity_mut(child_entity).despawn();
747
748 app.world_mut().run_schedule(FixedPostUpdate);
749
750 let (mass, angular_inertia, center_of_mass) =
751 get_computed_mass_properties(app.world_mut(), body_entity);
752
753 assert_eq!(mass.value() as f32, collider_mass_props.mass);
754 assert_eq!(
755 angular_inertia.value() as f32,
756 collider_mass_props.angular_inertia
757 );
758 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
759 }
760
761 #[test]
762 fn mass_properties_change_mass() {
763 let mut app = create_app();
770
771 let collider = Collider::circle(1.0);
772 let collider_mass_props = collider.mass_properties(1.0);
773
774 let body_entity = app
775 .world_mut()
776 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
777 .id();
778
779 app.world_mut().run_schedule(FixedPostUpdate);
780
781 let (mass, angular_inertia, center_of_mass) =
782 get_computed_mass_properties(app.world_mut(), body_entity);
783
784 assert_eq!(mass.value() as f32, 5.0);
785 assert_eq!(
786 angular_inertia.value() as f32,
787 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
788 );
789 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
790
791 app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
793
794 app.world_mut().run_schedule(FixedPostUpdate);
795
796 let (mass, angular_inertia, center_of_mass) =
797 get_computed_mass_properties(app.world_mut(), body_entity);
798
799 assert_eq!(mass.value() as f32, 10.0);
800 assert_eq!(
801 angular_inertia.value() as f32,
802 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
803 );
804 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
805 }
806
807 #[test]
808 fn mass_properties_move_child_collider() {
809 let mut app = create_app();
817
818 let body_entity = app
819 .world_mut()
820 .spawn((
821 RigidBody::Dynamic,
822 Mass(10.0),
823 CenterOfMass::new(0.0, -0.5),
824 Transform::default(),
825 ))
826 .id();
827
828 let child_entity = app
829 .world_mut()
830 .spawn((
831 ChildOf(body_entity),
832 Collider::circle(1.0),
833 Mass(5.0),
834 Transform::default(),
835 ))
836 .id();
837
838 app.world_mut().run_schedule(FixedPostUpdate);
839
840 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
841
842 assert_eq!(mass.value() as f32, 10.0 + 5.0);
843 assert_relative_eq!(
844 center_of_mass.0,
845 Vector::new(0.0, -1.0 / 3.0),
846 epsilon = 1.0e-6
847 );
848
849 app.world_mut()
851 .entity_mut(child_entity)
852 .insert(Transform::from_xyz(0.0, 4.0, 0.0));
853
854 app.world_mut().run_schedule(FixedPostUpdate);
855
856 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
857
858 assert_eq!(mass.value(), 10.0 + 5.0);
859 assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
860 }
861
862 #[test]
863 fn mass_properties_no_auto_mass_add_remove() {
864 let mut app = create_app();
874
875 let collider = Collider::circle(1.0);
876 let collider_mass_props = collider.mass_properties(1.0);
877
878 let body_entity = app
879 .world_mut()
880 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
881 .with_child((collider, Mass(10.0)))
882 .id();
883
884 app.world_mut().run_schedule(FixedPostUpdate);
885
886 let (mass, angular_inertia, center_of_mass) =
887 get_computed_mass_properties(app.world_mut(), body_entity);
888
889 assert_eq!(mass.value() as f32, 5.0 + 10.0);
890 assert_eq!(
891 angular_inertia.value() as f32,
892 5.0 * collider_mass_props.unit_angular_inertia()
893 + 10.0 * collider_mass_props.unit_angular_inertia()
894 );
895 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
896
897 app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
899
900 app.world_mut().run_schedule(FixedPostUpdate);
901
902 let (mass, angular_inertia, center_of_mass) =
903 get_computed_mass_properties(app.world_mut(), body_entity);
904
905 assert_eq!(mass.value() as f32, 5.0);
906 assert_eq!(
907 angular_inertia.value() as f32,
908 5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
909 + 10.0 * collider_mass_props.unit_angular_inertia())
910 / 15.0
911 );
912 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
913
914 app.world_mut()
916 .entity_mut(body_entity)
917 .remove::<NoAutoMass>();
918
919 app.world_mut().run_schedule(FixedPostUpdate);
920
921 let (mass, angular_inertia, center_of_mass) =
922 get_computed_mass_properties(app.world_mut(), body_entity);
923
924 assert_eq!(mass.value() as f32, 5.0 + 10.0);
925 assert_eq!(
926 angular_inertia.value() as f32,
927 5.0 * collider_mass_props.unit_angular_inertia()
928 + 10.0 * collider_mass_props.unit_angular_inertia()
929 );
930 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
931 }
932}