Skip to main content

bevy_heavy/dim2/
impls.rs

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