Skip to main content

bevy_heavy/dim2/
impls.rs

1use super::{ComputeMassProperties2d, MassProperties2d};
2use bevy_math::{
3    FloatPow, Vec2, ops,
4    primitives::{
5        Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse,
6        Line2d, Measured2d, Plane2d, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,
7        Triangle2d,
8    },
9};
10
11impl ComputeMassProperties2d for Circle {
12    #[inline]
13    fn mass(&self, density: f32) -> f32 {
14        self.area() * density
15    }
16
17    #[inline]
18    fn unit_angular_inertia(&self) -> f32 {
19        self.radius.squared() / 2.0
20    }
21
22    #[inline]
23    fn center_of_mass(&self) -> Vec2 {
24        Vec2::ZERO
25    }
26}
27
28impl ComputeMassProperties2d for CircularSector {
29    #[inline]
30    fn mass(&self, density: f32) -> f32 {
31        self.area() * density
32    }
33
34    #[inline]
35    fn unit_angular_inertia(&self) -> f32 {
36        0.5 * ops::powf(self.radius(), 4.0) * self.angle()
37    }
38
39    #[inline]
40    fn center_of_mass(&self) -> Vec2 {
41        let angle = self.angle();
42        let y = 2.0 * self.radius() * ops::sin(angle) / (3.0 * angle);
43        Vec2::new(0.0, y)
44    }
45}
46
47impl ComputeMassProperties2d for CircularSegment {
48    #[inline]
49    fn mass(&self, density: f32) -> f32 {
50        self.area() * density
51    }
52
53    #[inline]
54    fn unit_angular_inertia(&self) -> f32 {
55        let angle = self.angle();
56        let (sin, cos) = ops::sin_cos(angle);
57        ops::powf(self.radius(), 4.0) / 4.0 * (angle - sin + 2.0 / 3.0 * sin * (1.0 - cos) / 2.0)
58    }
59
60    #[inline]
61    fn center_of_mass(&self) -> Vec2 {
62        let y = self.radius() * ops::sin(self.half_angle()).cubed()
63            / (6.0 * self.half_angle() - ops::sin(self.angle()));
64        Vec2::new(0.0, y)
65    }
66}
67
68impl ComputeMassProperties2d for Ellipse {
69    #[inline]
70    fn mass(&self, density: f32) -> f32 {
71        self.area() * density
72    }
73
74    #[inline]
75    fn unit_angular_inertia(&self) -> f32 {
76        self.half_size.length_squared() / 4.0
77    }
78
79    #[inline]
80    fn center_of_mass(&self) -> Vec2 {
81        Vec2::ZERO
82    }
83}
84
85impl ComputeMassProperties2d for Annulus {
86    #[inline]
87    fn mass(&self, density: f32) -> f32 {
88        self.area() * density
89    }
90
91    #[inline]
92    fn unit_angular_inertia(&self) -> f32 {
93        0.5 * (self.outer_circle.radius.squared() + self.inner_circle.radius.squared())
94    }
95
96    #[inline]
97    fn center_of_mass(&self) -> Vec2 {
98        Vec2::ZERO
99    }
100}
101
102impl ComputeMassProperties2d for Triangle2d {
103    #[inline]
104    fn mass(&self, density: f32) -> f32 {
105        self.area() * density
106    }
107
108    #[inline]
109    fn unit_angular_inertia(&self) -> f32 {
110        // Adapted from Box2D: https://github.com/erincatto/box2d/blob/411acc32eb6d4f2e96fc70ddbdf01fe5f9b16230/src/collision/b2_polygon_shape.cpp#L274
111
112        // Note: The center of mass is used here, unlike in Box2D's or Parry's version.
113        let center_of_mass = self.center_of_mass();
114        let com_a = self.vertices[1] - center_of_mass;
115        let com_c = self.vertices[2] - center_of_mass;
116
117        (com_a.length_squared() + com_a.dot(com_c) + com_c.length_squared()) / 6.0
118    }
119
120    #[inline]
121    fn center_of_mass(&self) -> Vec2 {
122        (self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0
123    }
124
125    #[inline]
126    fn mass_properties(&self, density: f32) -> MassProperties2d {
127        let area = self.area();
128        let center_of_mass = self.center_of_mass();
129
130        if area < f32::EPSILON {
131            return MassProperties2d::new(0.0, 0.0, center_of_mass);
132        }
133
134        let mass = area * density;
135
136        MassProperties2d::new(mass, self.angular_inertia(mass), center_of_mass)
137    }
138}
139
140impl ComputeMassProperties2d for Rectangle {
141    #[inline]
142    fn mass(&self, density: f32) -> f32 {
143        self.area() * density
144    }
145
146    #[inline]
147    fn unit_angular_inertia(&self) -> f32 {
148        self.half_size.length_squared() / 3.0
149    }
150
151    #[inline]
152    fn center_of_mass(&self) -> Vec2 {
153        Vec2::ZERO
154    }
155}
156
157impl ComputeMassProperties2d for Rhombus {
158    #[inline]
159    fn mass(&self, density: f32) -> f32 {
160        self.area() * density
161    }
162
163    #[inline]
164    fn unit_angular_inertia(&self) -> f32 {
165        self.half_diagonals.length_squared() / 12.0
166    }
167
168    #[inline]
169    fn center_of_mass(&self) -> Vec2 {
170        Vec2::ZERO
171    }
172}
173
174impl ComputeMassProperties2d for RegularPolygon {
175    #[inline]
176    fn mass(&self, density: f32) -> f32 {
177        self.area() * density
178    }
179
180    #[inline]
181    fn unit_angular_inertia(&self) -> f32 {
182        let half_external_angle = core::f32::consts::PI / self.sides as f32;
183        self.circumradius().squared() / 6.0 * (1.0 + 2.0 * ops::cos(half_external_angle).squared())
184    }
185
186    #[inline]
187    fn center_of_mass(&self) -> Vec2 {
188        Vec2::ZERO
189    }
190}
191
192impl ComputeMassProperties2d for Capsule2d {
193    #[inline]
194    fn mass(&self, density: f32) -> f32 {
195        let area = self.radius * (core::f32::consts::PI * self.radius + 4.0 * self.half_length);
196        area * density
197    }
198
199    #[inline]
200    fn unit_angular_inertia(&self) -> f32 {
201        // The rectangle and hemicircle parts
202        let rectangle = Rectangle {
203            half_size: Vec2::new(self.radius, self.half_length),
204        };
205        let rectangle_height = rectangle.half_size.y * 2.0;
206        let circle = Circle::new(self.radius);
207
208        // Areas
209        let rectangle_area = rectangle.area();
210        let circle_area = circle.area();
211
212        // Masses
213        let density = 1.0 / (rectangle_area + circle_area);
214        let rectangle_mass = rectangle_area * density;
215        let circle_mass = circle_area * density;
216
217        // Principal inertias
218        let rectangle_inertia = rectangle.angular_inertia(rectangle_mass);
219        let circle_inertia = circle.angular_inertia(circle_mass);
220
221        // Total inertia
222        let mut capsule_inertia = rectangle_inertia + circle_inertia;
223
224        // Compensate for the hemicircles being away from the rotation axis using the parallel axis theorem.
225        capsule_inertia += (rectangle_height.squared() * 0.25
226            + rectangle_height * self.radius * 3.0 / 8.0)
227            * circle_mass;
228
229        capsule_inertia
230    }
231
232    #[inline]
233    fn center_of_mass(&self) -> Vec2 {
234        Vec2::ZERO
235    }
236
237    #[inline]
238    fn mass_properties(&self, density: f32) -> MassProperties2d {
239        // The rectangle and hemicircle parts
240        let rectangle = Rectangle {
241            half_size: Vec2::new(self.radius, self.half_length),
242        };
243        let rectangle_height = rectangle.half_size.y * 2.0;
244        let circle = Circle::new(self.radius);
245
246        // Areas
247        let rectangle_area = rectangle.area();
248        let circle_area = circle.area();
249
250        // Masses
251        let rectangle_mass = rectangle_area * density;
252        let circle_mass = circle_area * density;
253
254        // Principal inertias
255        let rectangle_inertia = rectangle.angular_inertia(rectangle_mass);
256        let circle_inertia = circle.angular_inertia(circle_mass);
257
258        // Total inertia
259        let mut capsule_inertia = rectangle_inertia + circle_inertia;
260
261        // Compensate for the hemicircles being away from the rotation axis using the parallel axis theorem.
262        capsule_inertia += (rectangle_height.squared() * 0.25
263            + rectangle_height * self.radius * 3.0 / 8.0)
264            * circle_mass;
265
266        MassProperties2d::new(rectangle_mass + circle_mass, capsule_inertia, Vec2::ZERO)
267    }
268}
269
270impl ComputeMassProperties2d for ConvexPolygon {
271    #[inline]
272    fn mass(&self, density: f32) -> f32 {
273        convex_polygon_mass(self.vertices(), density)
274    }
275
276    #[inline]
277    fn unit_angular_inertia(&self) -> f32 {
278        convex_polygon_unit_angular_inertia(self.vertices())
279    }
280
281    #[inline]
282    fn center_of_mass(&self) -> Vec2 {
283        convex_polygon_area_and_center_of_mass(self.vertices()).1
284    }
285
286    #[inline]
287    fn mass_properties(&self, density: f32) -> MassProperties2d {
288        convex_polygon_mass_properties(self.vertices(), density)
289    }
290}
291
292/// Computes the mass of a convex polygon defined by its vertices and density.
293///
294/// No checks are performed to ensure the polygon is convex.
295#[inline]
296pub fn convex_polygon_mass(vertices: &[Vec2], density: f32) -> f32 {
297    let geometric_center =
298        vertices.iter().fold(Vec2::ZERO, |acc, vtx| acc + *vtx) / vertices.len() as f32;
299
300    // Initialize polygon area.
301    let mut area = 0.0;
302
303    // Create a peekable iterator over the polygon vertices.
304    let mut iter = vertices.iter().peekable();
305    let Some(first) = iter.peek().copied().copied() else {
306        return 0.0;
307    };
308
309    // Iterate through vertices, computing the sum of the areas of triangles.
310    // Each triangle is formed by the current vertex, next vertex, and the geometric center of the polygon.
311    while let Some(vertex) = iter.next() {
312        let (a, b, c) = (
313            *vertex,
314            iter.peek().copied().copied().unwrap_or(first),
315            geometric_center,
316        );
317        let tri_area = Triangle2d::new(a, b, c).area();
318        area += tri_area;
319    }
320
321    area * density
322}
323
324/// Computes the unit angular inertia of a convex polygon defined by its vertices.
325///
326/// No checks are performed to ensure the polygon is convex.
327#[inline]
328pub fn convex_polygon_unit_angular_inertia(vertices: &[Vec2]) -> f32 {
329    // The polygon is assumed to be convex.
330    let (area, center_of_mass) = convex_polygon_area_and_center_of_mass(vertices);
331
332    if area < f32::EPSILON {
333        return 0.0;
334    }
335
336    // Initialize polygon inertia.
337    let mut inertia = 0.0;
338
339    // Create a peekable iterator over the polygon vertices.
340    let mut iter = vertices.iter().peekable();
341    let first = **iter.peek().unwrap();
342
343    // Iterate through vertices, computing the sum of the areas of triangles.
344    // Each triangle is formed by the current vertex, next vertex, and the geometric center of the polygon.
345    while let Some(vertex) = iter.next() {
346        let triangle = Triangle2d::new(
347            *vertex,
348            iter.peek().copied().copied().unwrap_or(first),
349            center_of_mass,
350        );
351        inertia += triangle.unit_angular_inertia() * triangle.area();
352    }
353
354    inertia / area
355}
356
357/// Computes the mass properties of a convex polygon defined by its vertices and density.
358///
359/// No checks are performed to ensure the polygon is convex.
360#[inline]
361pub fn convex_polygon_mass_properties(vertices: &[Vec2], density: f32) -> MassProperties2d {
362    // The polygon is assumed to be convex.
363    let (area, center_of_mass) = convex_polygon_area_and_center_of_mass(vertices);
364
365    if area < f32::EPSILON {
366        return MassProperties2d::new(0.0, 0.0, center_of_mass);
367    }
368
369    // Initialize polygon inertia.
370    let mut inertia = 0.0;
371
372    // Create a peekable iterator over the polygon vertices.
373    let mut iter = vertices.iter().peekable();
374    let first = **iter.peek().unwrap();
375
376    // Iterate through vertices, computing the sum of the areas of triangles.
377    // Each triangle is formed by the current vertex, next vertex, and the geometric center of the polygon.
378    while let Some(vertex) = iter.next() {
379        let triangle = Triangle2d::new(
380            *vertex,
381            iter.peek().copied().copied().unwrap_or(first),
382            center_of_mass,
383        );
384        inertia += triangle.unit_angular_inertia() * triangle.area();
385    }
386
387    MassProperties2d::new(area * density, inertia * density, center_of_mass)
388}
389
390/// Computes the area and center of mass of a convex polygon defined by its vertices.
391///
392/// No checks are performed to ensure the polygon is convex.
393#[inline]
394pub fn convex_polygon_area_and_center_of_mass(vertices: &[Vec2]) -> (f32, Vec2) {
395    let geometric_center =
396        vertices.iter().fold(Vec2::ZERO, |acc, vtx| acc + *vtx) / vertices.len() as f32;
397
398    // Initialize polygon area and center.
399    let mut area = 0.0;
400    let mut center = Vec2::ZERO;
401
402    // Create a peekable iterator over the polygon vertices.
403    let mut iter = vertices.iter().peekable();
404    let Some(first) = iter.peek().copied().copied() else {
405        return (0.0, Vec2::ZERO);
406    };
407
408    // Iterate through vertices, computing the sum of the areas and centers of triangles.
409    // Each triangle is formed by the current vertex, next vertex, and the geometric center of the polygon.
410    while let Some(vertex) = iter.next() {
411        let (a, b, c) = (
412            *vertex,
413            iter.peek().copied().copied().unwrap_or(first),
414            geometric_center,
415        );
416        let tri_area = Triangle2d::new(a, b, c).area();
417        let tri_center = (a + b + c) / 3.0;
418
419        area += tri_area;
420        center += tri_center * tri_area;
421    }
422
423    if area < f32::EPSILON {
424        (area, geometric_center)
425    } else {
426        (area, center / area)
427    }
428}
429
430macro_rules! impl_zero_mass_properties_2d {
431    ($($shape:ty),*) => {
432        $(
433            impl ComputeMassProperties2d for $shape {
434                #[inline]
435                fn mass(&self, _density: f32) -> f32 {
436                    0.0
437                }
438
439                #[inline]
440                fn unit_angular_inertia(&self) -> f32 {
441                    0.0
442                }
443
444                #[inline]
445                fn angular_inertia(&self, _mass: f32) -> f32 {
446                    0.0
447                }
448
449                #[inline]
450                fn center_of_mass(&self) -> Vec2 {
451                    Vec2::ZERO
452                }
453
454                #[inline]
455                fn mass_properties(&self, _density: f32) -> MassProperties2d {
456                    MassProperties2d::ZERO
457                }
458            }
459        )*
460    };
461}
462
463impl_zero_mass_properties_2d!(Arc2d);
464impl_zero_mass_properties_2d!(Plane2d);
465impl_zero_mass_properties_2d!(Line2d);
466impl_zero_mass_properties_2d!(Segment2d);
467impl_zero_mass_properties_2d!(Polyline2d);
468
469#[cfg(test)]
470mod tests {
471    use alloc::vec::Vec;
472
473    use approx::assert_relative_eq;
474    use bevy_math::ShapeSample;
475    use rand::SeedableRng;
476
477    use super::*;
478
479    macro_rules! test_shape {
480        ($test_name:tt, $shape:expr) => {
481            #[test]
482            fn $test_name() {
483                let shape = $shape;
484
485                // Sample enough points to have a close enough point cloud representation of the shape.
486                let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
487                let points = (0..1_000_000)
488                    .map(|_| shape.sample_interior(&mut rng))
489                    .collect::<Vec<_>>();
490
491                // Compute the mass properties to test.
492                let density = 2.0;
493                let mass = shape.mass(density);
494                let angular_inertia = shape.angular_inertia(mass);
495                let center_of_mass = shape.center_of_mass();
496
497                // First, test that the individually computed properties match the full properties.
498                let mass_props = shape.mass_properties(density);
499                assert_relative_eq!(mass, mass_props.mass);
500                assert_relative_eq!(angular_inertia, mass_props.angular_inertia);
501                assert_relative_eq!(center_of_mass, mass_props.center_of_mass);
502
503                // Estimate the expected mass properties using the point cloud.
504                // Note: We could also approximate the mass using Monte Carlo integration.
505                //       This would require point containment checks.
506                let expected = MassProperties2d::from_point_cloud(&points, mass);
507
508                assert_relative_eq!(mass, expected.mass);
509                assert_relative_eq!(angular_inertia, expected.angular_inertia, epsilon = 0.1);
510                assert_relative_eq!(center_of_mass, expected.center_of_mass, epsilon = 0.01);
511            }
512        };
513    }
514
515    // TODO: Test randomized shape definitions.
516
517    test_shape!(circle, Circle::new(2.0));
518    // test_shape!(circular_sector, CircularSector::new(2.0, TAU));
519    // test_shape!(circular_segment, CircularSegment::new(2.0, TAU));
520    // test_shape!(ellipse, Ellipse::new(2.0, 1.0));
521    test_shape!(annulus, Annulus::new(1.0, 2.0));
522    test_shape!(
523        triangle,
524        Triangle2d::new(
525            Vec2::new(8.0, 6.0),
526            Vec2::new(2.0, 0.0),
527            Vec2::new(6.0, 2.0)
528        )
529    );
530    test_shape!(rectangle, Rectangle::new(2.0, 1.0));
531    // test_shape!(rhombus, Rhombus::new(2.0, 1.0));
532    // test_shape!(regular_polygon, RegularPolygon::new(2.0, 6));
533    // test_shape!(polygon, Polygon::new([Vec2::ZERO, Vec2::X, Vec2::Y]));
534    test_shape!(capsule, Capsule2d::new(1.0, 0.25));
535}