bevy_heavy/dim3/
impls.rs

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