Skip to main content

bevy_heavy/dim3/
impls.rs

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