1use core::f32::consts::PI;
2
3use he3d_math::{Quat, Vec3};
4
5use crate::aabb::Aabb;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct MassProperties {
17 pub inv_mass: f32,
20 pub inv_inertia: Vec3,
24}
25
26impl MassProperties {
27 pub const IMMOVABLE: Self = Self {
30 inv_mass: 0.0,
31 inv_inertia: Vec3::ZERO,
32 };
33
34 #[must_use]
37 pub fn is_immovable(&self) -> bool {
38 self.inv_mass == 0.0 && self.inv_inertia == Vec3::ZERO
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
49#[non_exhaustive]
50pub enum Shape {
51 Box {
54 half_extents: Vec3,
57 },
58 Sphere {
61 radius: f32,
64 },
65}
66
67impl Shape {
68 #[must_use]
75 pub fn cuboid(half_extents: Vec3) -> Self {
76 Self::Box {
77 half_extents: half_extents.max(Vec3::ZERO),
78 }
79 }
80
81 #[must_use]
84 pub fn sphere(radius: f32) -> Self {
85 Self::Sphere {
86 radius: radius.max(0.0),
87 }
88 }
89
90 #[must_use]
93 pub fn volume(&self) -> f32 {
94 match *self {
95 Self::Box { half_extents } => 8.0 * half_extents.x * half_extents.y * half_extents.z,
96 Self::Sphere { radius } => 4.0 / 3.0 * PI * radius * radius * radius,
97 }
98 }
99
100 #[must_use]
103 pub fn local_inertia(&self, mass: f32) -> Vec3 {
104 match *self {
105 Self::Box { half_extents } => {
106 let h = half_extents;
107 Vec3::new(
110 mass / 3.0 * (h.y * h.y + h.z * h.z),
111 mass / 3.0 * (h.x * h.x + h.z * h.z),
112 mass / 3.0 * (h.x * h.x + h.y * h.y),
113 )
114 }
115 Self::Sphere { radius } => {
116 Vec3::splat(0.4 * mass * radius * radius)
119 }
120 }
121 }
122
123 #[must_use]
133 pub fn mass_properties(&self, mass: f32) -> MassProperties {
134 if !mass.is_finite() || mass <= 0.0 {
135 return MassProperties::IMMOVABLE;
136 }
137 let inertia = self.local_inertia(mass);
138 MassProperties {
139 inv_mass: 1.0 / mass,
140 inv_inertia: Vec3::new(
141 inverse_or_zero(inertia.x),
142 inverse_or_zero(inertia.y),
143 inverse_or_zero(inertia.z),
144 ),
145 }
146 }
147
148 #[must_use]
152 pub fn aabb(&self, position: Vec3, orientation: Quat) -> Aabb {
153 match *self {
154 Self::Box { half_extents } => {
155 let x = orientation * Vec3::X;
160 let y = orientation * Vec3::Y;
161 let z = orientation * Vec3::Z;
162 let abs = |v: Vec3| Vec3::new(v.x.abs(), v.y.abs(), v.z.abs());
163 let (ax, ay, az) = (abs(x), abs(y), abs(z));
164 let extent = Vec3::new(
165 ax.x * half_extents.x + ay.x * half_extents.y + az.x * half_extents.z,
166 ax.y * half_extents.x + ay.y * half_extents.y + az.y * half_extents.z,
167 ax.z * half_extents.x + ay.z * half_extents.y + az.z * half_extents.z,
168 );
169 Aabb::from_center_half_extents(position, extent)
170 }
171 Self::Sphere { radius } => Aabb::from_center_half_extents(position, Vec3::splat(radius)),
172 }
173 }
174}
175
176fn inverse_or_zero(value: f32) -> f32 {
179 if value.is_finite() && value > 0.0 {
180 1.0 / value
181 } else {
182 0.0
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::{MassProperties, Shape};
189 use he3d_math::{Quat, Vec3};
190
191 #[test]
192 fn cuboid_inertia_matches_the_solid_box_formula() {
193 let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
194 let inertia = shape.local_inertia(12.0);
195 assert!((inertia.x - 52.0).abs() < 1e-4);
197 assert!((inertia.y - 40.0).abs() < 1e-4);
199 assert!((inertia.z - 20.0).abs() < 1e-4);
201 }
202
203 #[test]
204 fn sphere_inertia_is_isotropic() {
205 let shape = Shape::sphere(2.0);
206 let inertia = shape.local_inertia(5.0);
207 let expected = 0.4 * 5.0 * 4.0;
208 assert!((inertia.x - expected).abs() < 1e-4);
209 assert!((inertia.y - expected).abs() < 1e-4);
210 assert!((inertia.z - expected).abs() < 1e-4);
211 }
212
213 #[test]
214 fn non_positive_mass_is_immovable() {
215 assert_eq!(
216 Shape::sphere(1.0).mass_properties(0.0),
217 MassProperties::IMMOVABLE
218 );
219 assert_eq!(
220 Shape::sphere(1.0).mass_properties(f32::NAN),
221 MassProperties::IMMOVABLE
222 );
223 assert!(Shape::sphere(1.0).mass_properties(0.0).is_immovable());
224 }
225
226 #[test]
227 fn degenerate_shape_yields_zero_inverse_inertia() {
228 let flat = Shape::cuboid(Vec3::new(1.0, 0.0, 1.0));
229 let props = flat.mass_properties(2.0);
230 assert!(props.inv_inertia.y.is_finite());
232 assert!(props.inv_inertia.x.is_finite());
233 assert!(props.inv_mass.is_finite());
234 }
235
236 #[test]
237 fn axis_aligned_aabb_is_exact() {
238 let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
239 let aabb = shape.aabb(Vec3::new(10.0, 0.0, 0.0), Quat::IDENTITY);
240 assert!((aabb.min.x - 9.0).abs() < 1e-5);
241 assert!((aabb.max.x - 11.0).abs() < 1e-5);
242 assert!((aabb.max.y - 2.0).abs() < 1e-5);
243 assert!((aabb.max.z - 3.0).abs() < 1e-5);
244 }
245
246 #[test]
247 fn rotated_aabb_grows_to_cover_the_corners() {
248 let shape = Shape::cuboid(Vec3::splat(1.0));
249 let aabb = shape.aabb(
251 Vec3::ZERO,
252 Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
253 );
254 let expected = 2.0_f32.sqrt();
255 assert!((aabb.max.x - expected).abs() < 1e-4);
256 assert!((aabb.max.y - expected).abs() < 1e-4);
257 assert!((aabb.max.z - 1.0).abs() < 1e-5);
258 }
259
260 #[test]
261 fn negative_extents_are_clamped_not_inverted() {
262 assert_eq!(
263 Shape::cuboid(Vec3::splat(-1.0)),
264 Shape::Box {
265 half_extents: Vec3::ZERO
266 }
267 );
268 assert_eq!(Shape::sphere(-2.0), Shape::Sphere { radius: 0.0 });
269 }
270}