bevy_heavy/dim3/
impls.rs

1use super::{AngularInertiaTensor, ComputeMassProperties3d, MassProperties3d};
2use bevy_math::{
3    ops,
4    prelude::Tetrahedron,
5    primitives::{
6        BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Measured3d,
7        Plane3d, Polyline3d, Segment3d, Sphere, Torus,
8    },
9    FloatPow, Mat3, Quat, Vec3,
10};
11
12impl ComputeMassProperties3d for Sphere {
13    #[inline]
14    fn mass(&self, density: f32) -> f32 {
15        self.volume() * density
16    }
17
18    #[inline]
19    fn unit_principal_angular_inertia(&self) -> Vec3 {
20        Vec3::splat(0.4 * self.radius.squared())
21    }
22
23    #[inline]
24    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
25        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
26    }
27
28    #[inline]
29    fn center_of_mass(&self) -> Vec3 {
30        Vec3::ZERO
31    }
32}
33
34impl ComputeMassProperties3d for Cuboid {
35    #[inline]
36    fn mass(&self, density: f32) -> f32 {
37        self.volume() * density
38    }
39
40    #[inline]
41    fn unit_principal_angular_inertia(&self) -> Vec3 {
42        let ix = self.half_size.x.squared() / 3.0;
43        let iy = self.half_size.y.squared() / 3.0;
44        let iz = self.half_size.z.squared() / 3.0;
45        Vec3::new(iy + iz, ix + iz, ix + iy)
46    }
47
48    #[inline]
49    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
50        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
51    }
52
53    #[inline]
54    fn center_of_mass(&self) -> Vec3 {
55        Vec3::ZERO
56    }
57}
58
59impl ComputeMassProperties3d for Cylinder {
60    #[inline]
61    fn mass(&self, density: f32) -> f32 {
62        self.volume() * density
63    }
64
65    #[inline]
66    fn unit_principal_angular_inertia(&self) -> Vec3 {
67        let radius_squared = self.radius.squared();
68        let height_squared = self.half_height.squared() * 4.0;
69        let principal = radius_squared / 2.0;
70        let off_principal = (radius_squared * 3.0 + height_squared) / 12.0;
71        Vec3::new(off_principal, principal, off_principal)
72    }
73
74    #[inline]
75    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
76        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
77    }
78
79    #[inline]
80    fn center_of_mass(&self) -> Vec3 {
81        Vec3::ZERO
82    }
83}
84
85impl ComputeMassProperties3d for Capsule3d {
86    #[inline]
87    fn mass(&self, density: f32) -> f32 {
88        self.volume() * density
89    }
90
91    #[inline]
92    fn unit_principal_angular_inertia(&self) -> Vec3 {
93        // The cylinder and hemisphere parts
94        let cylinder = Cylinder {
95            radius: self.radius,
96            half_height: self.half_length,
97        };
98        let cylinder_length = self.half_length * 2.0;
99        let sphere = Sphere::new(self.radius);
100
101        // Volumes
102        let cylinder_volume = cylinder.volume();
103        let sphere_volume = sphere.volume();
104
105        // Masses
106        let density = 1.0 / (cylinder_volume + sphere_volume);
107        let cylinder_mass = cylinder_volume * density;
108        let sphere_mass = sphere_volume * density;
109
110        // Principal inertias
111        let cylinder_inertia = cylinder.principal_angular_inertia(cylinder_mass);
112        let sphere_inertia = sphere.principal_angular_inertia(sphere_mass);
113
114        // Total inertia
115        let mut capsule_inertia = cylinder_inertia + sphere_inertia;
116
117        // Compensate for the hemispheres being away from the rotation axis using the parallel axis theorem.
118        let extra = (cylinder_length.squared() * 0.25 + cylinder_length * self.radius * 3.0 / 8.0)
119            * sphere_mass;
120        capsule_inertia.x += extra;
121        capsule_inertia.z += extra;
122
123        capsule_inertia
124    }
125
126    #[inline]
127    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
128        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
129    }
130
131    #[inline]
132    fn center_of_mass(&self) -> Vec3 {
133        Vec3::ZERO
134    }
135
136    #[inline]
137    fn mass_properties(&self, density: f32) -> MassProperties3d {
138        // The cylinder and hemisphere parts
139        let cylinder = Cylinder {
140            radius: self.radius,
141            half_height: self.half_length,
142        };
143        let cylinder_length = self.half_length * 2.0;
144        let sphere = Sphere::new(self.radius);
145
146        // Volumes
147        let cylinder_volume = cylinder.volume();
148        let sphere_volume = sphere.volume();
149
150        // Masses
151        let cylinder_mass = cylinder_volume * density;
152        let sphere_mass = sphere_volume * density;
153
154        // Principal inertias
155        let cylinder_inertia = cylinder.principal_angular_inertia(cylinder_mass);
156        let sphere_inertia = sphere.principal_angular_inertia(sphere_mass);
157
158        // Total inertia
159        let mut capsule_inertia = cylinder_inertia + sphere_inertia;
160
161        // Compensate for the hemispheres being away from the rotation axis using the parallel axis theorem.
162        let extra = (cylinder_length.squared() * 0.25 + cylinder_length * self.radius * 3.0 / 8.0)
163            * sphere_mass;
164        capsule_inertia.x += extra;
165        capsule_inertia.z += extra;
166
167        MassProperties3d::new(cylinder_mass + sphere_mass, capsule_inertia, Vec3::ZERO)
168    }
169}
170
171impl ComputeMassProperties3d for Cone {
172    #[inline]
173    fn mass(&self, density: f32) -> f32 {
174        self.volume() * density
175    }
176
177    #[inline]
178    fn unit_principal_angular_inertia(&self) -> Vec3 {
179        let radius_squared = self.radius.squared();
180        let height_squared = self.height.squared();
181
182        // About the Y axis
183        let principal = 3.0 / 10.0 * radius_squared;
184
185        // About the X or Z axis passing through the center of mass
186        let off_principal = principal * 0.5 + 3.0 / 80.0 * height_squared;
187
188        Vec3::new(off_principal, principal, off_principal)
189    }
190
191    #[inline]
192    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
193        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
194    }
195
196    #[inline]
197    fn center_of_mass(&self) -> Vec3 {
198        Vec3::new(0.0, -self.height * 0.25, 0.0)
199    }
200}
201
202impl ComputeMassProperties3d for ConicalFrustum {
203    #[inline]
204    fn mass(&self, density: f32) -> f32 {
205        if self.radius_top == self.radius_bottom {
206            Cylinder::new(self.radius_top, self.height).mass(density)
207        } else {
208            // https://mathworld.wolfram.com/ConicalFrustum.html
209            let radii_squared = self.radius_top.squared() + self.radius_bottom.squared();
210            let volume = core::f32::consts::FRAC_PI_3
211                * self.height
212                * (radii_squared + self.radius_top * self.radius_bottom);
213            volume * density
214        }
215    }
216
217    #[inline]
218    fn unit_principal_angular_inertia(&self) -> Vec3 {
219        if self.radius_top == self.radius_bottom {
220            Cylinder::new(self.radius_top, self.height).unit_principal_angular_inertia()
221        } else {
222            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
223                (self.radius_top, self.radius_bottom)
224            } else {
225                (self.radius_bottom, self.radius_top)
226            };
227
228            // TODO: The principal angular inertia along the symmetry axis Y is straightforward to compute directly.
229            //       However, I have not found a correct formula for the principal angular inertia along the other axes.
230            //       It would be much more efficient to compute directly instead of subtracting cones like what we're doing here though.
231            // let principal_y = 3.0 * (max_radius.powi(5) - min_radius.powi(5))
232            //     / (10.0 * (max_radius.powi(3) - min_radius.powi(3)));
233
234            // The conical frustum can be thought of as a cone with a smaller cone subtracted from it.
235            // To get its angular inertia, we can subtract the angular inertia of the small cone
236            // from the angular inertia of the small cone.
237
238            // Create the large and small cone.
239            let cone_height =
240                max_radius * (self.height / ops::abs(self.radius_top - self.radius_bottom));
241            let large_cone = Cone {
242                radius: max_radius,
243                height: cone_height,
244            };
245            let small_cone = Cone {
246                radius: min_radius,
247                height: cone_height - self.height,
248            };
249
250            // Compute the volumes of the large and small cone and the frustum.
251            // These are equivalent to the masses when using a uniform density of `1.0`.
252            let large_cone_volume = large_cone.volume();
253            let small_cone_volume = small_cone.volume();
254            let volume = large_cone_volume - small_cone_volume;
255
256            // The total mass of the frustum is `1.0` in this case, so we just want the fractional volumes
257            // for determining how much each cone contributes to the angular inertia.
258            let large_cone_volume_fraction = large_cone_volume / volume;
259            let small_cone_volume_fraction = small_cone_volume / volume;
260
261            let large_cone_angular_inertia =
262                large_cone.principal_angular_inertia(large_cone_volume_fraction);
263            let mut small_cone_angular_inertia =
264                small_cone.principal_angular_inertia(small_cone_volume_fraction);
265
266            // The small cone's center of mass is offset from the frustum's center of mass,
267            // so we need to take the parallel axis theorem (also known as Steiner's theorem) into account.
268            //
269            // I = I_cm + m * d^2
270            //
271            // - `I_cm` is the angular inertia about the center of mass
272            // - `m` is the mass, `small_cone_fraction` in this case
273            // - `d` is the distance between the parallel axes
274            let d = 0.5 * (self.height + small_cone.height) + small_cone.center_of_mass().y;
275            let extra_angular_inertia = small_cone_volume_fraction * d * d;
276            small_cone_angular_inertia.x += extra_angular_inertia;
277            small_cone_angular_inertia.z += extra_angular_inertia;
278
279            // Return the total principal angular inertia.
280            large_cone_angular_inertia.x - small_cone_angular_inertia
281        }
282    }
283
284    #[inline]
285    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
286        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
287    }
288
289    #[inline]
290    fn center_of_mass(&self) -> Vec3 {
291        if self.radius_top == self.radius_bottom {
292            Vec3::ZERO
293        } else {
294            // Adapted from: https://mathworld.wolfram.com/ConicalFrustum.html
295
296            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
297                (self.radius_top, self.radius_bottom)
298            } else {
299                (self.radius_bottom, self.radius_top)
300            };
301
302            let min_radius_squared = min_radius.squared();
303            let max_radius_squared = max_radius.squared();
304            let radii_product = self.radius_top * self.radius_bottom;
305
306            let y = self.height
307                * ((max_radius_squared + 2.0 * radii_product + 3.0 * min_radius_squared)
308                    / (4.0 * (max_radius_squared + radii_product + min_radius_squared))
309                    - 0.5);
310
311            Vec3::new(0.0, y, 0.0)
312        }
313    }
314
315    #[inline]
316    fn mass_properties(&self, density: f32) -> MassProperties3d {
317        if self.radius_top == self.radius_bottom {
318            Cylinder::new(self.radius_top, self.height).mass_properties(density)
319        } else {
320            // This is a combination of all the methods above, sharing variables where possible.
321
322            let (min_radius, max_radius) = if self.radius_top < self.radius_bottom {
323                (self.radius_top, self.radius_bottom)
324            } else {
325                (self.radius_bottom, self.radius_top)
326            };
327
328            // The conical frustum can be thought of as a cone with a smaller cone subtracted from it.
329            // To get its angular inertia, we can subtract the angular inertia of the small cone
330            // from the angular inertia of the small cone.
331
332            // Create the large and small cone.
333            let cone_height =
334                max_radius * (self.height / ops::abs(self.radius_top - self.radius_bottom));
335            let large_cone = Cone {
336                radius: max_radius,
337                height: cone_height,
338            };
339            let small_cone = Cone {
340                radius: min_radius,
341                height: cone_height - self.height,
342            };
343
344            // Compute the volumes of the large and small cone and the frustum.
345            // These are equivalent to the masses when using a uniform density of `1.0`.
346            let large_cone_volume = large_cone.volume();
347            let small_cone_volume = small_cone.volume();
348            let volume = large_cone_volume - small_cone_volume;
349
350            // Compute the mass.
351            let mass = volume * density;
352
353            // Compute how much each cone contributes to the angular inertia.
354            let large_cone_volume_fraction = large_cone_volume / volume;
355            let small_cone_volume_fraction = small_cone_volume / volume;
356
357            let large_cone_angular_inertia =
358                large_cone.principal_angular_inertia(large_cone_volume_fraction);
359            let mut small_cone_angular_inertia =
360                small_cone.principal_angular_inertia(small_cone_volume_fraction);
361
362            // The small cone's center of mass is offset from the frustum's center of mass,
363            // so we need to take the parallel axis theorem (also known as Steiner's theorem) into account.
364            //
365            // I = I_cm + m * d^2
366            //
367            // - `I_cm` is the angular inertia about the center of mass
368            // - `m` is the mass, `small_cone_fraction` in this case
369            // - `d` is the distance between the parallel axes
370            let d = 0.5 * (self.height + small_cone.height) + small_cone.center_of_mass().y;
371            let extra_angular_inertia = small_cone_volume_fraction * d * d;
372            small_cone_angular_inertia.x += extra_angular_inertia;
373            small_cone_angular_inertia.z += extra_angular_inertia;
374
375            // Return the total principal angular inertia.
376            let principal_angular_inertia =
377                mass * (large_cone_angular_inertia.x - small_cone_angular_inertia);
378
379            let min_radius_squared = min_radius.squared();
380            let max_radius_squared = max_radius.squared();
381            let radii_product = self.radius_top * self.radius_bottom;
382
383            // Compute the center of mass.
384            let y = self.height
385                * ((max_radius_squared + 2.0 * radii_product + 3.0 * min_radius_squared)
386                    / (4.0 * (max_radius_squared + radii_product + min_radius_squared))
387                    - 0.5);
388            let center_of_mass = Vec3::new(0.0, y, 0.0);
389
390            MassProperties3d::new(mass, principal_angular_inertia, center_of_mass)
391        }
392    }
393}
394
395impl ComputeMassProperties3d for Torus {
396    #[inline]
397    fn mass(&self, density: f32) -> f32 {
398        self.volume() * density
399    }
400
401    #[inline]
402    fn unit_principal_angular_inertia(&self) -> Vec3 {
403        // Reference: https://en.wikipedia.org/wiki/List_of_moments_of_inertia
404
405        let major_radius_squared = self.major_radius.squared();
406        let minor_radius_squared = self.minor_radius.squared();
407
408        let principal = 0.25 * (4.0 * major_radius_squared + 3.0 * minor_radius_squared);
409        let off_principal = 1.0 / 8.0 * (4.0 * major_radius_squared + 5.0 * minor_radius_squared);
410        Vec3::new(off_principal, principal, off_principal)
411    }
412
413    #[inline]
414    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
415        AngularInertiaTensor::new(self.unit_principal_angular_inertia())
416    }
417
418    #[inline]
419    fn center_of_mass(&self) -> Vec3 {
420        Vec3::ZERO
421    }
422}
423
424impl ComputeMassProperties3d for Tetrahedron {
425    #[inline]
426    fn mass(&self, density: f32) -> f32 {
427        self.volume() * density
428    }
429
430    #[inline]
431    fn unit_principal_angular_inertia(&self) -> Vec3 {
432        let tensor = self.unit_angular_inertia_tensor();
433        tensor.principal_angular_inertia_with_local_frame().0
434    }
435
436    #[inline]
437    fn local_inertial_frame(&self) -> Quat {
438        let tensor = self.unit_angular_inertia_tensor();
439        tensor.principal_angular_inertia_with_local_frame().1
440    }
441
442    #[inline]
443    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
444        // References:
445        // - F. Tonon. "Explicit Exact Formulas for the 3-D Tetrahedron Inertia Tensor in Terms of its Vertex Coordinates"
446        // - Parry: https://github.com/dimforge/parry/blob/837291f1a051dc04e6e4ab8bc2e5b438ce66d257/src/mass_properties/mass_properties_trimesh3d.rs#L38
447
448        let [p1, p2, p3, p4] = self.vertices;
449
450        // For readability
451        let x1 = p1.x;
452        let y1 = p1.y;
453        let z1 = p1.z;
454        let x2 = p2.x;
455        let y2 = p2.y;
456        let z2 = p2.z;
457        let x3 = p3.x;
458        let y3 = p3.y;
459        let z3 = p3.z;
460        let x4 = p4.x;
461        let y4 = p4.y;
462        let z4 = p4.z;
463
464        let diag_x = x1 * x1
465            + x1 * x2
466            + x2 * x2
467            + x1 * x3
468            + x2 * x3
469            + x3 * x3
470            + x1 * x4
471            + x2 * x4
472            + x3 * x4
473            + x4 * x4;
474        let diag_y = y1 * y1
475            + y1 * y2
476            + y2 * y2
477            + y1 * y3
478            + y2 * y3
479            + y3 * y3
480            + y1 * y4
481            + y2 * y4
482            + y3 * y4
483            + y4 * y4;
484        let diag_z = z1 * z1
485            + z1 * z2
486            + z2 * z2
487            + z1 * z3
488            + z2 * z3
489            + z3 * z3
490            + z1 * z4
491            + z2 * z4
492            + z3 * z4
493            + z4 * z4;
494
495        let a0 = (diag_y + diag_z) * 0.1;
496        let b0 = (diag_z + diag_x) * 0.1;
497        let c0 = (diag_x + diag_y) * 0.1;
498
499        let a1 = (y1 * z1 * 2.0
500            + y2 * z1
501            + y3 * z1
502            + y4 * z1
503            + y1 * z2
504            + y2 * z2 * 2.0
505            + y3 * z2
506            + y4 * z2
507            + y1 * z3
508            + y2 * z3
509            + y3 * z3 * 2.0
510            + y4 * z3
511            + y1 * z4
512            + y2 * z4
513            + y3 * z4
514            + y4 * z4 * 2.0)
515            * 0.05;
516        let b1 = (x1 * z1 * 2.0
517            + x2 * z1
518            + x3 * z1
519            + x4 * z1
520            + x1 * z2
521            + x2 * z2 * 2.0
522            + x3 * z2
523            + x4 * z2
524            + x1 * z3
525            + x2 * z3
526            + x3 * z3 * 2.0
527            + x4 * z3
528            + x1 * z4
529            + x2 * z4
530            + x3 * z4
531            + x4 * z4 * 2.0)
532            * 0.05;
533        let c1 = (x1 * y1 * 2.0
534            + x2 * y1
535            + x3 * y1
536            + x4 * y1
537            + x1 * y2
538            + x2 * y2 * 2.0
539            + x3 * y2
540            + x4 * y2
541            + x1 * y3
542            + x2 * y3
543            + x3 * y3 * 2.0
544            + x4 * y3
545            + x1 * y4
546            + x2 * y4
547            + x3 * y4
548            + x4 * y4 * 2.0)
549            * 0.05;
550
551        AngularInertiaTensor::from_mat3(Mat3::from_cols_array(&[
552            a0, -b1, -c1, -b1, b0, -a1, -c1, -a1, c0,
553        ]))
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!(BoxedPolyline3d);
629
630impl<const N: usize> ComputeMassProperties3d for Polyline3d<N> {
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}