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,
238 self.local_inertial_frame,
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_relative_eq!(mass.value(), 0.0);
496 assert_relative_eq!(angular_inertia.value(), SymmetricTensor::default());
497 assert_relative_eq!(center_of_mass.0, 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_relative_eq!(mass.value(), collider_mass_props.mass);
517 assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
518 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
519 }
520
521 #[test]
522 fn mass_properties_rb_collider_with_set_mass() {
523 let mut app = create_app();
526
527 let collider = Collider::circle(1.0);
528 let collider_mass_props = collider.mass_properties(1.0);
529
530 let body_entity = app
531 .world_mut()
532 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
533 .id();
534
535 app.world_mut().run_schedule(FixedPostUpdate);
536
537 let (mass, angular_inertia, center_of_mass) =
538 get_computed_mass_properties(app.world_mut(), body_entity);
539
540 assert_relative_eq!(mass.value(), 5.0);
541 assert_relative_eq!(
542 angular_inertia.value(),
543 mass.value() * collider_mass_props.unit_angular_inertia()
544 );
545 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
546 }
547
548 #[test]
549 fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
550 let mut app = create_app();
553
554 let collider = Collider::circle(1.0);
555
556 let body_entity = app
557 .world_mut()
558 .spawn((
559 RigidBody::Dynamic,
560 collider,
561 Mass(5.0),
562 AngularInertia(10.0),
563 ))
564 .id();
565
566 app.world_mut().run_schedule(FixedPostUpdate);
567
568 let (mass, angular_inertia, center_of_mass) =
569 get_computed_mass_properties(app.world_mut(), body_entity);
570
571 assert_relative_eq!(mass.value(), 5.0);
572 assert_relative_eq!(angular_inertia.value(), 10.0);
573 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
574 }
575
576 #[test]
577 fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
578 let mut app = create_app();
582
583 let collider = Collider::circle(1.0);
584 let collider_mass_props = collider.mass_properties(2.0);
585
586 let body_entity = app
587 .world_mut()
588 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
589 .with_child((collider, ColliderDensity(2.0)))
590 .id();
591
592 app.world_mut().run_schedule(FixedPostUpdate);
593
594 let (mass, angular_inertia, center_of_mass) =
595 get_computed_mass_properties(app.world_mut(), body_entity);
596
597 assert_relative_eq!(mass.value(), 5.0 + collider_mass_props.mass);
598 assert_relative_eq!(
599 angular_inertia.value(),
600 5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
601 );
602 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
603 }
604
605 #[test]
606 fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
607 let mut app = create_app();
611
612 let collider = Collider::circle(1.0);
613 let collider_mass_props = collider.mass_properties(1.0);
614
615 let body_entity = app
616 .world_mut()
617 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
618 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
619 .id();
620
621 app.world_mut().run_schedule(FixedPostUpdate);
622
623 let (mass, angular_inertia, center_of_mass) =
624 get_computed_mass_properties(app.world_mut(), body_entity);
625
626 assert_relative_eq!(mass.value(), 5.0 + 10.0);
627 assert_relative_eq!(
628 angular_inertia.value(),
629 5.0 * collider_mass_props.unit_angular_inertia()
630 + 10.0 * collider_mass_props.unit_angular_inertia()
631 );
632 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
633 }
634
635 #[test]
636 fn mass_properties_no_auto_mass_collider_no_set_mass() {
637 let mut app = create_app();
642
643 let body_entity = app
644 .world_mut()
645 .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
646 .id();
647
648 app.world_mut().run_schedule(FixedPostUpdate);
649
650 let (mass, angular_inertia, center_of_mass) =
651 get_computed_mass_properties(app.world_mut(), body_entity);
652
653 assert_relative_eq!(mass.value(), 0.0);
654 assert_relative_eq!(angular_inertia.value(), SymmetricTensor::default());
655 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
656 }
657
658 #[test]
659 fn mass_properties_no_auto_mass_hierarchy() {
660 let mut app = create_app();
664
665 let collider = Collider::circle(1.0);
666 let collider_mass_props = collider.mass_properties(1.0);
667
668 let body_entity = app
669 .world_mut()
670 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
671 .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
672 .id();
673
674 app.world_mut().run_schedule(FixedPostUpdate);
675
676 let (mass, angular_inertia, center_of_mass) =
677 get_computed_mass_properties(app.world_mut(), body_entity);
678
679 assert_relative_eq!(mass.value(), 5.0);
680 assert_relative_eq!(
681 angular_inertia.value(),
682 mass.value()
683 * (5.0 * collider_mass_props.unit_angular_inertia()
684 + 10.0 * collider_mass_props.unit_angular_inertia())
685 / 15.0
686 );
687 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
688 }
689
690 #[test]
691 fn mass_properties_add_remove_collider() {
692 let mut app = create_app();
701
702 let collider = Collider::circle(1.0);
703 let collider_mass_props = collider.mass_properties(1.0);
704
705 let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
706
707 app.world_mut().run_schedule(FixedPostUpdate);
708
709 let (mass, angular_inertia, center_of_mass) =
710 get_computed_mass_properties(app.world_mut(), body_entity);
711
712 assert_relative_eq!(mass.value(), collider_mass_props.mass);
713 assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
714 assert_relative_eq!(center_of_mass.0, Vector::ZERO);
715
716 let child_collider = Collider::circle(2.0);
718 let child_collider_mass_props = child_collider.mass_properties(1.0);
719
720 let child_entity = app
721 .world_mut()
722 .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
723 .id();
724
725 app.world_mut().run_schedule(FixedPostUpdate);
726
727 let (mass, angular_inertia, center_of_mass) =
728 get_computed_mass_properties(app.world_mut(), body_entity);
729
730 assert_relative_eq!(
731 mass.value(),
732 collider_mass_props.mass + child_collider_mass_props.mass
733 );
734 assert_relative_eq!(
735 angular_inertia.value(),
736 collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
737 );
738 assert_relative_eq!(center_of_mass.0, Vector::ZERO);
739
740 app.world_mut().entity_mut(child_entity).despawn();
742
743 app.world_mut().run_schedule(FixedPostUpdate);
744
745 let (mass, angular_inertia, center_of_mass) =
746 get_computed_mass_properties(app.world_mut(), body_entity);
747
748 assert_relative_eq!(mass.value(), collider_mass_props.mass);
749 assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
750 assert_relative_eq!(center_of_mass.0, Vector::ZERO);
751 }
752
753 #[test]
754 fn mass_properties_change_mass() {
755 let mut app = create_app();
762
763 let collider = Collider::circle(1.0);
764 let collider_mass_props = collider.mass_properties(1.0);
765
766 let body_entity = app
767 .world_mut()
768 .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
769 .id();
770
771 app.world_mut().run_schedule(FixedPostUpdate);
772
773 let (mass, angular_inertia, center_of_mass) =
774 get_computed_mass_properties(app.world_mut(), body_entity);
775
776 assert_relative_eq!(mass.value(), 5.0);
777 assert_relative_eq!(
778 angular_inertia.value(),
779 mass.value() * collider_mass_props.unit_angular_inertia()
780 );
781 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
782
783 app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
785
786 app.world_mut().run_schedule(FixedPostUpdate);
787
788 let (mass, angular_inertia, center_of_mass) =
789 get_computed_mass_properties(app.world_mut(), body_entity);
790
791 assert_relative_eq!(mass.value(), 10.0);
792 assert_relative_eq!(
793 angular_inertia.value(),
794 mass.value() * collider_mass_props.unit_angular_inertia()
795 );
796 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
797 }
798
799 #[test]
800 fn mass_properties_move_child_collider() {
801 let mut app = create_app();
809
810 let body_entity = app
811 .world_mut()
812 .spawn((
813 RigidBody::Dynamic,
814 Mass(10.0),
815 CenterOfMass::new(0.0, -0.5),
816 Transform::default(),
817 ))
818 .id();
819
820 let child_entity = app
821 .world_mut()
822 .spawn((
823 ChildOf(body_entity),
824 Collider::circle(1.0),
825 Mass(5.0),
826 Transform::default(),
827 ))
828 .id();
829
830 app.world_mut().run_schedule(FixedPostUpdate);
831
832 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
833
834 assert_relative_eq!(mass.value(), 10.0 + 5.0);
835 assert_relative_eq!(
836 center_of_mass.0,
837 Vector::new(0.0, -1.0 / 3.0),
838 epsilon = 1.0e-6
839 );
840
841 app.world_mut()
843 .entity_mut(child_entity)
844 .insert(Transform::from_xyz(0.0, 4.0, 0.0));
845
846 app.world_mut().run_schedule(FixedPostUpdate);
847
848 let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
849
850 assert_relative_eq!(mass.value(), 10.0 + 5.0);
851 assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
852 }
853
854 #[test]
855 fn mass_properties_no_auto_mass_add_remove() {
856 let mut app = create_app();
866
867 let collider = Collider::circle(1.0);
868 let collider_mass_props = collider.mass_properties(1.0);
869
870 let body_entity = app
871 .world_mut()
872 .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
873 .with_child((collider, Mass(10.0)))
874 .id();
875
876 app.world_mut().run_schedule(FixedPostUpdate);
877
878 let (mass, angular_inertia, center_of_mass) =
879 get_computed_mass_properties(app.world_mut(), body_entity);
880
881 assert_relative_eq!(mass.value(), 5.0 + 10.0);
882 assert_relative_eq!(
883 angular_inertia.value(),
884 5.0 * collider_mass_props.unit_angular_inertia()
885 + 10.0 * collider_mass_props.unit_angular_inertia()
886 );
887 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
888
889 app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
891
892 app.world_mut().run_schedule(FixedPostUpdate);
893
894 let (mass, angular_inertia, center_of_mass) =
895 get_computed_mass_properties(app.world_mut(), body_entity);
896
897 assert_relative_eq!(mass.value(), 5.0);
898 assert_relative_eq!(
899 angular_inertia.value(),
900 5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
901 + 10.0 * collider_mass_props.unit_angular_inertia())
902 / 15.0
903 );
904 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
905
906 app.world_mut()
908 .entity_mut(body_entity)
909 .remove::<NoAutoMass>();
910
911 app.world_mut().run_schedule(FixedPostUpdate);
912
913 let (mass, angular_inertia, center_of_mass) =
914 get_computed_mass_properties(app.world_mut(), body_entity);
915
916 assert_relative_eq!(mass.value(), 5.0 + 10.0);
917 assert_relative_eq!(
918 angular_inertia.value(),
919 5.0 * collider_mass_props.unit_angular_inertia()
920 + 10.0 * collider_mass_props.unit_angular_inertia()
921 );
922 assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
923 }
924}