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