use he3d_math::{Quat, Vec3};
use crate::aabb::Aabb;
use crate::shape::{MassProperties, Shape};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum MotionType {
#[default]
Dynamic,
Static,
Kinematic,
}
impl MotionType {
#[must_use]
pub fn is_simulated(self) -> bool {
matches!(self, Self::Dynamic)
}
#[must_use]
pub fn moves(self) -> bool {
!matches!(self, Self::Static)
}
#[must_use]
pub fn is_pushable(self) -> bool {
matches!(self, Self::Dynamic)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct BodyId {
index: u32,
generation: u32,
}
impl BodyId {
pub(crate) const fn new(index: u32, generation: u32) -> Self {
Self { index, generation }
}
#[must_use]
pub const fn index(self) -> u32 {
self.index
}
#[must_use]
pub const fn generation(self) -> u32 {
self.generation
}
}
pub const DEFAULT_FRICTION: f32 = 0.5;
pub(crate) const NO_SLEEP_GROUP: u32 = u32::MAX;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct BodyDesc {
pub shape: Shape,
pub motion: MotionType,
pub mass: f32,
pub position: Vec3,
pub orientation: Quat,
pub linear_velocity: Vec3,
pub angular_velocity: Vec3,
pub friction: f32,
pub restitution: f32,
pub linear_damping: f32,
pub gravity_scale: f32,
pub angular_damping: f32,
}
impl BodyDesc {
#[must_use]
pub fn new(shape: Shape, motion: MotionType) -> Self {
Self {
shape,
motion,
mass: 1.0,
position: Vec3::ZERO,
orientation: Quat::IDENTITY,
linear_velocity: Vec3::ZERO,
angular_velocity: Vec3::ZERO,
friction: DEFAULT_FRICTION,
restitution: 0.0,
linear_damping: 0.0,
gravity_scale: 1.0,
angular_damping: 0.0,
}
}
#[must_use]
pub fn dynamic(shape: Shape, mass: f32) -> Self {
Self::new(shape, MotionType::Dynamic).with_mass(mass)
}
#[must_use]
pub fn stationary(shape: Shape) -> Self {
Self::new(shape, MotionType::Static)
}
#[must_use]
pub fn kinematic(shape: Shape) -> Self {
Self::new(shape, MotionType::Kinematic)
}
#[must_use]
pub fn with_mass(mut self, mass: f32) -> Self {
self.mass = mass;
self
}
#[must_use]
pub fn at(mut self, position: Vec3) -> Self {
self.position = position;
self
}
#[must_use]
pub fn oriented(mut self, orientation: Quat) -> Self {
self.orientation = orientation;
self
}
#[must_use]
pub fn with_velocity(mut self, velocity: Vec3) -> Self {
self.linear_velocity = velocity;
self
}
#[must_use]
pub fn with_angular_velocity(mut self, angular_velocity: Vec3) -> Self {
self.angular_velocity = angular_velocity;
self
}
#[must_use]
pub fn with_friction(mut self, friction: f32) -> Self {
self.friction = friction;
self
}
#[must_use]
pub fn with_restitution(mut self, restitution: f32) -> Self {
self.restitution = restitution;
self
}
#[must_use]
pub fn with_gravity_scale(mut self, scale: f32) -> Self {
self.gravity_scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
0.0
};
self
}
#[must_use]
pub fn with_damping(mut self, linear: f32, angular: f32) -> Self {
self.linear_damping = linear;
self.angular_damping = angular;
self
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Body {
pub(crate) shape: Shape,
pub(crate) motion: MotionType,
pub(crate) mass: MassProperties,
pub(crate) position: Vec3,
pub(crate) orientation: Quat,
pub(crate) linear_velocity: Vec3,
pub(crate) angular_velocity: Vec3,
pub(crate) force: Vec3,
pub(crate) torque: Vec3,
pub(crate) pseudo_linear_velocity: Vec3,
pub(crate) pseudo_angular_velocity: Vec3,
pub(crate) friction: f32,
pub(crate) restitution: f32,
pub(crate) linear_damping: f32,
pub(crate) gravity_scale: f32,
pub(crate) angular_damping: f32,
pub(crate) sleeping: bool,
pub(crate) sleep_timer: f32,
pub(crate) was_moving: bool,
pub(crate) sleep_group: u32,
}
impl Body {
pub(crate) fn from_desc(desc: &BodyDesc) -> Self {
let mass = if desc.motion.is_simulated() {
desc.shape.mass_properties(desc.mass)
} else {
MassProperties::IMMOVABLE
};
let orientation = if desc.orientation.length_squared() > 0.0 {
desc.orientation.normalize()
} else {
Quat::IDENTITY
};
Self {
shape: desc.shape,
motion: desc.motion,
mass,
position: desc.position,
orientation,
linear_velocity: desc.linear_velocity,
angular_velocity: desc.angular_velocity,
force: Vec3::ZERO,
torque: Vec3::ZERO,
pseudo_linear_velocity: Vec3::ZERO,
pseudo_angular_velocity: Vec3::ZERO,
friction: desc.friction.max(0.0),
restitution: desc.restitution.max(0.0),
linear_damping: desc.linear_damping.max(0.0),
gravity_scale: desc.gravity_scale,
angular_damping: desc.angular_damping.max(0.0),
sleeping: false,
sleep_timer: 0.0,
was_moving: false,
sleep_group: NO_SLEEP_GROUP,
}
}
pub(crate) fn aabb(&self) -> Aabb {
self.shape.aabb(self.position, self.orientation)
}
pub(crate) fn apply_inv_inertia(&self, value: Vec3) -> Vec3 {
let local = self.orientation.inverse() * value;
self.orientation * (local * self.mass.inv_inertia)
}
pub(crate) fn point_velocity(&self, anchor: Vec3) -> Vec3 {
self.linear_velocity + self.angular_velocity.cross(anchor)
}
pub(crate) fn pseudo_point_velocity(&self, anchor: Vec3) -> Vec3 {
self.pseudo_linear_velocity + self.pseudo_angular_velocity.cross(anchor)
}
pub(crate) fn clear_pseudo_velocities(&mut self) {
self.pseudo_linear_velocity = Vec3::ZERO;
self.pseudo_angular_velocity = Vec3::ZERO;
}
pub(crate) fn is_below_sleep_threshold(&self, linear: f32, angular: f32) -> bool {
self.linear_velocity.length_squared() <= linear * linear
&& self.angular_velocity.length_squared() <= angular * angular
}
pub(crate) fn put_to_sleep(&mut self) {
self.linear_velocity = Vec3::ZERO;
self.angular_velocity = Vec3::ZERO;
self.force = Vec3::ZERO;
self.torque = Vec3::ZERO;
self.sleeping = true;
self.sleep_timer = 0.0;
}
pub(crate) fn wake(&mut self) {
self.sleeping = false;
self.sleep_timer = 0.0;
}
pub(crate) fn refresh_movement(&mut self, linear: f32, angular: f32) {
self.was_moving = !self.is_below_sleep_threshold(linear, angular);
}
}
#[cfg(test)]
mod tests {
use super::{Body, BodyDesc, BodyId, MotionType};
use crate::shape::Shape;
use he3d_math::{Quat, Vec3};
#[test]
fn motion_mode_reports_its_own_semantics() {
assert!(MotionType::Dynamic.is_simulated());
assert!(MotionType::Dynamic.moves());
assert!(MotionType::Dynamic.is_pushable());
assert!(!MotionType::Static.is_simulated());
assert!(!MotionType::Static.moves());
assert!(!MotionType::Static.is_pushable());
assert!(!MotionType::Kinematic.is_simulated());
assert!(MotionType::Kinematic.moves());
assert!(!MotionType::Kinematic.is_pushable());
}
#[test]
fn generated_bodies_carry_derived_mass() {
let dynamic = Body::from_desc(&BodyDesc::dynamic(Shape::sphere(1.0), 2.0));
assert!(dynamic.mass.inv_mass > 0.0);
let stationary = Body::from_desc(&BodyDesc::stationary(Shape::sphere(1.0)));
assert!(stationary.mass.is_immovable());
let kinematic = Body::from_desc(&BodyDesc::kinematic(Shape::sphere(1.0)));
assert!(kinematic.mass.is_immovable());
}
#[test]
fn a_non_unit_orientation_is_normalized_on_construction() {
let desc =
BodyDesc::dynamic(Shape::sphere(1.0), 1.0).oriented(Quat::from_xyzw(0.0, 0.0, 0.0, 3.0));
let body = Body::from_desc(&desc);
assert!((body.orientation.length() - 1.0).abs() < 1e-5);
}
#[test]
fn inverse_inertia_acts_along_the_bodys_own_axes() {
let desc = BodyDesc::dynamic(Shape::cuboid(Vec3::new(1.0, 2.0, 4.0)), 3.0)
.oriented(Quat::from_rotation_y(0.7));
let body = Body::from_desc(&desc);
let world_x = body.orientation * Vec3::X;
let applied = body.apply_inv_inertia(world_x);
assert!((applied.length() - body.mass.inv_inertia.x).abs() < 1e-4);
assert!(
applied.cross(world_x).length() < 1e-4,
"the result left the axis: {applied:?}"
);
}
#[test]
fn velocity_at_combines_linear_and_angular_motion() {
let mut desc = BodyDesc::dynamic(Shape::sphere(0.5), 1.0);
desc.linear_velocity = Vec3::new(0.0, 0.0, 1.0);
desc.angular_velocity = Vec3::new(0.0, 1.0, 0.0);
let body = Body::from_desc(&desc);
let at = body.point_velocity(Vec3::new(0.0, 0.0, 1.0));
assert!((at.x - 1.0).abs() < 1e-5, "got {at:?}");
assert!(at.y.abs() < 1e-5, "got {at:?}");
assert!((at.z - 1.0).abs() < 1e-5, "got {at:?}");
}
#[test]
fn ids_compare_by_slot_and_generation() {
assert_eq!(BodyId::new(1, 0), BodyId::new(1, 0));
assert_ne!(BodyId::new(1, 0), BodyId::new(1, 1));
assert_ne!(BodyId::new(1, 0), BodyId::new(2, 0));
assert_eq!(BodyId::new(7, 3).index(), 7);
assert_eq!(BodyId::new(7, 3).generation(), 3);
}
}