bevy_heavy/dim2/
impls.rs

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