parry3d/mass_properties/
mass_properties_trimesh3d.rs

1use crate::mass_properties::MassProperties;
2use crate::math::{Matrix, Real, Vector, DIM};
3use crate::shape::Tetrahedron;
4use num::Zero;
5
6impl MassProperties {
7    /// Computes the mass properties of a triangle mesh.
8    pub fn from_trimesh(
9        density: Real,
10        vertices: &[Vector],
11        indices: &[[u32; DIM]],
12    ) -> MassProperties {
13        let (volume, com) = trimesh_signed_volume_and_center_of_mass(vertices, indices);
14
15        if volume.is_zero() {
16            return MassProperties::zero();
17        }
18
19        let mut itot = Matrix::ZERO;
20
21        for t in indices {
22            let p2 = vertices[t[0] as usize];
23            let p3 = vertices[t[1] as usize];
24            let p4 = vertices[t[2] as usize];
25
26            let vol = Tetrahedron::new(com, p2, p3, p4).signed_volume();
27            let ipart = tetrahedron_unit_inertia_tensor_wrt_point(com, com, p2, p3, p4);
28
29            itot += ipart * vol;
30        }
31
32        let sign = volume.signum();
33        Self::with_inertia_matrix(com, volume * density * sign, itot * density * sign)
34    }
35}
36
37/// Computes the unit inertia tensor of a tetrahedron, with regard to the given `point`.
38pub fn tetrahedron_unit_inertia_tensor_wrt_point(
39    point: Vector,
40    p1: Vector,
41    p2: Vector,
42    p3: Vector,
43    p4: Vector,
44) -> Matrix {
45    let p1 = p1 - point;
46    let p2 = p2 - point;
47    let p3 = p3 - point;
48    let p4 = p4 - point;
49
50    // Just for readability.
51    let x1 = p1[0];
52    let y1 = p1[1];
53    let z1 = p1[2];
54    let x2 = p2[0];
55    let y2 = p2[1];
56    let z2 = p2[2];
57    let x3 = p3[0];
58    let y3 = p3[1];
59    let z3 = p3[2];
60    let x4 = p4[0];
61    let y4 = p4[1];
62    let z4 = p4[2];
63
64    let diag_x = x1 * x1
65        + x1 * x2
66        + x2 * x2
67        + x1 * x3
68        + x2 * x3
69        + x3 * x3
70        + x1 * x4
71        + x2 * x4
72        + x3 * x4
73        + x4 * x4;
74    let diag_y = y1 * y1
75        + y1 * y2
76        + y2 * y2
77        + y1 * y3
78        + y2 * y3
79        + y3 * y3
80        + y1 * y4
81        + y2 * y4
82        + y3 * y4
83        + y4 * y4;
84    let diag_z = z1 * z1
85        + z1 * z2
86        + z2 * z2
87        + z1 * z3
88        + z2 * z3
89        + z3 * z3
90        + z1 * z4
91        + z2 * z4
92        + z3 * z4
93        + z4 * z4;
94
95    let a0 = (diag_y + diag_z) * 0.1;
96    let b0 = (diag_z + diag_x) * 0.1;
97    let c0 = (diag_x + diag_y) * 0.1;
98
99    let a1 = (y1 * z1 * 2.0
100        + y2 * z1
101        + y3 * z1
102        + y4 * z1
103        + y1 * z2
104        + y2 * z2 * 2.0
105        + y3 * z2
106        + y4 * z2
107        + y1 * z3
108        + y2 * z3
109        + y3 * z3 * 2.0
110        + y4 * z3
111        + y1 * z4
112        + y2 * z4
113        + y3 * z4
114        + y4 * z4 * 2.0)
115        * 0.05;
116    let b1 = (x1 * z1 * 2.0
117        + x2 * z1
118        + x3 * z1
119        + x4 * z1
120        + x1 * z2
121        + x2 * z2 * 2.0
122        + x3 * z2
123        + x4 * z2
124        + x1 * z3
125        + x2 * z3
126        + x3 * z3 * 2.0
127        + x4 * z3
128        + x1 * z4
129        + x2 * z4
130        + x3 * z4
131        + x4 * z4 * 2.0)
132        * 0.05;
133    let c1 = (x1 * y1 * 2.0
134        + x2 * y1
135        + x3 * y1
136        + x4 * y1
137        + x1 * y2
138        + x2 * y2 * 2.0
139        + x3 * y2
140        + x4 * y2
141        + x1 * y3
142        + x2 * y3
143        + x3 * y3 * 2.0
144        + x4 * y3
145        + x1 * y4
146        + x2 * y4
147        + x3 * y4
148        + x4 * y4 * 2.0)
149        * 0.05;
150
151    Matrix::from_cols_array(&[a0, -c1, -b1, -c1, b0, -a1, -b1, -a1, c0])
152}
153
154/// Computes the volume and center-of-mass of a mesh.
155pub fn trimesh_signed_volume_and_center_of_mass(
156    vertices: &[Vector],
157    indices: &[[u32; DIM]],
158) -> (Real, Vector) {
159    let geometric_center = crate::utils::center(vertices);
160
161    let mut res = Vector::ZERO;
162    let mut vol = 0.0;
163
164    for t in indices {
165        let p2 = vertices[t[0] as usize];
166        let p3 = vertices[t[1] as usize];
167        let p4 = vertices[t[2] as usize];
168
169        let volume = Tetrahedron::new(geometric_center, p2, p3, p4).signed_volume();
170        let center = Tetrahedron::new(geometric_center, p2, p3, p4).center();
171
172        res += center * volume;
173        vol += volume;
174    }
175
176    if vol.is_zero() {
177        (vol, geometric_center)
178    } else {
179        (vol, res / vol)
180    }
181}
182
183#[cfg(test)]
184mod test {
185    use crate::math::{Pose, Rotation, Vector, VectorExt};
186    use crate::{
187        mass_properties::MassProperties,
188        shape::{Ball, Capsule, Cone, Cuboid, Cylinder, Shape},
189    };
190    use std::dbg;
191
192    fn assert_same_principal_inertias(mprops1: &MassProperties, mprops2: &MassProperties) {
193        for k in 0..3 {
194            let i1 = mprops1.principal_inertia_local_frame
195                * (mprops1.principal_inertia()
196                    * (mprops1.principal_inertia_local_frame.inverse() * Vector::ith(k, 1.0)));
197            let i2 = mprops2.principal_inertia_local_frame
198                * (mprops2.principal_inertia()
199                    * (mprops2.principal_inertia_local_frame.inverse() * Vector::ith(k, 1.0)));
200            assert!(i1.abs_diff_eq(i2, 0.5))
201        }
202    }
203
204    #[test]
205    fn cuboid_as_trimesh_mprops() {
206        let cuboid = Cuboid::new(Vector::new(1.0, 2.0, 3.0));
207
208        use crate::shape::Shape;
209        let orig_mprops = cuboid.mass_properties(1.0);
210        dbg!(orig_mprops.principal_inertia());
211
212        let mut trimesh = cuboid.to_trimesh();
213        let mprops = MassProperties::from_trimesh(1.0, &trimesh.0, &trimesh.1);
214        assert_relative_eq!(mprops.mass(), 48.0, epsilon = 1.0e-4);
215        assert!(
216            (mprops.principal_inertia_local_frame * mprops.principal_inertia())
217                .abs()
218                .abs_diff_eq(Vector::new(208.0, 160.0, 80.0), 1.0e-4)
219        );
220
221        // Check after shifting the trimesh off the origin.
222        trimesh
223            .0
224            .iter_mut()
225            .for_each(|pt| *pt += Vector::new(30.0, 20.0, 10.0));
226        let mprops = MassProperties::from_trimesh(1.0, &trimesh.0, &trimesh.1);
227        assert_relative_eq!(mprops.mass(), 48.0, epsilon = 1.0e-4);
228        assert!(
229            (mprops.principal_inertia_local_frame * mprops.principal_inertia())
230                .abs()
231                .abs_diff_eq(Vector::new(208.0, 160.0, 80.0), 1.0e-4)
232        );
233    }
234
235    #[test]
236    fn primitives_as_trimesh_mprops() {
237        let primitives = (
238            Cuboid::new(Vector::new(1.0, 2.0, 3.0)),
239            Capsule::new_y(2.0, 1.0),
240            Cone::new(2.0, 1.0),
241            Cylinder::new(2.0, 1.0),
242            Ball::new(2.0),
243        );
244        let mut meshes = [
245            primitives.0.to_trimesh(),
246            primitives.1.to_trimesh(100, 100),
247            primitives.2.to_trimesh(100),
248            primitives.3.to_trimesh(100),
249            primitives.4.to_trimesh(100, 100),
250        ];
251        let shapes = [
252            &primitives.0 as &dyn Shape,
253            &primitives.1 as &dyn Shape,
254            &primitives.2 as &dyn Shape,
255            &primitives.3 as &dyn Shape,
256            &primitives.4 as &dyn Shape,
257        ];
258
259        for (shape, mesh) in shapes.iter().zip(meshes.iter_mut()) {
260            let shape_mprops = shape.mass_properties(2.0);
261            let mesh_mprops = MassProperties::from_trimesh(2.0, &mesh.0, &mesh.1);
262            assert_relative_eq!(shape_mprops.mass(), mesh_mprops.mass(), epsilon = 1.0e-1);
263            assert_same_principal_inertias(&shape_mprops, &mesh_mprops);
264            assert_relative_eq!(
265                shape_mprops.local_com,
266                mesh_mprops.local_com,
267                epsilon = 1.0e-3
268            );
269
270            // Now try with a shifted mesh.
271            let shift = Vector::new(33.0, 22.0, 11.0);
272            mesh.0.iter_mut().for_each(|pt| *pt += shift);
273
274            let mesh_mprops = MassProperties::from_trimesh(2.0, &mesh.0, &mesh.1);
275
276            assert_relative_eq!(shape_mprops.mass(), mesh_mprops.mass(), epsilon = 1.0e-1);
277            assert_same_principal_inertias(&shape_mprops, &mesh_mprops);
278            assert_relative_eq!(
279                shape_mprops.local_com + shift, // The mesh is shifted, so its center-of-mass is shifted too.
280                mesh_mprops.local_com,
281                epsilon = 1.0e-3
282            );
283        }
284    }
285
286    // Regression test for https://github.com/dimforge/parry/pull/331
287    //
288    // We compare the angular inertia tensor of a rotated & translated Cuboid, with the
289    // inertia tensor of the same cuboid represented as a `TriMesh` with rotated & translated
290    // vertices.
291    #[test]
292    fn rotated_inertia_tensor() {
293        let cuboid = Cuboid::new(Vector::new(3.0, 2.0, 1.0));
294        let density = 1.0;
295
296        // Compute mass properties with a translated and rotated cuboid.
297        let pose = Pose::new(Vector::new(5.0, 2.0, 3.0), Vector::new(0.6, 0.7, 0.8));
298        let mprops = cuboid.mass_properties(density);
299
300        // Compute mass properties with a manually transformed cuboid.
301        let mut vertices = cuboid.to_trimesh().0;
302        let trimesh_origin_mprops =
303            MassProperties::from_trimesh(density, &vertices, &cuboid.to_trimesh().1);
304        vertices.iter_mut().for_each(|v| *v = pose * *v);
305        let trimesh_transformed_mprops =
306            MassProperties::from_trimesh(density, &vertices, &cuboid.to_trimesh().1);
307
308        // Compare the mass properties.
309        assert_relative_eq!(
310            mprops.mass(),
311            trimesh_origin_mprops.mass(),
312            epsilon = 1.0e-4
313        );
314        assert_relative_eq!(
315            mprops.mass(),
316            trimesh_transformed_mprops.mass(),
317            epsilon = 1.0e-4
318        );
319        // compare local_com
320        assert_relative_eq!(
321            pose * mprops.local_com,
322            trimesh_transformed_mprops.local_com,
323            epsilon = 1.0e-4
324        );
325        assert_relative_eq!(
326            pose * trimesh_origin_mprops.local_com,
327            trimesh_transformed_mprops.local_com,
328            epsilon = 1.0e-4
329        );
330        assert!(trimesh_origin_mprops
331            .principal_inertia()
332            .abs_diff_eq(mprops.principal_inertia(), 1.0e-4));
333        let w1 = trimesh_origin_mprops.world_inv_inertia(&pose.rotation);
334        let w2 = trimesh_transformed_mprops.world_inv_inertia(&Rotation::IDENTITY);
335        assert_relative_eq!(w1.m11, w2.m11, epsilon = 1.0e-7);
336        assert_relative_eq!(w1.m12, w2.m12, epsilon = 1.0e-7);
337        assert_relative_eq!(w1.m13, w2.m13, epsilon = 1.0e-7);
338        assert_relative_eq!(w1.m22, w2.m22, epsilon = 1.0e-7);
339        assert_relative_eq!(w1.m23, w2.m23, epsilon = 1.0e-7);
340        assert_relative_eq!(w1.m33, w2.m33, epsilon = 1.0e-7);
341    }
342}