1use crate::math::{AngVector, AngularInertia, Isometry, Point, Real, Rotation, Vector};
2use crate::utils;
3use core::ops::{Add, AddAssign, Sub, SubAssign};
4use num::Zero;
5#[cfg(feature = "dim3")]
6use {core::ops::MulAssign, na::Matrix3};
7
8#[cfg(feature = "rkyv")]
9use rkyv::{bytecheck, CheckBytes};
10
11#[cfg_attr(feature = "f32", expect(clippy::unnecessary_cast))]
12const EPSILON: Real = f32::EPSILON as Real;
13
14#[derive(Copy, Clone, Debug, Default, PartialEq)]
15#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
16#[cfg_attr(
17 feature = "rkyv",
18 derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, CheckBytes),
19 archive(as = "Self")
20)]
21pub struct MassProperties {
23 pub local_com: Point<Real>,
25 pub inv_mass: Real,
29 pub inv_principal_inertia: AngVector<Real>,
34 #[cfg(feature = "dim3")]
35 pub principal_inertia_local_frame: Rotation<Real>,
37}
38
39impl MassProperties {
40 #[cfg(feature = "dim2")]
44 pub fn new(local_com: Point<Real>, mass: Real, principal_inertia: Real) -> Self {
45 let inv_mass = utils::inv(mass);
46 let inv_principal_inertia = utils::inv(principal_inertia);
47 Self {
48 local_com,
49 inv_mass,
50 inv_principal_inertia,
51 }
52 }
53
54 #[cfg(feature = "dim3")]
60 pub fn new(local_com: Point<Real>, mass: Real, principal_inertia: AngVector<Real>) -> Self {
61 Self::with_principal_inertia_frame(local_com, mass, principal_inertia, Rotation::identity())
62 }
63
64 #[cfg(feature = "dim3")]
70 pub fn with_principal_inertia_frame(
71 local_com: Point<Real>,
72 mass: Real,
73 principal_inertia: AngVector<Real>,
74 principal_inertia_local_frame: Rotation<Real>,
75 ) -> Self {
76 let inv_mass = utils::inv(mass);
77 let inv_principal_inertia = principal_inertia.map(utils::inv);
78 Self {
79 local_com,
80 inv_mass,
81 inv_principal_inertia,
82 principal_inertia_local_frame,
83 }
84 }
85
86 #[cfg(feature = "dim3")]
91 pub fn with_inertia_matrix(local_com: Point<Real>, mass: Real, inertia: Matrix3<Real>) -> Self {
92 let mut eigen = inertia.symmetric_eigen();
93
94 if eigen.eigenvectors.determinant() < 0.0 {
95 eigen.eigenvectors.swap_columns(1, 2);
96 eigen.eigenvalues.swap_rows(1, 2);
97 }
98 let mut principal_inertia_local_frame = Rotation::from_rotation_matrix(
99 &na::Rotation3::from_matrix_unchecked(eigen.eigenvectors),
100 );
101 let _ = principal_inertia_local_frame.renormalize();
102
103 let principal_inertia = eigen.eigenvalues.map(|e| e.max(0.0));
105
106 Self::with_principal_inertia_frame(
107 local_com,
108 mass,
109 principal_inertia,
110 principal_inertia_local_frame,
111 )
112 }
113
114 pub fn mass(&self) -> Real {
116 utils::inv(self.inv_mass)
117 }
118
119 pub fn principal_inertia(&self) -> AngVector<Real> {
121 #[cfg(feature = "dim2")]
122 return utils::inv(self.inv_principal_inertia);
123 #[cfg(feature = "dim3")]
124 return self.inv_principal_inertia.map(utils::inv);
125 }
126
127 pub fn world_com(&self, pos: &Isometry<Real>) -> Point<Real> {
129 pos * self.local_com
130 }
131
132 #[cfg(feature = "dim2")]
133 pub fn world_inv_inertia(&self, _rot: &Rotation<Real>) -> AngularInertia<Real> {
135 self.inv_principal_inertia
136 }
137
138 #[cfg(feature = "dim3")]
139 pub fn world_inv_inertia(&self, rot: &Rotation<Real>) -> AngularInertia<Real> {
141 if !self.inv_principal_inertia.is_zero() {
142 let mut lhs = (rot * self.principal_inertia_local_frame)
143 .to_rotation_matrix()
144 .into_inner();
145 let rhs = lhs.transpose();
146 lhs.column_mut(0).mul_assign(self.inv_principal_inertia.x);
147 lhs.column_mut(1).mul_assign(self.inv_principal_inertia.y);
148 lhs.column_mut(2).mul_assign(self.inv_principal_inertia.z);
149 let inertia = lhs * rhs;
150 AngularInertia::from_sdp_matrix(inertia)
151 } else {
152 AngularInertia::zero()
153 }
154 }
155
156 #[cfg(feature = "dim3")]
157 pub fn reconstruct_inverse_inertia_matrix(&self) -> Matrix3<Real> {
159 let inv_principal_inertia = self.inv_principal_inertia;
160 self.principal_inertia_local_frame.to_rotation_matrix()
161 * Matrix3::from_diagonal(&inv_principal_inertia)
162 * self
163 .principal_inertia_local_frame
164 .inverse()
165 .to_rotation_matrix()
166 }
167
168 #[cfg(feature = "dim3")]
169 pub fn reconstruct_inertia_matrix(&self) -> Matrix3<Real> {
171 let principal_inertia = self.inv_principal_inertia.map(utils::inv);
172 self.principal_inertia_local_frame.to_rotation_matrix()
173 * Matrix3::from_diagonal(&principal_inertia)
174 * self
175 .principal_inertia_local_frame
176 .inverse()
177 .to_rotation_matrix()
178 }
179
180 #[cfg(feature = "dim2")]
181 pub(crate) fn construct_shifted_inertia_matrix(&self, shift: Vector<Real>) -> Real {
182 let i = utils::inv(self.inv_principal_inertia);
183
184 if self.inv_mass != 0.0 {
185 let mass = 1.0 / self.inv_mass;
186 i + shift.norm_squared() * mass
187 } else {
188 i
189 }
190 }
191
192 #[cfg(feature = "dim3")]
193 pub(crate) fn construct_shifted_inertia_matrix(&self, shift: Vector<Real>) -> Matrix3<Real> {
194 let matrix = self.reconstruct_inertia_matrix();
195
196 if self.inv_mass != 0.0 {
197 let mass = 1.0 / self.inv_mass;
198 let diag = shift.norm_squared();
199 let diagm = Matrix3::from_diagonal_element(diag);
200 matrix + (diagm - shift * shift.transpose()) * mass
201 } else {
202 matrix
203 }
204 }
205
206 pub fn transform_by(&self, m: &Isometry<Real>) -> Self {
208 Self {
211 local_com: m * self.local_com,
212 inv_mass: self.inv_mass,
213 inv_principal_inertia: self.inv_principal_inertia,
214 #[cfg(feature = "dim3")]
215 principal_inertia_local_frame: m.rotation * self.principal_inertia_local_frame,
216 }
217 }
218
219 pub fn set_mass(&mut self, new_mass: Real, adjust_angular_inertia: bool) {
228 let new_inv_mass = utils::inv(new_mass);
229
230 if adjust_angular_inertia {
231 let curr_mass = utils::inv(self.inv_mass);
232 self.inv_principal_inertia *= new_inv_mass * curr_mass;
233 }
234
235 self.inv_mass = new_inv_mass;
236 }
237}
238
239impl Zero for MassProperties {
240 fn zero() -> Self {
241 Self {
242 inv_mass: 0.0,
243 inv_principal_inertia: na::zero(),
244 #[cfg(feature = "dim3")]
245 principal_inertia_local_frame: Rotation::identity(),
246 local_com: Point::origin(),
247 }
248 }
249
250 fn is_zero(&self) -> bool {
251 *self == Self::zero()
252 }
253}
254
255impl Sub<MassProperties> for MassProperties {
256 type Output = Self;
257
258 #[cfg(feature = "dim2")]
259 fn sub(self, other: MassProperties) -> Self {
260 if self.is_zero() || other.is_zero() {
261 return self;
262 }
263
264 let m1 = utils::inv(self.inv_mass);
265 let m2 = utils::inv(other.inv_mass);
266
267 let mut new_mass = m1 - m2;
268
269 if new_mass < EPSILON {
270 new_mass = 0.0;
272 }
273
274 let inv_mass = utils::inv(new_mass);
275
276 let local_com = (self.local_com * m1 - other.local_com.coords * m2) * inv_mass;
277 let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
278 let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
279 let mut inertia = i1 - i2;
280
281 if inertia < EPSILON {
282 inertia = 0.0;
284 }
285
286 let inv_principal_inertia = utils::inv(inertia);
288
289 Self {
290 local_com,
291 inv_mass,
292 inv_principal_inertia,
293 }
294 }
295
296 #[cfg(feature = "dim3")]
297 fn sub(self, other: MassProperties) -> Self {
298 if self.is_zero() || other.is_zero() {
299 return self;
300 }
301
302 let m1 = utils::inv(self.inv_mass);
303 let m2 = utils::inv(other.inv_mass);
304 let mut new_mass = m1 - m2;
305
306 if new_mass < EPSILON {
307 new_mass = 0.0;
308 }
309
310 let inv_mass = utils::inv(new_mass);
311 let local_com = (self.local_com * m1 - other.local_com.coords * m2) * inv_mass;
312 let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
313 let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
314 let inertia = i1 - i2;
315 Self::with_inertia_matrix(local_com, new_mass, inertia)
316 }
317}
318
319impl SubAssign<MassProperties> for MassProperties {
320 fn sub_assign(&mut self, rhs: MassProperties) {
321 *self = *self - rhs
322 }
323}
324
325impl Add<MassProperties> for MassProperties {
326 type Output = Self;
327
328 #[cfg(feature = "dim2")]
329 fn add(self, other: MassProperties) -> Self {
330 if self.is_zero() {
331 return other;
332 } else if other.is_zero() {
333 return self;
334 }
335
336 let m1 = utils::inv(self.inv_mass);
337 let m2 = utils::inv(other.inv_mass);
338 let inv_mass = utils::inv(m1 + m2);
339 let local_com = (self.local_com * m1 + other.local_com.coords * m2) * inv_mass;
340 let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
341 let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
342 let inertia = i1 + i2;
343 let inv_principal_inertia = utils::inv(inertia);
344
345 Self {
346 local_com,
347 inv_mass,
348 inv_principal_inertia,
349 }
350 }
351
352 #[cfg(feature = "dim3")]
353 fn add(self, other: MassProperties) -> Self {
354 if self.is_zero() {
355 return other;
356 } else if other.is_zero() {
357 return self;
358 }
359
360 let m1 = utils::inv(self.inv_mass);
361 let m2 = utils::inv(other.inv_mass);
362 let inv_mass = utils::inv(m1 + m2);
363 let local_com = (self.local_com * m1 + other.local_com.coords * m2) * inv_mass;
364 let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
365 let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
366 let inertia = i1 + i2;
367
368 Self::with_inertia_matrix(local_com, m1 + m2, inertia)
369 }
370}
371
372impl AddAssign<MassProperties> for MassProperties {
373 fn add_assign(&mut self, rhs: MassProperties) {
374 *self = *self + rhs
375 }
376}
377
378#[cfg(feature = "alloc")]
379impl core::iter::Sum<MassProperties> for MassProperties {
380 #[cfg(feature = "dim2")]
381 fn sum<I>(iter: I) -> Self
382 where
383 I: Iterator<Item = Self>,
384 {
385 use alloc::vec::Vec;
386
387 let mut total_mass = 0.0;
388 let mut total_com = Point::origin();
389 let mut total_inertia = 0.0;
390 let mut all_props = Vec::new();
393
394 for props in iter {
395 let mass = utils::inv(props.inv_mass);
396 total_mass += mass;
397 total_com += props.local_com.coords * mass;
398 all_props.push(props);
399 }
400
401 if total_mass > 0.0 {
402 total_com /= total_mass;
403 }
404
405 for props in all_props {
406 total_inertia += props.construct_shifted_inertia_matrix(total_com - props.local_com);
407 }
408
409 Self {
410 local_com: total_com,
411 inv_mass: utils::inv(total_mass),
412 inv_principal_inertia: utils::inv(total_inertia),
413 }
414 }
415
416 #[cfg(feature = "dim3")]
417 fn sum<I>(iter: I) -> Self
418 where
419 I: Iterator<Item = Self>,
420 {
421 use alloc::vec::Vec;
422
423 let mut total_mass = 0.0;
424 let mut total_com = Point::origin();
425 let mut total_inertia = Matrix3::zeros();
426 let mut all_props = Vec::new();
429
430 for props in iter {
431 let mass = utils::inv(props.inv_mass);
432 total_mass += mass;
433 total_com += props.local_com.coords * mass;
434 all_props.push(props);
435 }
436
437 if total_mass > 0.0 {
438 total_com /= total_mass;
439 }
440
441 for props in all_props {
442 total_inertia += props.construct_shifted_inertia_matrix(total_com - props.local_com);
443 }
444
445 Self::with_inertia_matrix(total_com, total_mass, total_inertia)
446 }
447}
448
449impl approx::AbsDiffEq for MassProperties {
450 type Epsilon = Real;
451 fn default_epsilon() -> Self::Epsilon {
452 Real::default_epsilon()
453 }
454
455 fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
456 #[cfg(feature = "dim2")]
457 let inertia_is_ok = self
458 .inv_principal_inertia
459 .abs_diff_eq(&other.inv_principal_inertia, epsilon);
460
461 #[cfg(feature = "dim3")]
462 let inertia_is_ok = self
463 .reconstruct_inverse_inertia_matrix()
464 .abs_diff_eq(&other.reconstruct_inverse_inertia_matrix(), epsilon);
465
466 inertia_is_ok
467 && self.local_com.abs_diff_eq(&other.local_com, epsilon)
468 && self.inv_mass.abs_diff_eq(&other.inv_mass, epsilon)
469 && self
470 .inv_principal_inertia
471 .abs_diff_eq(&other.inv_principal_inertia, epsilon)
472 }
473}
474
475impl approx::RelativeEq for MassProperties {
476 fn default_max_relative() -> Self::Epsilon {
477 Real::default_max_relative()
478 }
479
480 fn relative_eq(
481 &self,
482 other: &Self,
483 epsilon: Self::Epsilon,
484 max_relative: Self::Epsilon,
485 ) -> bool {
486 #[cfg(feature = "dim2")]
487 let inertia_is_ok = self.inv_principal_inertia.relative_eq(
488 &other.inv_principal_inertia,
489 epsilon,
490 max_relative,
491 );
492
493 #[cfg(feature = "dim3")]
494 let inertia_is_ok = self.reconstruct_inverse_inertia_matrix().relative_eq(
495 &other.reconstruct_inverse_inertia_matrix(),
496 epsilon,
497 max_relative,
498 );
499
500 inertia_is_ok
501 && self
502 .local_com
503 .relative_eq(&other.local_com, epsilon, max_relative)
504 && self
505 .inv_mass
506 .relative_eq(&other.inv_mass, epsilon, max_relative)
507 }
508}
509
510#[cfg(test)]
511mod test {
512 use super::MassProperties;
513 use crate::math::{AngVector, Point};
514 #[cfg(feature = "dim3")]
515 use crate::math::{Rotation, Vector};
516 use crate::shape::{Ball, Capsule, Shape};
517 use approx::assert_relative_eq;
518 use num::Zero;
519
520 #[test]
521 fn mass_properties_add_partial_zero() {
522 let m1 = MassProperties {
523 local_com: Point::origin(),
524 inv_mass: 2.0,
525 inv_principal_inertia: na::zero(),
526 #[cfg(feature = "dim3")]
527 principal_inertia_local_frame: Rotation::identity(),
528 };
529 let m2 = MassProperties {
530 local_com: Point::origin(),
531 inv_mass: 0.0,
532 #[cfg(feature = "dim2")]
533 inv_principal_inertia: 1.0,
534 #[cfg(feature = "dim3")]
535 inv_principal_inertia: Vector::new(1.0, 2.0, 3.0),
536 #[cfg(feature = "dim3")]
537 principal_inertia_local_frame: Rotation::identity(),
538 };
539 let result = MassProperties {
540 local_com: Point::origin(),
541 inv_mass: 2.0,
542 #[cfg(feature = "dim2")]
543 inv_principal_inertia: 1.0,
544 #[cfg(feature = "dim3")]
545 inv_principal_inertia: Vector::new(1.0, 2.0, 3.0),
546 #[cfg(feature = "dim3")]
547 principal_inertia_local_frame: Rotation::identity(),
548 };
549
550 assert_eq!(m1 + m2, result);
551 assert_eq!(m2 + m1, result);
552 }
553
554 #[test]
555 fn mass_properties_add_sub() {
556 let c1 = Capsule::new_x(1.0, 2.0);
558 let c2 = Capsule::new_y(3.0, 4.0);
559 let c3 = Ball::new(2.0);
560
561 let m1 = c1.mass_properties(1.0);
562 let m2 = c2.mass_properties(1.0);
563 let m3 = c3.mass_properties(1.0);
564 let m1m2m3 = m1 + m2 + m3;
565
566 assert_relative_eq!(m1 + m2, m2 + m1, epsilon = 1.0e-6);
567 assert_relative_eq!(m1m2m3 - m1, m2 + m3, epsilon = 1.0e-6);
568 assert_relative_eq!(m1m2m3 - m2, m1 + m3, epsilon = 1.0e-6);
569 assert_relative_eq!(m1m2m3 - m3, m1 + m2, epsilon = 1.0e-6);
570 assert_relative_eq!(m1m2m3 - (m1 + m2), m3, epsilon = 1.0e-6);
571 assert_relative_eq!(m1m2m3 - (m1 + m3), m2, epsilon = 1.0e-6);
572 assert_relative_eq!(m1m2m3 - (m2 + m3), m1, epsilon = 1.0e-6);
573 assert_relative_eq!(m1m2m3 - m1 - m2, m3, epsilon = 1.0e-6);
574 assert_relative_eq!(m1m2m3 - m1 - m3, m2, epsilon = 1.0e-6);
575 assert_relative_eq!(m1m2m3 - m2 - m3, m1, epsilon = 1.0e-6);
576
577 assert_relative_eq!(
581 (((m1m2m3 - m1) - m2) - m3).principal_inertia(),
582 AngVector::zero(),
583 epsilon = 1.0e-3
584 );
585 assert_relative_eq!((((m1m2m3 - m1) - m2) - m3).mass(), 0.0, epsilon = 1.0e-6);
586 }
587
588 #[test]
589 #[cfg(feature = "alloc")]
590 fn mass_properties_compound() {
591 use na::Isometry;
592
593 use crate::{
594 math::Vector,
595 shape::{Compound, Cuboid, SharedShape},
596 };
597
598 let shape = Cuboid::new(Vector::repeat(0.5));
600 let mp = shape.mass_properties(1.0);
601 let iso2 = Isometry::from_parts(Vector::y().into(), Default::default());
602 let iso3 = Isometry::from_parts((-Vector::y()).into(), Default::default());
603
604 let sum = [mp, mp.transform_by(&iso2), mp.transform_by(&iso3)]
606 .into_iter()
607 .sum::<MassProperties>();
608
609 let compound_shape = Compound::new(vec![
611 (
612 Isometry::from_parts(Vector::default().into(), Default::default()),
613 SharedShape::new(shape),
614 ),
615 (iso2, SharedShape::new(shape)),
616 (iso3, SharedShape::new(shape)),
617 ]);
618 let mp_compound = compound_shape.mass_properties(1.0);
619
620 #[cfg(feature = "dim2")]
623 let expected = Cuboid::new(Vector::new(0.5, 1.5)).mass_properties(1.0);
624 #[cfg(feature = "dim3")]
625 let expected = Cuboid::new(Vector::new(0.5, 1.5, 0.5)).mass_properties(1.0);
626
627 assert_relative_eq!(sum.local_com, expected.local_com, epsilon = 1.0e-6);
629 assert_relative_eq!(sum.inv_mass, expected.inv_mass, epsilon = 1.0e-6);
630 assert_relative_eq!(
631 sum.inv_principal_inertia,
632 expected.inv_principal_inertia,
633 epsilon = 1.0e-6
634 );
635
636 assert_relative_eq!(mp_compound.local_com, expected.local_com, epsilon = 1.0e-6);
638 assert_relative_eq!(mp_compound.inv_mass, expected.inv_mass, epsilon = 1.0e-6);
639 assert_relative_eq!(
640 mp_compound.inv_principal_inertia,
641 expected.inv_principal_inertia,
642 epsilon = 1.0e-6
643 );
644 }
645
646 #[test]
647 #[cfg(feature = "alloc")]
648 fn mass_properties_sum_no_nan() {
649 let mp: MassProperties = [MassProperties::zero()].iter().copied().sum();
650 assert!(!mp.local_com.x.is_nan() && !mp.local_com.y.is_nan());
651 #[cfg(feature = "dim3")]
652 assert!(!mp.local_com.z.is_nan());
653 }
654}