Skip to main content

bevy_heavy/dim3/
impls.rs

1use super::{AngularInertiaTensor, ComputeMassProperties3d, MassProperties3d};
2use bevy_math::{
3    FloatPow, Quat, Vec3, ops,
4    prelude::Tetrahedron,
5    primitives::{
6        Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Measured3d, Plane3d, Polyline3d,
7        Segment3d, Sphere, Torus, Triangle3d,
8    },
9};
10use glam_matrix_extras::SymmetricMat3;
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        // TODO: Are -b1 and -c1 flipped here?
552        AngularInertiaTensor::from_symmetric_mat3(SymmetricMat3::new(a0, -b1, -c1, b0, -a1, c0))
553    }
554
555    #[inline]
556    fn center_of_mass(&self) -> Vec3 {
557        (self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0
558    }
559
560    #[inline]
561    fn mass_properties(&self, density: f32) -> MassProperties3d {
562        let volume = self.volume();
563        let center_of_mass = self.center_of_mass();
564
565        if volume < f32::EPSILON {
566            return MassProperties3d::new(0.0, Vec3::ZERO, center_of_mass);
567        }
568
569        let mass = volume * density;
570        let tensor = self.angular_inertia_tensor(mass);
571
572        MassProperties3d::new_with_angular_inertia_tensor(mass, tensor, center_of_mass)
573    }
574}
575
576macro_rules! impl_zero_mass_properties_3d {
577    ($($shape:ty),*) => {
578        $(
579            impl ComputeMassProperties3d for $shape {
580                #[inline]
581                fn mass(&self, _density: f32) -> f32 {
582                    0.0
583                }
584
585                #[inline]
586                fn unit_principal_angular_inertia(&self) -> Vec3 {
587                    Vec3::ZERO
588                }
589
590                #[inline]
591                fn principal_angular_inertia(&self, _mass: f32) -> Vec3 {
592                    Vec3::ZERO
593                }
594
595                #[inline]
596                fn local_inertial_frame(&self) -> Quat {
597                    Quat::IDENTITY
598                }
599
600                #[inline]
601                fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
602                    AngularInertiaTensor::ZERO
603                }
604
605                #[inline]
606                fn angular_inertia_tensor(&self, _mass: f32) -> AngularInertiaTensor {
607                    AngularInertiaTensor::ZERO
608                }
609
610                #[inline]
611                fn center_of_mass(&self) -> Vec3 {
612                    Vec3::ZERO
613                }
614
615                #[inline]
616                fn mass_properties(&self, _density: f32) -> MassProperties3d {
617                    MassProperties3d::ZERO
618                }
619            }
620        )*
621    };
622}
623
624impl_zero_mass_properties_3d!(Plane3d);
625impl_zero_mass_properties_3d!(Line3d);
626impl_zero_mass_properties_3d!(Segment3d);
627impl_zero_mass_properties_3d!(Triangle3d);
628
629impl ComputeMassProperties3d for Polyline3d {
630    #[inline]
631    fn mass(&self, _density: f32) -> f32 {
632        0.0
633    }
634
635    #[inline]
636    fn unit_principal_angular_inertia(&self) -> Vec3 {
637        Vec3::ZERO
638    }
639
640    #[inline]
641    fn principal_angular_inertia(&self, _mass: f32) -> Vec3 {
642        Vec3::ZERO
643    }
644
645    #[inline]
646    fn local_inertial_frame(&self) -> bevy_math::Quat {
647        Quat::IDENTITY
648    }
649
650    #[inline]
651    fn unit_angular_inertia_tensor(&self) -> AngularInertiaTensor {
652        AngularInertiaTensor::ZERO
653    }
654
655    #[inline]
656    fn angular_inertia_tensor(&self, _mass: f32) -> AngularInertiaTensor {
657        AngularInertiaTensor::ZERO
658    }
659
660    #[inline]
661    fn center_of_mass(&self) -> Vec3 {
662        Vec3::ZERO
663    }
664
665    #[inline]
666    fn mass_properties(&self, _density: f32) -> MassProperties3d {
667        MassProperties3d::ZERO
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use alloc::vec::Vec;
674
675    use approx::assert_relative_eq;
676    use bevy_math::{
677        Isometry3d, ShapeSample, Vec3Swizzles,
678        bounding::{Bounded3d, BoundingVolume},
679    };
680    use rand::SeedableRng;
681
682    use super::*;
683
684    macro_rules! test_shape {
685        ($test_name:tt, $shape:expr, $point_generator:expr) => {
686            #[test]
687            fn $test_name() {
688                let shape = $shape;
689
690                // Sample enough points to have a close enough point cloud representation of the shape.
691                let points: Vec<Vec3> = $point_generator(&shape);
692
693                // Compute the mass properties to test.
694                let density = 2.0;
695                let mass = shape.mass(density);
696                let principal_angular_inertia = shape.principal_angular_inertia(mass);
697                let local_inertial_frame = shape.local_inertial_frame();
698                let angular_inertia_tensor = shape.angular_inertia_tensor(mass);
699                let center_of_mass = shape.center_of_mass();
700
701                // First, test that the individually computed properties match the full properties.
702                let mass_props = shape.mass_properties(density);
703                assert_relative_eq!(mass, mass_props.mass);
704                assert_relative_eq!(
705                    principal_angular_inertia,
706                    mass_props.principal_angular_inertia
707                );
708                assert_relative_eq!(center_of_mass, mass_props.center_of_mass);
709
710                // Estimate the expected mass properties using the point cloud.
711                // Note: We could also approximate the mass using Monte Carlo integration.
712                //       This would require point containment checks.
713                let expected =
714                    MassProperties3d::from_point_cloud(&points, mass, local_inertial_frame);
715
716                assert_relative_eq!(mass, expected.mass);
717                assert_relative_eq!(
718                    principal_angular_inertia,
719                    expected.principal_angular_inertia,
720                    epsilon = 0.1
721                );
722                assert_relative_eq!(center_of_mass, expected.center_of_mass, epsilon = 0.01);
723
724                // Check that the computed tensor matches the tensor computed from
725                // the principal moments of inertia and the local inertial frame.
726                assert_relative_eq!(
727                    angular_inertia_tensor,
728                    AngularInertiaTensor::new_with_local_frame(
729                        principal_angular_inertia,
730                        local_inertial_frame
731                    ),
732                    epsilon = 1e-5
733                );
734
735                // Check that the principal moments of inertia and the local inertial frame
736                // extracted from the tensor produce the original tensor. This tests that
737                // the diagonalization is correct.
738                // Note: The principal moments of inertia are *not* unique, and can differ
739                //       from the original values while still producing the same tensor
740                //       when taking the local inertial frame into account.
741                let (principal, frame) =
742                    angular_inertia_tensor.principal_angular_inertia_with_local_frame();
743                assert_relative_eq!(
744                    AngularInertiaTensor::new_with_local_frame(principal, frame),
745                    angular_inertia_tensor,
746                    epsilon = 1e-5
747                );
748            }
749        };
750    }
751
752    fn sample_shape<S: ShapeSample<Output = Vec3>>(shape: &S) -> Vec<S::Output> {
753        let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
754        (0..2_000_000)
755            .map(|_| shape.sample_interior(&mut rng))
756            .collect::<Vec<_>>()
757    }
758
759    fn rejection_sample_shape<S: Bounded3d>(
760        func: impl Fn(&S, Vec3) -> bool,
761        shape: &S,
762    ) -> Vec<Vec3> {
763        let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
764        let mut points = Vec::new();
765        let aabb = shape.aabb_3d(Isometry3d::IDENTITY);
766        let aabb_center: Vec3 = aabb.center().into();
767        let cuboid = Cuboid {
768            half_size: aabb.half_size().into(),
769        };
770        while points.len() < 2_000_000 {
771            let point = aabb_center + cuboid.sample_interior(&mut rng);
772            if func(shape, point) {
773                points.push(point);
774            }
775        }
776        points
777    }
778
779    test_shape!(sphere, Sphere::new(2.0), sample_shape);
780    test_shape!(cuboid, Cuboid::new(1.0, 2.0, 3.0), sample_shape);
781    test_shape!(cylinder, Cylinder::new(1.0, 4.0), sample_shape);
782    test_shape!(capsule, Capsule3d::new(1.0, 2.0), sample_shape);
783    test_shape!(
784        cone,
785        Cone {
786            radius: 1.0,
787            height: 2.0,
788        },
789        |shape| rejection_sample_shape(cone_contains_point, shape)
790    );
791    test_shape!(
792        conical_frustum,
793        ConicalFrustum {
794            radius_top: 0.5,
795            radius_bottom: 1.0,
796            height: 1.0
797        },
798        |shape| rejection_sample_shape(conical_frustum_contains_point, shape)
799    );
800    test_shape!(torus, Torus::new(1.0, 2.0), |shape| rejection_sample_shape(
801        torus_contains_point,
802        shape,
803    ));
804    test_shape!(
805        tetrahedron,
806        Tetrahedron::new(
807            Vec3::new(0.0, 0.0, 0.0),
808            Vec3::new(1.0, 0.0, 0.0),
809            Vec3::new(0.0, 1.0, 0.0),
810            Vec3::new(0.0, 0.0, 1.0),
811        ),
812        sample_shape
813    );
814
815    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for cones.
816    fn cone_contains_point(cone: &Cone, point: Vec3) -> bool {
817        let half_height = cone.height * 0.5;
818
819        if point.y < -half_height || point.y > half_height {
820            return false;
821        }
822
823        // a = tip
824        // b = base center
825        let pb_dot_ba = -cone.height * (point.y + half_height);
826
827        // Compute the radius of the circular slice.
828        // Derived geometrically from the triangular cross-section.
829        //
830        // let y = pb_dot_ba / cone.height;
831        // let slope = cone.height / cone.radius;
832        //
833        // let delta_radius = y / slope
834        //                  = y / (cone.height / cone.radius)
835        //                  = y * cone.radius / cone.height
836        //                  = pb_dot_ba / cone.height * cone.radius / cone.height
837        //                  = pb_dot_ba * cone.radius / (cone.height * cone.height);
838        // let radius = cone.radius + delta_radius;
839
840        let delta_radius = pb_dot_ba * cone.radius / cone.height.squared();
841        let radius = cone.radius + delta_radius;
842
843        // The squared orthogonal distance from the cone axis
844        let ortho_distance_squared = point.xz().length_squared();
845
846        ortho_distance_squared < radius * radius
847    }
848
849    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for conical frusta.
850    fn conical_frustum_contains_point(frustum: &ConicalFrustum, point: Vec3) -> bool {
851        let half_height = frustum.height * 0.5;
852
853        if point.y < -half_height || point.y > half_height {
854            return false;
855        }
856
857        // a = top center
858        // b = bottom center
859        let pb_dot_ba = -frustum.height * (point.y + half_height);
860
861        // Compute the radius of the circular slice.
862        // Derived geometrically from the trapezoidal cross-section.
863        //
864        // let y = pb_dot_ba / frustum.height;
865        // let slope = frustum.height / (frustum.radius_bottom - frustum.radius_top);
866        //
867        // let delta_radius = y / slope
868        //                  = y / (frustum.height / (frustum.radius_bottom - frustum.radius_top))
869        //                  = y * (frustum.radius_bottom - frustum.radius_top) / frustum.height
870        //                  = pb_dot_ba / frustum.height * (frustum.radius_bottom - frustum.radius_top) / frustum.height
871        //                  = pb_dot_ba * (frustum.radius_bottom - frustum.radius_top) / (frustum.height * frustum.height);
872        // let radius = frustum.radius_bottom + delta_radius;
873
874        let delta_radius =
875            pb_dot_ba * (frustum.radius_bottom - frustum.radius_top) / frustum.height.squared();
876        let radius = frustum.radius_bottom + delta_radius;
877
878        // The squared orthogonal distance from the frustum axis
879        let ortho_distance_squared = point.xz().length_squared();
880
881        ortho_distance_squared < radius * radius
882    }
883
884    // TODO: This should be removed once Bevy either has this built-in or it has uniform sampling for tori.
885    fn torus_contains_point(torus: &Torus, point: Vec3) -> bool {
886        let minor_radius_squared = torus.minor_radius * torus.minor_radius;
887        (torus.major_radius - point.xz().length()).squared() + point.y.squared()
888            < minor_radius_squared
889    }
890}