bevy_heavy/dim3/
impls.rs

1use super::{AngularInertiaTensor, ComputeMassProperties3d, MassProperties3d};
2use bevy_math::{
3    ops,
4    prelude::Tetrahedron,
5    primitives::{
6        Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Measured3d, Plane3d, Polyline3d,
7        Segment3d, Sphere, Torus, Triangle3d,
8    },
9    FloatPow, Quat, Vec3,
10};
11use glam_matrix_extras::SymmetricMat3;
12
13impl ComputeMassProperties3d for Sphere {
14    #[inline]
15    fn mass(&self, density: f32) -> f32 {
16        self.volume() * density
17    }
18
19    #[inline]
20    fn unit_principal_angular_inertia(&self) -> Vec3 {
21        Vec3::splat(0.4 * self.radius.squared())
22    }
23
24    #[inline]
25    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
26        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
27    }
28
29    #[inline]
30    fn center_of_mass(&self) -> Vec3 {
31        Vec3::ZERO
32    }
33}
34
35impl ComputeMassProperties3d for Cuboid {
36    #[inline]
37    fn mass(&self, density: f32) -> f32 {
38        self.volume() * density
39    }
40
41    #[inline]
42    fn unit_principal_angular_inertia(&self) -> Vec3 {
43        let ix = self.half_size.x.squared() / 3.0;
44        let iy = self.half_size.y.squared() / 3.0;
45        let iz = self.half_size.z.squared() / 3.0;
46        Vec3::new(iy + iz, ix + iz, ix + iy)
47    }
48
49    #[inline]
50    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
51        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
52    }
53
54    #[inline]
55    fn center_of_mass(&self) -> Vec3 {
56        Vec3::ZERO
57    }
58}
59
60impl ComputeMassProperties3d for Cylinder {
61    #[inline]
62    fn mass(&self, density: f32) -> f32 {
63        self.volume() * density
64    }
65
66    #[inline]
67    fn unit_principal_angular_inertia(&self) -> Vec3 {
68        let radius_squared = self.radius.squared();
69        let height_squared = self.half_height.squared() * 4.0;
70        let principal = radius_squared / 2.0;
71        let off_principal = (radius_squared * 3.0 + height_squared) / 12.0;
72        Vec3::new(off_principal, principal, off_principal)
73    }
74
75    #[inline]
76    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
77        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
78    }
79
80    #[inline]
81    fn center_of_mass(&self) -> Vec3 {
82        Vec3::ZERO
83    }
84}
85
86impl ComputeMassProperties3d for Capsule3d {
87    #[inline]
88    fn mass(&self, density: f32) -> f32 {
89        self.volume() * density
90    }
91
92    #[inline]
93    fn unit_principal_angular_inertia(&self) -> Vec3 {
94        // The cylinder and hemisphere parts
95        let cylinder = Cylinder {
96            radius: self.radius,
97            half_height: self.half_length,
98        };
99        let cylinder_length = self.half_length * 2.0;
100        let sphere = Sphere::new(self.radius);
101
102        // Volumes
103        let cylinder_volume = cylinder.volume();
104        let sphere_volume = sphere.volume();
105
106        // Masses
107        let density = 1.0 / (cylinder_volume + sphere_volume);
108        let cylinder_mass = cylinder_volume * density;
109        let sphere_mass = sphere_volume * density;
110
111        // Principal inertias
112        let cylinder_inertia = cylinder.principal_angular_inertia(cylinder_mass);
113        let sphere_inertia = sphere.principal_angular_inertia(sphere_mass);
114
115        // Total inertia
116        let mut capsule_inertia = cylinder_inertia + sphere_inertia;
117
118        // Compensate for the hemispheres being away from the rotation axis using the parallel axis theorem.
119        let extra = (cylinder_length.squared() * 0.25 + cylinder_length * self.radius * 3.0 / 8.0)
120            * sphere_mass;
121        capsule_inertia.x += extra;
122        capsule_inertia.z += extra;
123
124        capsule_inertia
125    }
126
127    #[inline]
128    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
129        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
130    }
131
132    #[inline]
133    fn center_of_mass(&self) -> Vec3 {
134        Vec3::ZERO
135    }
136
137    #[inline]
138    fn mass_properties(&self, density: f32) -> MassProperties3d {
139        // The cylinder and hemisphere parts
140        let cylinder = Cylinder {
141            radius: self.radius,
142            half_height: self.half_length,
143        };
144        let cylinder_length = self.half_length * 2.0;
145        let sphere = Sphere::new(self.radius);
146
147        // Volumes
148        let cylinder_volume = cylinder.volume();
149        let sphere_volume = sphere.volume();
150
151        // Masses
152        let cylinder_mass = cylinder_volume * density;
153        let sphere_mass = sphere_volume * density;
154
155        // Principal inertias
156        let cylinder_inertia = cylinder.principal_angular_inertia(cylinder_mass);
157        let sphere_inertia = sphere.principal_angular_inertia(sphere_mass);
158
159        // Total inertia
160        let mut capsule_inertia = cylinder_inertia + sphere_inertia;
161
162        // Compensate for the hemispheres being away from the rotation axis using the parallel axis theorem.
163        let extra = (cylinder_length.squared() * 0.25 + cylinder_length * self.radius * 3.0 / 8.0)
164            * sphere_mass;
165        capsule_inertia.x += extra;
166        capsule_inertia.z += extra;
167
168        MassProperties3d::new(cylinder_mass + sphere_mass, capsule_inertia, Vec3::ZERO)
169    }
170}
171
172impl ComputeMassProperties3d for Cone {
173    #[inline]
174    fn mass(&self, density: f32) -> f32 {
175        self.volume() * density
176    }
177
178    #[inline]
179    fn unit_principal_angular_inertia(&self) -> Vec3 {
180        let radius_squared = self.radius.squared();
181        let height_squared = self.height.squared();
182
183        // About the Y axis
184        let principal = 3.0 / 10.0 * radius_squared;
185
186        // About the X or Z axis passing through the center of mass
187        let off_principal = principal * 0.5 + 3.0 / 80.0 * height_squared;
188
189        Vec3::new(off_principal, principal, off_principal)
190    }
191
192    #[inline]
193    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
194        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
195    }
196
197    #[inline]
198    fn center_of_mass(&self) -> Vec3 {
199        Vec3::new(0.0, -self.height * 0.25, 0.0)
200    }
201}
202
203impl ComputeMassProperties3d for ConicalFrustum {
204    #[inline]
205    fn mass(&self, density: f32) -> f32 {
206        if self.radius_top == self.radius_bottom {
207            Cylinder::new(self.radius_top, self.height).mass(density)
208        } else {
209            // https://mathworld.wolfram.com/ConicalFrustum.html
210            let radii_squared = self.radius_top.squared() + self.radius_bottom.squared();
211            let volume = core::f32::consts::FRAC_PI_3
212                * self.height
213                * (radii_squared + self.radius_top * self.radius_bottom);
214            volume * density
215        }
216    }
217
218    #[inline]
219    fn unit_principal_angular_inertia(&self) -> Vec3 {
220        if self.radius_top == self.radius_bottom {
221            Cylinder::new(self.radius_top, self.height).unit_principal_angular_inertia()
222        } else {
223            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
224                (self.radius_top, self.radius_bottom)
225            } else {
226                (self.radius_bottom, self.radius_top)
227            };
228
229            // TODO: The principal angular inertia along the symmetry axis Y is straightforward to compute directly.
230            //       However, I have not found a correct formula for the principal angular inertia along the other axes.
231            //       It would be much more efficient to compute directly instead of subtracting cones like what we're doing here though.
232            // let principal_y = 3.0 * (max_radius.powi(5) - min_radius.powi(5))
233            //     / (10.0 * (max_radius.powi(3) - min_radius.powi(3)));
234
235            // The conical frustum can be thought of as a cone with a smaller cone subtracted from it.
236            // To get its angular inertia, we can subtract the angular inertia of the small cone
237            // from the angular inertia of the small cone.
238
239            // Create the large and small cone.
240            let cone_height =
241                max_radius * (self.height / ops::abs(self.radius_top - self.radius_bottom));
242            let large_cone = Cone {
243                radius: max_radius,
244                height: cone_height,
245            };
246            let small_cone = Cone {
247                radius: min_radius,
248                height: cone_height - self.height,
249            };
250
251            // Compute the volumes of the large and small cone and the frustum.
252            // These are equivalent to the masses when using a uniform density of `1.0`.
253            let large_cone_volume = large_cone.volume();
254            let small_cone_volume = small_cone.volume();
255            let volume = large_cone_volume - small_cone_volume;
256
257            // The total mass of the frustum is `1.0` in this case, so we just want the fractional volumes
258            // for determining how much each cone contributes to the angular inertia.
259            let large_cone_volume_fraction = large_cone_volume / volume;
260            let small_cone_volume_fraction = small_cone_volume / volume;
261
262            let large_cone_angular_inertia =
263                large_cone.principal_angular_inertia(large_cone_volume_fraction);
264            let mut small_cone_angular_inertia =
265                small_cone.principal_angular_inertia(small_cone_volume_fraction);
266
267            // The small cone's center of mass is offset from the frustum's center of mass,
268            // so we need to take the parallel axis theorem (also known as Steiner's theorem) into account.
269            //
270            // I = I_cm + m * d^2
271            //
272            // - `I_cm` is the angular inertia about the center of mass
273            // - `m` is the mass, `small_cone_fraction` in this case
274            // - `d` is the distance between the parallel axes
275            let d = 0.5 * (self.height + small_cone.height) + small_cone.center_of_mass().y;
276            let extra_angular_inertia = small_cone_volume_fraction * d * d;
277            small_cone_angular_inertia.x += extra_angular_inertia;
278            small_cone_angular_inertia.z += extra_angular_inertia;
279
280            // Return the total principal angular inertia.
281            large_cone_angular_inertia.x - small_cone_angular_inertia
282        }
283    }
284
285    #[inline]
286    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
287        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
288    }
289
290    #[inline]
291    fn center_of_mass(&self) -> Vec3 {
292        if self.radius_top == self.radius_bottom {
293            Vec3::ZERO
294        } else {
295            // Adapted from: https://mathworld.wolfram.com/ConicalFrustum.html
296
297            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
298                (self.radius_top, self.radius_bottom)
299            } else {
300                (self.radius_bottom, self.radius_top)
301            };
302
303            let min_radius_squared = min_radius.squared();
304            let max_radius_squared = max_radius.squared();
305            let radii_product = self.radius_top * self.radius_bottom;
306
307            let y = self.height
308                * ((max_radius_squared + 2.0 * radii_product + 3.0 * min_radius_squared)
309                    / (4.0 * (max_radius_squared + radii_product + min_radius_squared))
310                    - 0.5);
311
312            Vec3::new(0.0, y, 0.0)
313        }
314    }
315
316    #[inline]
317    fn mass_properties(&self, density: f32) -> MassProperties3d {
318        if self.radius_top == self.radius_bottom {
319            Cylinder::new(self.radius_top, self.height).mass_properties(density)
320        } else {
321            // This is a combination of all the methods above, sharing variables where possible.
322
323            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
324                (self.radius_top, self.radius_bottom)
325            } else {
326                (self.radius_bottom, self.radius_top)
327            };
328
329            // The conical frustum can be thought of as a cone with a smaller cone subtracted from it.
330            // To get its angular inertia, we can subtract the angular inertia of the small cone
331            // from the angular inertia of the small cone.
332
333            // Create the large and small cone.
334            let cone_height =
335                max_radius * (self.height / ops::abs(self.radius_top - self.radius_bottom));
336            let large_cone = Cone {
337                radius: max_radius,
338                height: cone_height,
339            };
340            let small_cone = Cone {
341                radius: min_radius,
342                height: cone_height - self.height,
343            };
344
345            // Compute the volumes of the large and small cone and the frustum.
346            // These are equivalent to the masses when using a uniform density of `1.0`.
347            let large_cone_volume = large_cone.volume();
348            let small_cone_volume = small_cone.volume();
349            let volume = large_cone_volume - small_cone_volume;
350
351            // Compute the mass.
352            let mass = volume * density;
353
354            // Compute how much each cone contributes to the angular inertia.
355            let large_cone_volume_fraction = large_cone_volume / volume;
356            let small_cone_volume_fraction = small_cone_volume / volume;
357
358            let large_cone_angular_inertia =
359                large_cone.principal_angular_inertia(large_cone_volume_fraction);
360            let mut small_cone_angular_inertia =
361                small_cone.principal_angular_inertia(small_cone_volume_fraction);
362
363            // The small cone's center of mass is offset from the frustum's center of mass,
364            // so we need to take the parallel axis theorem (also known as Steiner's theorem) into account.
365            //
366            // I = I_cm + m * d^2
367            //
368            // - `I_cm` is the angular inertia about the center of mass
369            // - `m` is the mass, `small_cone_fraction` in this case
370            // - `d` is the distance between the parallel axes
371            let d = 0.5 * (self.height + small_cone.height) + small_cone.center_of_mass().y;
372            let extra_angular_inertia = small_cone_volume_fraction * d * d;
373            small_cone_angular_inertia.x += extra_angular_inertia;
374            small_cone_angular_inertia.z += extra_angular_inertia;
375
376            // Return the total principal angular inertia.
377            let principal_angular_inertia =
378                mass * (large_cone_angular_inertia.x - small_cone_angular_inertia);
379
380            let min_radius_squared = min_radius.squared();
381            let max_radius_squared = max_radius.squared();
382            let radii_product = self.radius_top * self.radius_bottom;
383
384            // Compute the center of mass.
385            let y = self.height
386                * ((max_radius_squared + 2.0 * radii_product + 3.0 * min_radius_squared)
387                    / (4.0 * (max_radius_squared + radii_product + min_radius_squared))
388                    - 0.5);
389            let center_of_mass = Vec3::new(0.0, y, 0.0);
390
391            MassProperties3d::new(mass, principal_angular_inertia, center_of_mass)
392        }
393    }
394}
395
396impl ComputeMassProperties3d for Torus {
397    #[inline]
398    fn mass(&self, density: f32) -> f32 {
399        self.volume() * density
400    }
401
402    #[inline]
403    fn unit_principal_angular_inertia(&self) -> Vec3 {
404        // Reference: https://en.wikipedia.org/wiki/List_of_moments_of_inertia
405
406        let major_radius_squared = self.major_radius.squared();
407        let minor_radius_squared = self.minor_radius.squared();
408
409        let principal = 0.25 * (4.0 * major_radius_squared + 3.0 * minor_radius_squared);
410        let off_principal = 1.0 / 8.0 * (4.0 * major_radius_squared + 5.0 * minor_radius_squared);
411        Vec3::new(off_principal, principal, off_principal)
412    }
413
414    #[inline]
415    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
416        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
417    }
418
419    #[inline]
420    fn center_of_mass(&self) -> Vec3 {
421        Vec3::ZERO
422    }
423}
424
425impl ComputeMassProperties3d for Tetrahedron {
426    #[inline]
427    fn mass(&self, density: f32) -> f32 {
428        self.volume() * density
429    }
430
431    #[inline]
432    fn unit_principal_angular_inertia(&self) -> Vec3 {
433        let tensor = self.unit_angular_inertia_tensor();
434        tensor.principal_angular_inertia_with_local_frame().0
435    }
436
437    #[inline]
438    fn local_inertial_frame(&self) -> Quat {
439        let tensor = self.unit_angular_inertia_tensor();
440        tensor.principal_angular_inertia_with_local_frame().1
441    }
442
443    #[inline]
444    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
445        // References:
446        // - F. Tonon. "Explicit Exact Formulas for the 3-D Tetrahedron Inertia Tensor in Terms of its Vertex Coordinates"
447        // - Parry: https://github.com/dimforge/parry/blob/837291f1a051dc04e6e4ab8bc2e5b438ce66d257/src/mass_properties/mass_properties_trimesh3d.rs#L38
448
449        let [p1, p2, p3, p4] = self.vertices;
450
451        // For readability
452        let x1 = p1.x;
453        let y1 = p1.y;
454        let z1 = p1.z;
455        let x2 = p2.x;
456        let y2 = p2.y;
457        let z2 = p2.z;
458        let x3 = p3.x;
459        let y3 = p3.y;
460        let z3 = p3.z;
461        let x4 = p4.x;
462        let y4 = p4.y;
463        let z4 = p4.z;
464
465        let diag_x = x1 * x1
466            + x1 * x2
467            + x2 * x2
468            + x1 * x3
469            + x2 * x3
470            + x3 * x3
471            + x1 * x4
472            + x2 * x4
473            + x3 * x4
474            + x4 * x4;
475        let diag_y = y1 * y1
476            + y1 * y2
477            + y2 * y2
478            + y1 * y3
479            + y2 * y3
480            + y3 * y3
481            + y1 * y4
482            + y2 * y4
483            + y3 * y4
484            + y4 * y4;
485        let diag_z = z1 * z1
486            + z1 * z2
487            + z2 * z2
488            + z1 * z3
489            + z2 * z3
490            + z3 * z3
491            + z1 * z4
492            + z2 * z4
493            + z3 * z4
494            + z4 * z4;
495
496        let a0 = (diag_y + diag_z) * 0.1;
497        let b0 = (diag_z + diag_x) * 0.1;
498        let c0 = (diag_x + diag_y) * 0.1;
499
500        let a1 = (y1 * z1 * 2.0
501            + y2 * z1
502            + y3 * z1
503            + y4 * z1
504            + y1 * z2
505            + y2 * z2 * 2.0
506            + y3 * z2
507            + y4 * z2
508            + y1 * z3
509            + y2 * z3
510            + y3 * z3 * 2.0
511            + y4 * z3
512            + y1 * z4
513            + y2 * z4
514            + y3 * z4
515            + y4 * z4 * 2.0)
516            * 0.05;
517        let b1 = (x1 * z1 * 2.0
518            + x2 * z1
519            + x3 * z1
520            + x4 * z1
521            + x1 * z2
522            + x2 * z2 * 2.0
523            + x3 * z2
524            + x4 * z2
525            + x1 * z3
526            + x2 * z3
527            + x3 * z3 * 2.0
528            + x4 * z3
529            + x1 * z4
530            + x2 * z4
531            + x3 * z4
532            + x4 * z4 * 2.0)
533            * 0.05;
534        let c1 = (x1 * y1 * 2.0
535            + x2 * y1
536            + x3 * y1
537            + x4 * y1
538            + x1 * y2
539            + x2 * y2 * 2.0
540            + x3 * y2
541            + x4 * y2
542            + x1 * y3
543            + x2 * y3
544            + x3 * y3 * 2.0
545            + x4 * y3
546            + x1 * y4
547            + x2 * y4
548            + x3 * y4
549            + x4 * y4 * 2.0)
550            * 0.05;
551
552        // TODO: Are -b1 and -c1 flipped here?
553        AngularInertiaTensor::from_symmetric_mat3(SymmetricMat3::new(a0, -b1, -c1, b0, -a1, c0))
554    }
555
556    #[inline]
557    fn center_of_mass(&self) -> Vec3 {
558        (self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0
559    }
560
561    #[inline]
562    fn mass_properties(&self, density: f32) -> MassProperties3d {
563        let volume = self.volume();
564        let center_of_mass = self.center_of_mass();
565
566        if volume < f32::EPSILON {
567            return MassProperties3d::new(0.0, Vec3::ZERO, center_of_mass);
568        }
569
570        let mass = volume * density;
571        let tensor = self.angular_inertia_tensor(mass);
572
573        MassProperties3d::new_with_angular_inertia_tensor(mass, tensor, center_of_mass)
574    }
575}
576
577macro_rules! impl_zero_mass_properties_3d {
578    ($($shape:ty),*) => {
579        $(
580            impl ComputeMassProperties3d for $shape {
581                #[inline]
582                fn mass(&self, _density: f32) -> f32 {
583                    0.0
584                }
585
586                #[inline]
587                fn unit_principal_angular_inertia(&self) -> Vec3 {
588                    Vec3::ZERO
589                }
590
591                #[inline]
592                fn principal_angular_inertia(&self, _mass: f32) -> Vec3 {
593                    Vec3::ZERO
594                }
595
596                #[inline]
597                fn local_inertial_frame(&self) -> Quat {
598                    Quat::IDENTITY
599                }
600
601                #[inline]
602                fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
603                    AngularInertiaTensor::ZERO
604                }
605
606                #[inline]
607                fn angular_inertia_tensor(&self, _mass: f32) -> AngularInertiaTensor {
608                    AngularInertiaTensor::ZERO
609                }
610
611                #[inline]
612                fn center_of_mass(&self) -> Vec3 {
613                    Vec3::ZERO
614                }
615
616                #[inline]
617                fn mass_properties(&self, _density: f32) -> MassProperties3d {
618                    MassProperties3d::ZERO
619                }
620            }
621        )*
622    };
623}
624
625impl_zero_mass_properties_3d!(Plane3d);
626impl_zero_mass_properties_3d!(Line3d);
627impl_zero_mass_properties_3d!(Segment3d);
628impl_zero_mass_properties_3d!(Triangle3d);
629
630impl ComputeMassProperties3d for Polyline3d {
631    #[inline]
632    fn mass(&self, _density: f32) -> f32 {
633        0.0
634    }
635
636    #[inline]
637    fn unit_principal_angular_inertia(&self) -> Vec3 {
638        Vec3::ZERO
639    }
640
641    #[inline]
642    fn principal_angular_inertia(&self, _mass: f32) -> Vec3 {
643        Vec3::ZERO
644    }
645
646    #[inline]
647    fn local_inertial_frame(&self) -> bevy_math::Quat {
648        Quat::IDENTITY
649    }
650
651    #[inline]
652    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
653        AngularInertiaTensor::ZERO
654    }
655
656    #[inline]
657    fn angular_inertia_tensor(&self, _mass: f32) -> AngularInertiaTensor {
658        AngularInertiaTensor::ZERO
659    }
660
661    #[inline]
662    fn center_of_mass(&self) -> Vec3 {
663        Vec3::ZERO
664    }
665
666    #[inline]
667    fn mass_properties(&self, _density: f32) -> MassProperties3d {
668        MassProperties3d::ZERO
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use alloc::vec::Vec;
675
676    use approx::assert_relative_eq;
677    use bevy_math::{
678        bounding::{Bounded3d, BoundingVolume},
679        Isometry3d, ShapeSample, Vec3Swizzles,
680    };
681    use rand::SeedableRng;
682
683    use super::*;
684
685    macro_rules! test_shape {
686        ($test_name:tt, $shape:expr, $point_generator:expr) => {
687            #[test]
688            fn $test_name() {
689                let shape = $shape;
690
691                // Sample enough points to have a close enough point cloud representation of the shape.
692                let points: Vec<Vec3> = $point_generator(&shape);
693
694                // Compute the mass properties to test.
695                let density = 2.0;
696                let mass = shape.mass(density);
697                let principal_angular_inertia = shape.principal_angular_inertia(mass);
698                let local_inertial_frame = shape.local_inertial_frame();
699                let angular_inertia_tensor = shape.angular_inertia_tensor(mass);
700                let center_of_mass = shape.center_of_mass();
701
702                // First, test that the individually computed properties match the full properties.
703                let mass_props = shape.mass_properties(density);
704                assert_relative_eq!(mass, mass_props.mass);
705                assert_relative_eq!(
706                    principal_angular_inertia,
707                    mass_props.principal_angular_inertia
708                );
709                assert_relative_eq!(center_of_mass, mass_props.center_of_mass);
710
711                // Estimate the expected mass properties using the point cloud.
712                // Note: We could also approximate the mass using Monte Carlo integration.
713                //       This would require point containment checks.
714                let expected =
715                    MassProperties3d::from_point_cloud(&points, mass, local_inertial_frame);
716
717                assert_relative_eq!(mass, expected.mass);
718                assert_relative_eq!(
719                    principal_angular_inertia,
720                    expected.principal_angular_inertia,
721                    epsilon = 0.1
722                );
723                assert_relative_eq!(center_of_mass, expected.center_of_mass, epsilon = 0.01);
724
725                // Check that the computed tensor matches the tensor computed from
726                // the principal moments of inertia and the local inertial frame.
727                assert_relative_eq!(
728                    angular_inertia_tensor,
729                    AngularInertiaTensor::new_with_local_frame(
730                        principal_angular_inertia,
731                        local_inertial_frame
732                    ),
733                    epsilon = 1e-5
734                );
735
736                // Check that the principal moments of inertia and the local inertial frame
737                // extracted from the tensor produce the original tensor. This tests that
738                // the diagonalization is correct.
739                // Note: The principal moments of inertia are *not* unique, and can differ
740                //       from the original values while still producing the same tensor
741                //       when taking the local inertial frame into account.
742                let (principal, frame) =
743                    angular_inertia_tensor.principal_angular_inertia_with_local_frame();
744                assert_relative_eq!(
745                    AngularInertiaTensor::new_with_local_frame(principal, frame),
746                    angular_inertia_tensor,
747                    epsilon = 1e-5
748                );
749            }
750        };
751    }
752
753    fn sample_shape<S: ShapeSample<Output = Vec3>>(shape: &S) -> Vec<S::Output> {
754        let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
755        (0..2_000_000)
756            .map(|_| shape.sample_interior(&mut rng))
757            .collect::<Vec<_>>()
758    }
759
760    fn rejection_sample_shape<S: Bounded3d>(
761        func: impl Fn(&S, Vec3) -> bool,
762        shape: &S,
763    ) -> Vec<Vec3> {
764        let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
765        let mut points = Vec::new();
766        let aabb = shape.aabb_3d(Isometry3d::IDENTITY);
767        let aabb_center: Vec3 = aabb.center().into();
768        let cuboid = Cuboid {
769            half_size: aabb.half_size().into(),
770        };
771        while points.len() < 2_000_000 {
772            let point = aabb_center + cuboid.sample_interior(&mut rng);
773            if func(shape, point) {
774                points.push(point);
775            }
776        }
777        points
778    }
779
780    test_shape!(sphere, Sphere::new(2.0), sample_shape);
781    test_shape!(cuboid, Cuboid::new(1.0, 2.0, 3.0), sample_shape);
782    test_shape!(cylinder, Cylinder::new(1.0, 4.0), sample_shape);
783    test_shape!(capsule, Capsule3d::new(1.0, 2.0), sample_shape);
784    test_shape!(
785        cone,
786        Cone {
787            radius: 1.0,
788            height: 2.0,
789        },
790        |shape| rejection_sample_shape(cone_contains_point, shape)
791    );
792    test_shape!(
793        conical_frustum,
794        ConicalFrustum {
795            radius_top: 0.5,
796            radius_bottom: 1.0,
797            height: 1.0
798        },
799        |shape| rejection_sample_shape(conical_frustum_contains_point, shape)
800    );
801    test_shape!(torus, Torus::new(1.0, 2.0), |shape| rejection_sample_shape(
802        torus_contains_point,
803        shape,
804    ));
805    test_shape!(
806        tetrahedron,
807        Tetrahedron::new(
808            Vec3::new(0.0, 0.0, 0.0),
809            Vec3::new(1.0, 0.0, 0.0),
810            Vec3::new(0.0, 1.0, 0.0),
811            Vec3::new(0.0, 0.0, 1.0),
812        ),
813        sample_shape
814    );
815
816    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for cones.
817    fn cone_contains_point(cone: &Cone, point: Vec3) -> bool {
818        let half_height = cone.height * 0.5;
819
820        if point.y < -half_height || point.y > half_height {
821            return false;
822        }
823
824        // a = tip
825        // b = base center
826        let pb_dot_ba = -cone.height * (point.y + half_height);
827
828        // Compute the radius of the circular slice.
829        // Derived geometrically from the triangular cross-section.
830        //
831        // let y = pb_dot_ba / cone.height;
832        // let slope = cone.height / cone.radius;
833        //
834        // let delta_radius = y / slope
835        //                  = y / (cone.height / cone.radius)
836        //                  = y * cone.radius / cone.height
837        //                  = pb_dot_ba / cone.height * cone.radius / cone.height
838        //                  = pb_dot_ba * cone.radius / (cone.height * cone.height);
839        // let radius = cone.radius + delta_radius;
840
841        let delta_radius = pb_dot_ba * cone.radius / cone.height.squared();
842        let radius = cone.radius + delta_radius;
843
844        // The squared orthogonal distance from the cone axis
845        let ortho_distance_squared = point.xz().length_squared();
846
847        ortho_distance_squared < radius * radius
848    }
849
850    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for conical frusta.
851    fn conical_frustum_contains_point(frustum: &ConicalFrustum, point: Vec3) -> bool {
852        let half_height = frustum.height * 0.5;
853
854        if point.y < -half_height || point.y > half_height {
855            return false;
856        }
857
858        // a = top center
859        // b = bottom center
860        let pb_dot_ba = -frustum.height * (point.y + half_height);
861
862        // Compute the radius of the circular slice.
863        // Derived geometrically from the trapezoidal cross-section.
864        //
865        // let y = pb_dot_ba / frustum.height;
866        // let slope = frustum.height / (frustum.radius_bottom - frustum.radius_top);
867        //
868        // let delta_radius = y / slope
869        //                  = y / (frustum.height / (frustum.radius_bottom - frustum.radius_top))
870        //                  = y * (frustum.radius_bottom - frustum.radius_top) / frustum.height
871        //                  = pb_dot_ba / frustum.height * (frustum.radius_bottom - frustum.radius_top) / frustum.height
872        //                  = pb_dot_ba * (frustum.radius_bottom - frustum.radius_top) / (frustum.height * frustum.height);
873        // let radius = frustum.radius_bottom + delta_radius;
874
875        let delta_radius =
876            pb_dot_ba * (frustum.radius_bottom - frustum.radius_top) / frustum.height.squared();
877        let radius = frustum.radius_bottom + delta_radius;
878
879        // The squared orthogonal distance from the frustum axis
880        let ortho_distance_squared = point.xz().length_squared();
881
882        ortho_distance_squared < radius * radius
883    }
884
885    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for tori.
886    fn torus_contains_point(torus: &Torus, point: Vec3) -> bool {
887        let minor_radius_squared = torus.minor_radius * torus.minor_radius;
888        (torus.major_radius - point.xz().length()).squared() + point.y.squared()
889            < minor_radius_squared
890    }
891}