phys_inertia/
lib.rs

1// Copyright (C) 2020-2025 phys-inertia authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15extern crate nalgebra as na;
16use math::{is_near_zero, vec3, Isometry3, Mat3, Mat3Ext, Real, UnitQuat, Vec3};
17pub use phys_geom::shape;
18use phys_geom::shape::{Capsule, Cuboid, Cylinder, InfinitePlane, Sphere, Tetrahedron};
19use transformer::InertiaTensorTransformer;
20
21use crate::math::UnitQuatExt;
22
23pub mod math;
24pub mod transformer;
25
26/// The inertia properties of a shape.
27#[derive(Clone)]
28pub struct Inertia {
29    /// The mass normalized diagonal inerita with principal axis pass through the center of mass.
30    pub diag_inertia: Vec3,
31    /// The rotation from principal axis space to shape space. None if no rotation is needed.
32    pub inertia_rotation: Option<UnitQuat>,
33    /// The center of mass in shape space. None if the center of mass is at the origin.
34    pub center_of_mass: Option<Vec3>,
35}
36
37impl Default for Inertia {
38    fn default() -> Self {
39        Inertia {
40            diag_inertia: Vec3::repeat(1.0),
41            inertia_rotation: None,
42            center_of_mass: None,
43        }
44    }
45}
46
47impl Inertia {
48    /// Zero valued inertia.
49    pub const ZERO: Inertia = Inertia {
50        diag_inertia: Vec3::new(0.0, 0.0, 0.0),
51        inertia_rotation: None,
52        center_of_mass: None,
53    };
54}
55
56/// Implement this trait to provide the inertia computation ability.
57pub trait ComputeInertia {
58    /// Compute the inertia properties of an object.
59    fn compute_inertia(&self) -> Inertia;
60}
61
62impl ComputeInertia for Sphere {
63    #[inline]
64    fn compute_inertia(&self) -> Inertia {
65        Inertia {
66            diag_inertia: sphere(self.radius()),
67            ..Default::default()
68        }
69    }
70}
71
72impl ComputeInertia for Cuboid {
73    #[inline]
74    fn compute_inertia(&self) -> Inertia {
75        Inertia {
76            diag_inertia: cube(self.length()),
77            ..Default::default()
78        }
79    }
80}
81
82impl ComputeInertia for Cylinder {
83    #[inline]
84    fn compute_inertia(&self) -> Inertia {
85        Inertia {
86            diag_inertia: cylinder(self.radius(), self.height()),
87            ..Default::default()
88        }
89    }
90}
91
92impl ComputeInertia for Capsule {
93    #[inline]
94    fn compute_inertia(&self) -> Inertia {
95        Inertia {
96            diag_inertia: capsule(self.radius(), self.height()),
97            ..Default::default()
98        }
99    }
100}
101
102impl ComputeInertia for InfinitePlane {
103    #[inline]
104    fn compute_inertia(&self) -> Inertia {
105        Inertia::ZERO
106    }
107}
108
109/// Compute the diagonal inertia of a cube in principal axis space with mass = 1.
110#[inline]
111pub fn cube(size: impl Into<[Real; 3]>) -> Vec3 {
112    let [a, b, c] = size.into();
113    const M: Real = 1.0 / 12.0;
114    let (a2, b2, c2) = (a * a, b * b, c * c);
115    vec3(b2 + c2, a2 + c2, a2 + b2) * M
116}
117
118/// Compute the diagonal inertia of a solid sphere in principal axis space with mass = 1.
119#[inline]
120pub fn sphere(radius: Real) -> Vec3 {
121    let a = 2.0 / 5.0 * radius * radius;
122    vec3(a, a, a)
123}
124
125/// Compute the diagonal inertia of a cylinder in principal axis space with mass = 1.
126///
127/// The cylinder is assumed to be oriented along the y-axis.
128#[inline]
129pub fn cylinder(radius: Real, height: Real) -> Vec3 {
130    const SA: Real = 1.0 / 12.0;
131    const SB: Real = 1.0 / 2.0;
132
133    let a = SA * (3.0 * radius * radius + height * height);
134    let b = SB * radius * radius;
135    vec3(a, b, a)
136}
137
138/// Compute the diagonal inertia of a capsule in principal axis space with mass = 1.
139///
140/// The capsule is assumed to be oriented along the y-axis.
141#[inline]
142pub fn capsule(radius: Real, height: Real) -> Vec3 {
143    const A: Real = 13.0 / 20.0;
144    const B: Real = 7.0 / 12.0;
145    const C: Real = 3.0 / 8.0;
146    const D: Real = 9.0 / 10.0;
147
148    let radius_sq = radius * radius;
149    let height_sq = height * height;
150    let a = A * radius_sq + B * height_sq + C * radius * height;
151    let b = D * radius_sq;
152    vec3(a, b, a)
153}
154
155/// Compute the inertia tensor of a tetrahedron with mass = 1.
156pub fn tetrahedron(points: [Vec3; 4]) -> Mat3 {
157    let [p1, p2, p3, p4] = points;
158
159    // Just for readability.
160    let x1 = p1[0];
161    let y1 = p1[1];
162    let z1 = p1[2];
163    let x2 = p2[0];
164    let y2 = p2[1];
165    let z2 = p2[2];
166    let x3 = p3[0];
167    let y3 = p3[1];
168    let z3 = p3[2];
169    let x4 = p4[0];
170    let y4 = p4[1];
171    let z4 = p4[2];
172
173    let diag_x = x1 * x1
174        + x1 * x2
175        + x2 * x2
176        + x1 * x3
177        + x2 * x3
178        + x3 * x3
179        + x1 * x4
180        + x2 * x4
181        + x3 * x4
182        + x4 * x4;
183    let diag_y = y1 * y1
184        + y1 * y2
185        + y2 * y2
186        + y1 * y3
187        + y2 * y3
188        + y3 * y3
189        + y1 * y4
190        + y2 * y4
191        + y3 * y4
192        + y4 * y4;
193    let diag_z = z1 * z1
194        + z1 * z2
195        + z2 * z2
196        + z1 * z3
197        + z2 * z3
198        + z3 * z3
199        + z1 * z4
200        + z2 * z4
201        + z3 * z4
202        + z4 * z4;
203
204    let a0 = (diag_y + diag_z) * 0.1;
205    let b0 = (diag_z + diag_x) * 0.1;
206    let c0 = (diag_x + diag_y) * 0.1;
207
208    let a1 = (y1 * z1 * 2.0
209        + y2 * z1
210        + y3 * z1
211        + y4 * z1
212        + y1 * z2
213        + y2 * z2 * 2.0
214        + y3 * z2
215        + y4 * z2
216        + y1 * z3
217        + y2 * z3
218        + y3 * z3 * 2.0
219        + y4 * z3
220        + y1 * z4
221        + y2 * z4
222        + y3 * z4
223        + y4 * z4 * 2.0)
224        * 0.05;
225    let b1 = (x1 * z1 * 2.0
226        + x2 * z1
227        + x3 * z1
228        + x4 * z1
229        + x1 * z2
230        + x2 * z2 * 2.0
231        + x3 * z2
232        + x4 * z2
233        + x1 * z3
234        + x2 * z3
235        + x3 * z3 * 2.0
236        + x4 * z3
237        + x1 * z4
238        + x2 * z4
239        + x3 * z4
240        + x4 * z4 * 2.0)
241        * 0.05;
242    let c1 = (x1 * y1 * 2.0
243        + x2 * y1
244        + x3 * y1
245        + x4 * y1
246        + x1 * y2
247        + x2 * y2 * 2.0
248        + x3 * y2
249        + x4 * y2
250        + x1 * y3
251        + x2 * y3
252        + x3 * y3 * 2.0
253        + x4 * y3
254        + x1 * y4
255        + x2 * y4
256        + x3 * y4
257        + x4 * y4 * 2.0)
258        * 0.05;
259
260    Mat3::from_column_slice(&[a0, -b1, -c1, -b1, b0, -a1, -c1, -a1, c0])
261}
262
263/// Compute the inertia tensor of a mesh with mass = 1.
264pub fn mesh(triangles: impl Iterator<Item = [Vec3; 3]>) -> Inertia {
265    let mut vol = 0.0;
266    let mut itot = Mat3::zeros();
267    let mut com = Vec3::zeros();
268    for triangle in triangles {
269        let a = Vec3::zeros();
270        let [b, c, d] = triangle;
271        let tet = Tetrahedron {
272            a: a.into(),
273            b: b.into(),
274            c: c.into(),
275            d: d.into(),
276        };
277        let volume = tet.signed_volume();
278        let ipart = tetrahedron([a, b, c, d]);
279
280        com += Vec3::from(tet.center()) * volume;
281        itot += ipart * volume;
282        vol += volume;
283    }
284
285    if vol == 0.0 {
286        return Inertia::ZERO;
287    }
288    let inertia = itot * vol.recip();
289    com /= vol;
290
291    let (diag_inertia, inertia_rotation) = inertia.diagonalize();
292    Inertia {
293        diag_inertia,
294        inertia_rotation: if inertia_rotation.is_near_identity() {
295            None
296        } else {
297            inertia_rotation.into()
298        },
299        center_of_mass: if is_near_zero(com, 1e-5) {
300            None
301        } else {
302            com.into()
303        },
304    }
305}
306
307pub struct CompoundUnit<S> {
308    pub object: S,
309    pub pose: Isometry3,
310    pub mass: Real,
311}
312/// Calculate the mass and mass-normalized inertia of compound shapes
313///
314/// # Parameters
315///
316/// - `iter`: an iterator of compound units, each unit contains a shape, a pose and a mass.
317/// - `zero_mass_as_infinite`: a flag to determine the behavior when a shape's mass is 0.0.
318///     - if true, when any shape's mass is 0.0, the mass of the compound shape will be 0.0.
319///     - if false, when any shape's mass is 0.0, it will be ignored.
320///
321/// # Note
322///
323/// - We assume all shapes have the same density, so the center of mass and the inertia rotation
324///   only depend on the shape's volume and their relative pose.
325/// - if the given iterator is empty, the returned inertia will be same as a unit sphere with 1kg
326///   mass.
327pub fn compound<S>(
328    iter: impl Iterator<Item = CompoundUnit<S>>,
329    zero_mass_as_infinite: bool,
330) -> (Inertia, Real)
331where
332    S: ComputeInertia,
333{
334    let mut compound_computer = InertiaTensorTransformer::new(Vec3::zeros(), Mat3::zeros(), 0.0);
335
336    let mut has_shape = false;
337    for CompoundUnit {
338        object: entity,
339        pose,
340        mass,
341    } in iter
342    {
343        has_shape = true;
344        if mass == 0.0 {
345            if zero_mass_as_infinite {
346                return (Inertia::ZERO, 0.0);
347            } else {
348                continue;
349            }
350        }
351        let inertia = entity.compute_inertia();
352
353        // we assume all shapes' density is 1.0.
354        let inertia_matrix = Mat3::from_diagonal(&(inertia.diag_inertia * mass));
355        let mut shape_computer = InertiaTensorTransformer::new(Vec3::zeros(), inertia_matrix, mass);
356        if let Some(rotation) = inertia.inertia_rotation {
357            shape_computer.rotate(rotation);
358        }
359
360        if let Some(translation) = inertia.center_of_mass {
361            shape_computer.translate(translation);
362        }
363
364        shape_computer.transform(&pose);
365        compound_computer.add(&shape_computer);
366    }
367    if !has_shape {
368        return (
369            Inertia {
370                diag_inertia: sphere(0.5),
371                ..Default::default()
372            },
373            1.0,
374        );
375    }
376
377    let compound_com = compound_computer.center_of_mass();
378    compound_computer.center();
379    let (diag, rot) = compound_computer.inertia().diagonalize();
380    let mass = compound_computer.mass();
381    (
382        Inertia {
383            diag_inertia: diag / compound_computer.mass(),
384            inertia_rotation: if rot.is_near_identity() {
385                None
386            } else {
387                Some(rot)
388            },
389            center_of_mass: if is_near_zero(compound_com, 1e-6) {
390                None
391            } else {
392                Some(compound_com)
393            },
394        },
395        mass,
396    )
397}
398
399/// Given the low triangle part of a symmetric inertia matrix, which is column-major, then
400/// diagonalize it.
401///
402/// # Returns
403///
404/// The diagonalized inertia matrix and the rotation.
405#[inline]
406pub fn diagonalize_inertia(inertia_matrix: [math::Real; 6]) -> (Vec3, UnitQuat) {
407    let inertia = Mat3::from_column_slice(&[
408        inertia_matrix[0],
409        inertia_matrix[1],
410        inertia_matrix[2],
411        inertia_matrix[1],
412        inertia_matrix[3],
413        inertia_matrix[4],
414        inertia_matrix[2],
415        inertia_matrix[4],
416        inertia_matrix[5],
417    ]);
418    inertia.diagonalize()
419}
420
421#[cfg(test)]
422mod tests {
423    use approx::assert_relative_eq;
424
425    use super::*;
426
427    #[test]
428    fn test_cylinder() {
429        let cylinder = Cylinder::new(1.0, 1.0);
430        let inertia = cylinder.compute_inertia();
431
432        assert_relative_eq!(inertia.diag_inertia, Vec3::new(7.0 / 12.0, 0.5, 7.0 / 12.0));
433        assert_eq!(inertia.center_of_mass, None);
434        assert_eq!(inertia.inertia_rotation, None);
435    }
436
437    #[test]
438    fn test_capsule() {
439        let capsule = Capsule::new(0.5, 0.5);
440        let inertia = capsule.compute_inertia();
441
442        assert_relative_eq!(inertia.diag_inertia, Vec3::new(0.9333333, 0.225, 0.9333333));
443        assert_eq!(inertia.center_of_mass, None);
444        assert_eq!(inertia.inertia_rotation, None);
445    }
446
447    #[test]
448    fn test_cuboid() {
449        let cuboid = Cuboid::new([1.0, 1.0, 1.0]);
450        let inertia = cuboid.compute_inertia();
451
452        assert_relative_eq!(
453            inertia.diag_inertia,
454            Vec3::new(1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0)
455        );
456        assert_eq!(inertia.center_of_mass, None);
457        assert_eq!(inertia.inertia_rotation, None);
458    }
459
460    #[test]
461    fn test_sphere() {
462        let sphere = Sphere::new(1.0);
463        let inertia = sphere.compute_inertia();
464
465        assert_relative_eq!(
466            inertia.diag_inertia,
467            Vec3::new(2.0 / 5.0, 2.0 / 5.0, 2.0 / 5.0)
468        );
469        assert_eq!(inertia.center_of_mass, None);
470        assert_eq!(inertia.inertia_rotation, None);
471    }
472
473    #[test]
474    fn test_infinite_plane() {
475        let plane = InfinitePlane::default();
476        let inertia = plane.compute_inertia();
477        assert_relative_eq!(inertia.diag_inertia, Inertia::ZERO.diag_inertia);
478        assert_eq!(inertia.center_of_mass, None);
479        assert_eq!(inertia.inertia_rotation, None);
480    }
481
482    #[test]
483    fn test_tetrahedron() {
484        let inertia = super::tetrahedron([
485            vec3(0.0, 0.0, 0.0),
486            vec3(1.0, 0.0, 0.0),
487            vec3(0.0, 1.0, 0.0),
488            vec3(0.0, 0.0, 1.0),
489        ]);
490        assert_relative_eq!(
491            inertia,
492            Mat3::from_column_slice(&[0.2, -0.05, -0.05, -0.05, 0.2, -0.05, -0.05, -0.05, 0.2])
493        );
494
495        let inertia = super::tetrahedron([
496            vec3(1.0, 1.0, 1.0),
497            vec3(-1.0, -1.0, 1.0),
498            vec3(-1.0, 1.0, -1.0),
499            vec3(1.0, -1.0, -1.0),
500        ]);
501
502        assert_relative_eq!(inertia, Mat3::from_diagonal(&vec3(0.4, 0.4, 0.4)));
503    }
504
505    #[test]
506    fn test_mesh() {
507        let com = vec3(0.5, 0.5, 0.5);
508        // a cube with side length 1.0
509        // ```text
510        // 7-------6
511        // |\      |\
512        // | 3-----|-2
513        // 4-|-----5 |
514        //  \|      \|
515        //   0-------1
516        // ```
517        let cube_corners = [
518            vec3(0.0, 0.0, 0.0) - com,
519            vec3(1.0, 0.0, 0.0) - com,
520            vec3(1.0, 1.0, 0.0) - com,
521            vec3(0.0, 1.0, 0.0) - com,
522            vec3(0.0, 0.0, 1.0) - com,
523            vec3(1.0, 0.0, 1.0) - com,
524            vec3(1.0, 1.0, 1.0) - com,
525            vec3(0.0, 1.0, 1.0) - com,
526        ];
527        let indices = [
528            [0, 1, 2],
529            [0, 2, 3],
530            [4, 6, 5],
531            [4, 7, 6],
532            [0, 4, 1],
533            [1, 4, 5],
534            [2, 6, 3],
535            [3, 6, 7],
536            [0, 3, 7],
537            [0, 7, 4],
538            [5, 6, 2],
539            [5, 2, 1],
540        ];
541        let triangles = indices
542            .iter()
543            .map(|[a, b, c]| [cube_corners[*a], cube_corners[*b], cube_corners[*c]]);
544
545        let inertia = super::mesh(triangles);
546
547        let expected = Cuboid::UNIT.compute_inertia();
548
549        assert_relative_eq!(inertia.diag_inertia, expected.diag_inertia);
550        assert_eq!(inertia.center_of_mass, None);
551        assert_eq!(inertia.inertia_rotation, None);
552    }
553
554    #[test]
555    fn test_zero_volume_mesh() {
556        let triangles = vec![[
557            vec3(0.0, 0.0, 0.0),
558            vec3(1.0, 0.0, 0.0),
559            vec3(0.0, 1.0, 0.0),
560        ]];
561        let inertia = super::mesh(triangles.into_iter());
562        assert_relative_eq!(inertia.diag_inertia, Vec3::zeros());
563    }
564
565    mod compounds {
566        use approx::assert_relative_eq;
567        use phys_geom::shape::{Cuboid, Sphere};
568
569        use super::Real;
570        use crate::math::{vec3, Isometry3, UnitQuat, Vec3};
571        use crate::{CompoundUnit, ComputeInertia, Inertia};
572
573        struct TransformedShapeRefs<'a, S> {
574            shapes: &'a [S],
575            poses: &'a [Isometry3],
576            masses: &'a [Real],
577        }
578
579        impl<'a, S: Clone> TransformedShapeRefs<'a, S> {
580            #[inline]
581            fn iter(&self) -> impl Iterator<Item = CompoundUnit<S>> + '_ {
582                let len = self.shapes.len();
583                (0..len).map(|index| CompoundUnit {
584                    object: self.shapes[index].clone(),
585                    pose: self.poses[index],
586                    mass: self.masses[index],
587                })
588            }
589        }
590
591        pub(crate) fn compound<S: ComputeInertia + Clone>(
592            shapes: &[S],
593            poses: &[Isometry3],
594            masses: &[Real],
595            zero_mass_as_infinite: bool,
596        ) -> (Inertia, Real) {
597            let shapes = TransformedShapeRefs {
598                shapes,
599                poses,
600                masses,
601            };
602            crate::compound(shapes.iter(), zero_mass_as_infinite)
603        }
604
605        #[test]
606        fn test_compound_one_cube() {
607            const EPS: Real = 1e-5;
608
609            let shapes = &[Cuboid::new(Vec3::repeat(1.0))];
610            let poses: &[Isometry3; 1] = &[Isometry3::identity()];
611            let masses = &[1.0];
612            let (
613                Inertia {
614                    diag_inertia,
615                    center_of_mass,
616                    inertia_rotation,
617                },
618                mass,
619            ) = compound(shapes, poses, masses, true);
620
621            assert_eq!(mass, 1.0);
622            assert_eq!(inertia_rotation, None);
623            assert_eq!(center_of_mass, None);
624            assert_relative_eq!(
625                diag_inertia,
626                vec3(0.166_666_67, 0.166_666_67, 0.166_666_67),
627                epsilon = EPS
628            );
629        }
630
631        #[test]
632        fn test_compound_two_cubes_symmetric() {
633            //combine two half cubes to a unit cube.
634
635            let shapes = &[
636                Cuboid::new_xyz(0.5, 1.0, 1.0),
637                Cuboid::new_xyz(0.5, 1.0, 1.0),
638            ];
639            let poses = &[
640                Isometry3::from_parts(vec3(-0.25, 0., 0.).into(), UnitQuat::identity()),
641                Isometry3::from_parts(vec3(0.25, 0., 0.).into(), UnitQuat::identity()),
642            ];
643            let masses = &[1.0, 1.0];
644            let (
645                Inertia {
646                    diag_inertia,
647                    inertia_rotation,
648                    center_of_mass,
649                },
650                mass,
651            ) = compound(shapes, poses, masses, true);
652
653            assert_eq!(mass, 2.0);
654            assert_relative_eq!(
655                diag_inertia,
656                crate::cube(vec3(1.0, 1.0, 1.0)),
657                epsilon = 1e-5
658            );
659            assert_eq!(inertia_rotation, None);
660            assert_eq!(center_of_mass, None);
661        }
662
663        #[test]
664        fn test_compound_zero_volume_shape() {
665            #[derive(Clone)]
666            struct MockInertia {
667                pub inertia: Inertia,
668            }
669            impl ComputeInertia for MockInertia {
670                fn compute_inertia(&self) -> Inertia {
671                    self.inertia.clone()
672                }
673            }
674
675            let inertias = &[
676                MockInertia {
677                    inertia: Cuboid::UNIT.compute_inertia(),
678                },
679                MockInertia {
680                    inertia: Inertia::ZERO,
681                },
682            ];
683            let poses = &[
684                Isometry3::from_parts(vec3(-1., 1., 0.).into(), UnitQuat::identity()),
685                Isometry3::from_parts(vec3(2., 1., 0.).into(), UnitQuat::identity()),
686            ];
687            let masses = &[1.0, 0.0];
688            let (
689                Inertia {
690                    diag_inertia,
691                    center_of_mass,
692                    inertia_rotation,
693                },
694                mass,
695            ) = compound(inertias, poses, masses, true);
696            assert_eq!(mass, 0.0);
697            assert_eq!(diag_inertia, Vec3::zeros());
698            assert_eq!(center_of_mass, None);
699            assert_eq!(inertia_rotation, None);
700
701            let (Inertia { diag_inertia, .. }, mass) = compound(inertias, poses, masses, false);
702            assert_eq!(mass, 1.0);
703            assert_relative_eq!(
704                diag_inertia,
705                inertias[0].inertia.diag_inertia,
706                epsilon = 1e-5
707            );
708        }
709
710        #[test]
711        fn test_compound_empty_shape() {
712            let shapes: &[Sphere; 0] = &[];
713            let poses = &[];
714            let masses = &[];
715            let (
716                Inertia {
717                    diag_inertia,
718                    inertia_rotation,
719                    center_of_mass,
720                },
721                mass,
722            ) = compound(shapes, poses, masses, false);
723            assert_eq!(mass, 1.0);
724            assert_eq!(diag_inertia, crate::sphere(0.5));
725            assert_eq!(inertia_rotation, None);
726            assert_eq!(center_of_mass, None);
727        }
728    }
729
730    #[test]
731    fn test_diagonalize_inertia() {
732        let inertia = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
733        let (diag, rot) = diagonalize_inertia(inertia);
734        let inertia_matrix = Mat3::from_column_slice(&[
735            inertia[0], inertia[1], inertia[2], inertia[1], inertia[3], inertia[4], inertia[2],
736            inertia[4], inertia[5],
737        ]);
738
739        let diag_matrix = Mat3::from_diagonal(&diag);
740        let rot_matrix = Mat3::from(rot);
741        let result = rot_matrix * diag_matrix * rot_matrix.transpose();
742        assert_relative_eq!(inertia_matrix, result, epsilon = 1e-5);
743    }
744}