use core::f32::consts::PI;
use he3d_math::{Quat, Vec3};
use crate::aabb::Aabb;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MassProperties {
pub inv_mass: f32,
pub inv_inertia: Vec3,
}
impl MassProperties {
pub const IMMOVABLE: Self = Self {
inv_mass: 0.0,
inv_inertia: Vec3::ZERO,
};
#[must_use]
pub fn is_immovable(&self) -> bool {
self.inv_mass == 0.0 && self.inv_inertia == Vec3::ZERO
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Shape {
Box {
half_extents: Vec3,
},
Sphere {
radius: f32,
},
}
impl Shape {
#[must_use]
pub fn cuboid(half_extents: Vec3) -> Self {
Self::Box {
half_extents: half_extents.max(Vec3::ZERO),
}
}
#[must_use]
pub fn sphere(radius: f32) -> Self {
Self::Sphere {
radius: radius.max(0.0),
}
}
#[must_use]
pub fn volume(&self) -> f32 {
match *self {
Self::Box { half_extents } => 8.0 * half_extents.x * half_extents.y * half_extents.z,
Self::Sphere { radius } => 4.0 / 3.0 * PI * radius * radius * radius,
}
}
#[must_use]
pub fn local_inertia(&self, mass: f32) -> Vec3 {
match *self {
Self::Box { half_extents } => {
let h = half_extents;
Vec3::new(
mass / 3.0 * (h.y * h.y + h.z * h.z),
mass / 3.0 * (h.x * h.x + h.z * h.z),
mass / 3.0 * (h.x * h.x + h.y * h.y),
)
}
Self::Sphere { radius } => {
Vec3::splat(0.4 * mass * radius * radius)
}
}
}
#[must_use]
pub fn mass_properties(&self, mass: f32) -> MassProperties {
if !mass.is_finite() || mass <= 0.0 {
return MassProperties::IMMOVABLE;
}
let inertia = self.local_inertia(mass);
MassProperties {
inv_mass: 1.0 / mass,
inv_inertia: Vec3::new(
inverse_or_zero(inertia.x),
inverse_or_zero(inertia.y),
inverse_or_zero(inertia.z),
),
}
}
#[must_use]
pub fn aabb(&self, position: Vec3, orientation: Quat) -> Aabb {
match *self {
Self::Box { half_extents } => {
let x = orientation * Vec3::X;
let y = orientation * Vec3::Y;
let z = orientation * Vec3::Z;
let abs = |v: Vec3| Vec3::new(v.x.abs(), v.y.abs(), v.z.abs());
let (ax, ay, az) = (abs(x), abs(y), abs(z));
let extent = Vec3::new(
ax.x * half_extents.x + ay.x * half_extents.y + az.x * half_extents.z,
ax.y * half_extents.x + ay.y * half_extents.y + az.y * half_extents.z,
ax.z * half_extents.x + ay.z * half_extents.y + az.z * half_extents.z,
);
Aabb::from_center_half_extents(position, extent)
}
Self::Sphere { radius } => Aabb::from_center_half_extents(position, Vec3::splat(radius)),
}
}
}
fn inverse_or_zero(value: f32) -> f32 {
if value.is_finite() && value > 0.0 {
1.0 / value
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::{MassProperties, Shape};
use he3d_math::{Quat, Vec3};
#[test]
fn cuboid_inertia_matches_the_solid_box_formula() {
let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
let inertia = shape.local_inertia(12.0);
assert!((inertia.x - 52.0).abs() < 1e-4);
assert!((inertia.y - 40.0).abs() < 1e-4);
assert!((inertia.z - 20.0).abs() < 1e-4);
}
#[test]
fn sphere_inertia_is_isotropic() {
let shape = Shape::sphere(2.0);
let inertia = shape.local_inertia(5.0);
let expected = 0.4 * 5.0 * 4.0;
assert!((inertia.x - expected).abs() < 1e-4);
assert!((inertia.y - expected).abs() < 1e-4);
assert!((inertia.z - expected).abs() < 1e-4);
}
#[test]
fn non_positive_mass_is_immovable() {
assert_eq!(
Shape::sphere(1.0).mass_properties(0.0),
MassProperties::IMMOVABLE
);
assert_eq!(
Shape::sphere(1.0).mass_properties(f32::NAN),
MassProperties::IMMOVABLE
);
assert!(Shape::sphere(1.0).mass_properties(0.0).is_immovable());
}
#[test]
fn degenerate_shape_yields_zero_inverse_inertia() {
let flat = Shape::cuboid(Vec3::new(1.0, 0.0, 1.0));
let props = flat.mass_properties(2.0);
assert!(props.inv_inertia.y.is_finite());
assert!(props.inv_inertia.x.is_finite());
assert!(props.inv_mass.is_finite());
}
#[test]
fn axis_aligned_aabb_is_exact() {
let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
let aabb = shape.aabb(Vec3::new(10.0, 0.0, 0.0), Quat::IDENTITY);
assert!((aabb.min.x - 9.0).abs() < 1e-5);
assert!((aabb.max.x - 11.0).abs() < 1e-5);
assert!((aabb.max.y - 2.0).abs() < 1e-5);
assert!((aabb.max.z - 3.0).abs() < 1e-5);
}
#[test]
fn rotated_aabb_grows_to_cover_the_corners() {
let shape = Shape::cuboid(Vec3::splat(1.0));
let aabb = shape.aabb(
Vec3::ZERO,
Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
);
let expected = 2.0_f32.sqrt();
assert!((aabb.max.x - expected).abs() < 1e-4);
assert!((aabb.max.y - expected).abs() < 1e-4);
assert!((aabb.max.z - 1.0).abs() < 1e-5);
}
#[test]
fn negative_extents_are_clamped_not_inverted() {
assert_eq!(
Shape::cuboid(Vec3::splat(-1.0)),
Shape::Box {
half_extents: Vec3::ZERO
}
);
assert_eq!(Shape::sphere(-2.0), Shape::Sphere { radius: 0.0 });
}
}