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.finish();
463 app
464 }
465
466 fn get_computed_mass_properties(
467 world: &mut World,
468 entity: Entity,
469 ) -> (
470 &ComputedMass,
471 &ComputedAngularInertia,
472 &ComputedCenterOfMass,
473 ) {
474 let mut query = world.query::<(
475 &ComputedMass,
476 &ComputedAngularInertia,
477 &ComputedCenterOfMass,
478 )>();
479 query.get(world, entity).unwrap()
480 }
481
482 #[test]
483 fn mass_properties_zero_by_default() {
484 let mut app = create_app();
487
488 let body_entity = app.world_mut().spawn(RigidBody::Dynamic).id();
489
490 app.world_mut().run_schedule(FixedPostUpdate);
491
492 let (mass, angular_inertia, center_of_mass) =
493 get_computed_mass_properties(app.world_mut(), body_entity);
494
495 assert_eq!(*mass, ComputedMass::default());
496 assert_eq!(*angular_inertia, ComputedAngularInertia::default());
497 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
498 }
499
500 #[test]
501 fn mass_properties_rb_collider() {
502 let mut app = create_app();
505
506 let collider = Collider::circle(1.0);
507 let collider_mass_props = collider.mass_properties(1.0);
508
509 let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
510
511 app.world_mut().run_schedule(FixedPostUpdate);
512
513 let (mass, angular_inertia, center_of_mass) =
514 get_computed_mass_properties(app.world_mut(), body_entity);
515
516 assert_eq!(mass.value() as f32, collider_mass_props.mass);
517 assert_eq!(
518 angular_inertia.value() as f32,
519 collider_mass_props.angular_inertia
520 );
521 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
522 }
523
524 #[test]
525 fn mass_properties_rb_collider_with_set_mass() {
526 let mut app = create_app();
529
530 let collider = Collider::circle(1.0);
531 let collider_mass_props = collider.mass_properties(1.0);
532
533 let body_entity = app
534 .world_mut()
535 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
536 .id();
537
538 app.world_mut().run_schedule(FixedPostUpdate);
539
540 let (mass, angular_inertia, center_of_mass) =
541 get_computed_mass_properties(app.world_mut(), body_entity);
542
543 assert_eq!(mass.value() as f32, 5.0);
544 assert_eq!(
545 angular_inertia.value() as f32,
546 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
547 );
548 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
549 }
550
551 #[test]
552 fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
553 let mut app = create_app();
556
557 let collider = Collider::circle(1.0);
558
559 let body_entity = app
560 .world_mut()
561 .spawn((
562 RigidBody::Dynamic,
563 collider,
564 Mass(5.0),
565 AngularInertia(10.0),
566 ))
567 .id();
568
569 app.world_mut().run_schedule(FixedPostUpdate);
570
571 let (mass, angular_inertia, center_of_mass) =
572 get_computed_mass_properties(app.world_mut(), body_entity);
573
574 assert_eq!(mass.value() as f32, 5.0);
575 assert_eq!(angular_inertia.value() as f32, 10.0);
576 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
577 }
578
579 #[test]
580 fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
581 let mut app = create_app();
585
586 let collider = Collider::circle(1.0);
587 let collider_mass_props = collider.mass_properties(2.0);
588
589 let body_entity = app
590 .world_mut()
591 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
592 .with_child((collider, ColliderDensity(2.0)))
593 .id();
594
595 app.world_mut().run_schedule(FixedPostUpdate);
596
597 let (mass, angular_inertia, center_of_mass) =
598 get_computed_mass_properties(app.world_mut(), body_entity);
599
600 assert_eq!(mass.value() as f32, 5.0 + collider_mass_props.mass);
601 assert_eq!(
602 angular_inertia.value() as f32,
603 5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
604 );
605 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
606 }
607
608 #[test]
609 fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
610 let mut app = create_app();
614
615 let collider = Collider::circle(1.0);
616 let collider_mass_props = collider.mass_properties(1.0);
617
618 let body_entity = app
619 .world_mut()
620 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
621 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
622 .id();
623
624 app.world_mut().run_schedule(FixedPostUpdate);
625
626 let (mass, angular_inertia, center_of_mass) =
627 get_computed_mass_properties(app.world_mut(), body_entity);
628
629 assert_eq!(mass.value() as f32, 5.0 + 10.0);
630 assert_eq!(
631 angular_inertia.value() as f32,
632 5.0 * collider_mass_props.unit_angular_inertia()
633 + 10.0 * collider_mass_props.unit_angular_inertia()
634 );
635 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
636 }
637
638 #[test]
639 fn mass_properties_no_auto_mass_collider_no_set_mass() {
640 let mut app = create_app();
645
646 let body_entity = app
647 .world_mut()
648 .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
649 .id();
650
651 app.world_mut().run_schedule(FixedPostUpdate);
652
653 let (mass, angular_inertia, center_of_mass) =
654 get_computed_mass_properties(app.world_mut(), body_entity);
655
656 assert_eq!(*mass, ComputedMass::default());
657 assert_eq!(*angular_inertia, ComputedAngularInertia::default());
658 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
659 }
660
661 #[test]
662 fn mass_properties_no_auto_mass_hierarchy() {
663 let mut app = create_app();
667
668 let collider = Collider::circle(1.0);
669 let collider_mass_props = collider.mass_properties(1.0);
670
671 let body_entity = app
672 .world_mut()
673 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
674 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
675 .id();
676
677 app.world_mut().run_schedule(FixedPostUpdate);
678
679 let (mass, angular_inertia, center_of_mass) =
680 get_computed_mass_properties(app.world_mut(), body_entity);
681
682 assert_eq!(mass.value() as f32, 5.0);
683 assert_eq!(
684 angular_inertia.value() as f32,
685 mass.value() as f32
686 * (5.0 * collider_mass_props.unit_angular_inertia()
687 + 10.0 * collider_mass_props.unit_angular_inertia())
688 / 15.0
689 );
690 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
691 }
692
693 #[test]
694 fn mass_properties_add_remove_collider() {
695 let mut app = create_app();
704
705 let collider = Collider::circle(1.0);
706 let collider_mass_props = collider.mass_properties(1.0);
707
708 let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
709
710 app.world_mut().run_schedule(FixedPostUpdate);
711
712 let (mass, angular_inertia, center_of_mass) =
713 get_computed_mass_properties(app.world_mut(), body_entity);
714
715 assert_eq!(mass.value() as f32, collider_mass_props.mass);
716 assert_eq!(
717 angular_inertia.value() as f32,
718 collider_mass_props.angular_inertia
719 );
720 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
721
722 let child_collider = Collider::circle(2.0);
724 let child_collider_mass_props = child_collider.mass_properties(1.0);
725
726 let child_entity = app
727 .world_mut()
728 .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
729 .id();
730
731 app.world_mut().run_schedule(FixedPostUpdate);
732
733 let (mass, angular_inertia, center_of_mass) =
734 get_computed_mass_properties(app.world_mut(), body_entity);
735
736 assert_eq!(
737 mass.value() as f32,
738 collider_mass_props.mass + child_collider_mass_props.mass
739 );
740 assert_eq!(
741 angular_inertia.value() as f32,
742 collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
743 );
744 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
745
746 app.world_mut().entity_mut(child_entity).despawn();
748
749 app.world_mut().run_schedule(FixedPostUpdate);
750
751 let (mass, angular_inertia, center_of_mass) =
752 get_computed_mass_properties(app.world_mut(), body_entity);
753
754 assert_eq!(mass.value() as f32, collider_mass_props.mass);
755 assert_eq!(
756 angular_inertia.value() as f32,
757 collider_mass_props.angular_inertia
758 );
759 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
760 }
761
762 #[test]
763 fn mass_properties_change_mass() {
764 let mut app = create_app();
771
772 let collider = Collider::circle(1.0);
773 let collider_mass_props = collider.mass_properties(1.0);
774
775 let body_entity = app
776 .world_mut()
777 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
778 .id();
779
780 app.world_mut().run_schedule(FixedPostUpdate);
781
782 let (mass, angular_inertia, center_of_mass) =
783 get_computed_mass_properties(app.world_mut(), body_entity);
784
785 assert_eq!(mass.value() as f32, 5.0);
786 assert_eq!(
787 angular_inertia.value() as f32,
788 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
789 );
790 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
791
792 app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
794
795 app.world_mut().run_schedule(FixedPostUpdate);
796
797 let (mass, angular_inertia, center_of_mass) =
798 get_computed_mass_properties(app.world_mut(), body_entity);
799
800 assert_eq!(mass.value() as f32, 10.0);
801 assert_eq!(
802 angular_inertia.value() as f32,
803 mass.value() as f32 * collider_mass_props.unit_angular_inertia()
804 );
805 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
806 }
807
808 #[test]
809 fn mass_properties_move_child_collider() {
810 let mut app = create_app();
818
819 let body_entity = app
820 .world_mut()
821 .spawn((
822 RigidBody::Dynamic,
823 Mass(10.0),
824 CenterOfMass::new(0.0, -0.5),
825 Transform::default(),
826 ))
827 .id();
828
829 let child_entity = app
830 .world_mut()
831 .spawn((
832 ChildOf(body_entity),
833 Collider::circle(1.0),
834 Mass(5.0),
835 Transform::default(),
836 ))
837 .id();
838
839 app.world_mut().run_schedule(FixedPostUpdate);
840
841 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
842
843 assert_eq!(mass.value() as f32, 10.0 + 5.0);
844 assert_relative_eq!(
845 center_of_mass.0,
846 Vector::new(0.0, -1.0 / 3.0),
847 epsilon = 1.0e-6
848 );
849
850 app.world_mut()
852 .entity_mut(child_entity)
853 .insert(Transform::from_xyz(0.0, 4.0, 0.0));
854
855 app.world_mut().run_schedule(FixedPostUpdate);
856
857 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
858
859 assert_eq!(mass.value(), 10.0 + 5.0);
860 assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
861 }
862
863 #[test]
864 fn mass_properties_no_auto_mass_add_remove() {
865 let mut app = create_app();
875
876 let collider = Collider::circle(1.0);
877 let collider_mass_props = collider.mass_properties(1.0);
878
879 let body_entity = app
880 .world_mut()
881 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
882 .with_child((collider, Mass(10.0)))
883 .id();
884
885 app.world_mut().run_schedule(FixedPostUpdate);
886
887 let (mass, angular_inertia, center_of_mass) =
888 get_computed_mass_properties(app.world_mut(), body_entity);
889
890 assert_eq!(mass.value() as f32, 5.0 + 10.0);
891 assert_eq!(
892 angular_inertia.value() as f32,
893 5.0 * collider_mass_props.unit_angular_inertia()
894 + 10.0 * collider_mass_props.unit_angular_inertia()
895 );
896 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
897
898 app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
900
901 app.world_mut().run_schedule(FixedPostUpdate);
902
903 let (mass, angular_inertia, center_of_mass) =
904 get_computed_mass_properties(app.world_mut(), body_entity);
905
906 assert_eq!(mass.value() as f32, 5.0);
907 assert_eq!(
908 angular_inertia.value() as f32,
909 5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
910 + 10.0 * collider_mass_props.unit_angular_inertia())
911 / 15.0
912 );
913 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
914
915 app.world_mut()
917 .entity_mut(body_entity)
918 .remove::<NoAutoMass>();
919
920 app.world_mut().run_schedule(FixedPostUpdate);
921
922 let (mass, angular_inertia, center_of_mass) =
923 get_computed_mass_properties(app.world_mut(), body_entity);
924
925 assert_eq!(mass.value() as f32, 5.0 + 10.0);
926 assert_eq!(
927 angular_inertia.value() as f32,
928 5.0 * collider_mass_props.unit_angular_inertia()
929 + 10.0 * collider_mass_props.unit_angular_inertia()
930 );
931 assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
932 }
933}