mod mapper;
mod settings;
mod settle;
use std::fmt;
use std::ptr::NonNull;
use oxijolt_sys::*;
use crate::body::{corners, kinematic_velocities, with_locked_body};
use crate::constraint::SixDofConstraintAxis;
use crate::limits::{self, ANGULAR_VELOCITY_RULE, LINEAR_VELOCITY_RULE};
use crate::owned::{JoltObject, Owned};
use crate::world::{advance_structure_epoch, WorldTag, DELTA_TIME_RULE};
use crate::{Activation, BodyId, MotionType, PhysicsWorld, Quat, RVec3, RagdollError, Vec3};
pub use mapper::{MappedSkeleton, SkeletonMapper, TranslationLocks};
use settings::JointKind;
pub use settings::{
JointTransform, RagdollJoint, RagdollPart, RagdollSettings, Skeleton, SkeletonJoint,
SkeletonPose,
};
pub use settle::SettleDetector;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RagdollId {
raw: u32,
pub(crate) world: WorldTag,
}
impl RagdollId {
pub(crate) fn new(raw: u32, world: WorldTag) -> Self {
Self { raw, world }
}
pub fn to_raw(self) -> u32 {
self.raw
}
}
impl fmt::Debug for RagdollId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RagdollId").field(&self.raw).finish()
}
}
impl JoltObject for JPH_Ragdoll {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_Ragdoll_Destroy(ptr) };
}
}
pub(crate) struct RagdollEntry {
ragdoll: Owned<JPH_Ragdoll>,
bodies: Vec<BodyId>,
parents: Vec<Option<u32>>,
joints: Vec<Option<JointKind>>,
constraint_of_part: Vec<Option<u32>>,
}
impl RagdollEntry {
fn constraint(&self, part: usize) -> Option<(JointKind, *mut JPH_TwoBodyConstraint)> {
let kind = self.joints[part]?;
let index = self.constraint_of_part[part]?;
let constraint = unsafe { JPH_Ragdoll_GetConstraint(self.ragdoll.as_ptr(), index as i32) };
debug_assert_eq!(
unsafe { JPH_Constraint_GetSubType(constraint.cast()) },
kind.sub_type(),
"the recorded joint kind matches the Jolt constraint"
);
Some((kind, constraint))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum JointReading {
SwingTwist {
rotation_in_constraint_space: Quat,
},
Hinge {
current_angle: f32,
},
SixDof {
rotation_in_constraint_space: Quat,
},
}
pub struct RagdollRef<'w> {
world: &'w PhysicsWorld,
id: RagdollId,
entry: &'w RagdollEntry,
}
impl fmt::Debug for RagdollRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RagdollRef")
.field("id", &self.id)
.finish_non_exhaustive()
}
}
impl RagdollRef<'_> {
pub fn id(&self) -> RagdollId {
self.id
}
pub fn part_count(&self) -> u32 {
self.entry.bodies.len() as u32
}
pub fn body_ids(&self) -> &[BodyId] {
&self.entry.bodies
}
pub fn pose(&self) -> SkeletonPose {
let transforms: Vec<(RVec3, Quat)> = self
.entry
.bodies
.iter()
.map(|&id| {
let body = self.world.body(id).unwrap_or_else(|_| {
unreachable!("the world removes parts only with the ragdoll")
});
(body.position(), body.rotation())
})
.collect();
let root_offset = transforms[0].0;
#[allow(clippy::unnecessary_cast)]
let joints = transforms
.iter()
.map(|&(position, rotation)| JointTransform {
translation: Vec3::new(
(position.x - root_offset.x) as f32,
(position.y - root_offset.y) as f32,
(position.z - root_offset.z) as f32,
),
rotation,
})
.collect();
SkeletonPose {
root_offset,
joints,
}
}
pub fn root_transform(&self) -> (RVec3, Quat) {
let mut position = RVec3::ZERO.to_jph();
let mut rotation = Quat::IDENTITY.to_jph();
unsafe {
JPH_Ragdoll_GetRootTransform(
self.entry.ragdoll.as_ptr(),
&mut position,
&mut rotation,
true,
)
};
(RVec3::from_jph(position), Quat::from_jph(rotation))
}
pub fn is_active(&self) -> bool {
unsafe { JPH_Ragdoll_IsActive(self.entry.ragdoll.as_ptr(), true) }
}
pub fn joint(&self, part: u32) -> Option<JointReading> {
let part = part as usize;
if part >= self.entry.bodies.len() {
return None;
}
let (kind, constraint) = self.entry.constraint(part)?;
let mut rotation = Quat::IDENTITY.to_jph();
let reading = unsafe {
match kind {
JointKind::SwingTwist => {
JPH_SwingTwistConstraint_GetRotationInConstraintSpace(
constraint.cast_const().cast(),
&mut rotation,
);
JointReading::SwingTwist {
rotation_in_constraint_space: Quat::from_jph(rotation),
}
}
JointKind::Hinge => JointReading::Hinge {
current_angle: JPH_HingeConstraint_GetCurrentAngle(constraint.cast()),
},
JointKind::SixDof => {
JPH_SixDOFConstraint_GetRotationInConstraintSpace(
constraint.cast(),
&mut rotation,
);
JointReading::SixDof {
rotation_in_constraint_space: Quat::from_jph(rotation),
}
}
}
};
Some(reading)
}
pub fn joint_motors_on(&self, part: u32) -> Option<bool> {
let part = part as usize;
if part >= self.entry.bodies.len() {
return None;
}
let (kind, constraint) = self.entry.constraint(part)?;
let on = |state: JPH_MotorState| state != JPH_MotorState_Off;
let motors_on = unsafe {
match kind {
JointKind::SwingTwist => {
let constraint = constraint.cast_const().cast();
on(JPH_SwingTwistConstraint_GetSwingMotorState(constraint))
|| on(JPH_SwingTwistConstraint_GetTwistMotorState(constraint))
}
JointKind::Hinge => on(JPH_HingeConstraint_GetMotorState(constraint.cast())),
JointKind::SixDof => SixDofConstraintAxis::ROTATIONS.iter().any(|axis| {
on(JPH_SixDOFConstraint_GetMotorState(
constraint.cast(),
axis.to_jph(),
))
}),
}
};
Some(motors_on)
}
pub fn is_calm(&self, max_linear_speed: f32, max_angular_speed: f32) -> bool {
self.entry.bodies.iter().all(|&id| {
let body = self
.world
.body(id)
.unwrap_or_else(|_| unreachable!("the world removes parts only with the ragdoll"));
!body.is_active()
|| (body.linear_velocity().length() < max_linear_speed
&& body.angular_velocity().length() < max_angular_speed)
})
}
}
pub struct RagdollMut<'w> {
entry: &'w mut RagdollEntry,
body_interface: NonNull<JPH_BodyInterface>,
structure_epoch: &'w mut u64,
}
impl fmt::Debug for RagdollMut<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RagdollMut").finish_non_exhaustive()
}
}
const PART_MOTION_RULE: &str = "ragdoll parts are dynamic or kinematic";
const RAGDOLL_GROUP_BASE: u32 = 1 << 31;
impl RagdollMut<'_> {
fn interface(&self) -> *mut JPH_BodyInterface {
self.body_interface.as_ptr()
}
fn validate(&self, pose: &SkeletonPose) -> Result<(), RagdollError> {
pose.validate(self.entry.bodies.len())
}
pub fn set_pose(&mut self, pose: &SkeletonPose) -> Result<(), RagdollError> {
self.validate(pose)?;
place_parts(self.body_interface, &self.entry.bodies, pose);
unsafe { JPH_Ragdoll_ResetWarmStart(self.entry.ragdoll.as_ptr()) };
Ok(())
}
pub fn drive_to_pose_using_kinematics(
&mut self,
pose: &SkeletonPose,
delta_time: f32,
) -> Result<(), RagdollError> {
self.validate(pose)?;
if !PhysicsWorld::is_valid_delta_time(delta_time) {
return Err(RagdollError::InvalidValue(DELTA_TIME_RULE));
}
for (index, &id) in self.entry.bodies.iter().enumerate() {
let rotation = pose.joints[index].rotation;
let velocities = unsafe {
kinematic_velocities(
self.body_interface,
id,
pose.position(index),
rotation,
delta_time,
)
};
let within_limits = velocities.is_some_and(|(linear, angular)| {
limits::is_linear_velocity(linear) && limits::is_angular_velocity(angular)
});
if !within_limits {
return Err(RagdollError::InvalidValue(KINEMATIC_DRIVE_RULE));
}
}
for (index, &id) in self.entry.bodies.iter().enumerate() {
let mut position = pose.position(index).to_jph();
let mut rotation = pose.joints[index].rotation.to_jph();
unsafe {
JPH_BodyInterface_MoveKinematic(
self.interface(),
id.to_raw(),
&mut position,
&mut rotation,
delta_time,
)
};
}
Ok(())
}
pub fn drive_to_pose_using_motors(&mut self, pose: &SkeletonPose) -> Result<(), RagdollError> {
self.validate(pose)?;
for part in 0..self.entry.bodies.len() {
let Some(parent) = self.entry.parents[part] else {
continue;
};
let Some((kind, constraint)) = self.entry.constraint(part) else {
continue;
};
let parent_rotation = pose.joints[parent as usize].rotation;
let target = parent_rotation
.conjugated()
.product(pose.joints[part].rotation)
.normalized()
.to_jph();
unsafe {
match kind {
JointKind::SwingTwist => {
let constraint = constraint.cast();
JPH_SwingTwistConstraint_SetSwingMotorState(
constraint,
JPH_MotorState_Position,
);
JPH_SwingTwistConstraint_SetTwistMotorState(
constraint,
JPH_MotorState_Position,
);
JPH_SwingTwistConstraint_SetTargetOrientationBS(constraint, &target);
}
JointKind::Hinge => {
let constraint = constraint.cast();
JPH_HingeConstraint_SetMotorState(constraint, JPH_MotorState_Position);
JPH_HingeConstraint_SetTargetOrientationBS(constraint, &target);
}
JointKind::SixDof => {
let constraint = constraint.cast();
for axis in SixDofConstraintAxis::ROTATIONS {
JPH_SixDOFConstraint_SetMotorState(
constraint,
axis.to_jph(),
JPH_MotorState_Position,
);
}
let mut target = target;
JPH_SixDOFConstraint_SetTargetOrientationBS(constraint, &mut target);
}
}
}
}
self.activate();
Ok(())
}
pub fn stop_motors(&mut self) {
for part in 0..self.entry.bodies.len() {
let Some((kind, constraint)) = self.entry.constraint(part) else {
continue;
};
unsafe {
match kind {
JointKind::SwingTwist => {
let constraint = constraint.cast();
JPH_SwingTwistConstraint_SetSwingMotorState(constraint, JPH_MotorState_Off);
JPH_SwingTwistConstraint_SetTwistMotorState(constraint, JPH_MotorState_Off);
}
JointKind::Hinge => {
JPH_HingeConstraint_SetMotorState(constraint.cast(), JPH_MotorState_Off)
}
JointKind::SixDof => {
for axis in SixDofConstraintAxis::ALL {
JPH_SixDOFConstraint_SetMotorState(
constraint.cast(),
axis.to_jph(),
JPH_MotorState_Off,
);
}
}
}
}
}
}
pub fn set_motion_type(
&mut self,
motion_type: MotionType,
activation: Activation,
) -> Result<(), RagdollError> {
if motion_type == MotionType::Static {
return Err(RagdollError::InvalidValue(PART_MOTION_RULE));
}
advance_structure_epoch(self.structure_epoch);
for &id in &self.entry.bodies {
unsafe {
JPH_BodyInterface_SetMotionType(
self.interface(),
id.to_raw(),
motion_type.to_jph(),
activation.to_jph(),
)
};
}
Ok(())
}
pub fn set_linear_and_angular_velocity(
&mut self,
linear: Vec3,
angular: Vec3,
) -> Result<(), RagdollError> {
if !limits::is_linear_velocity(linear) {
return Err(RagdollError::InvalidValue(LINEAR_VELOCITY_RULE));
}
if !limits::is_angular_velocity(angular) {
return Err(RagdollError::InvalidValue(ANGULAR_VELOCITY_RULE));
}
for &id in &self.entry.bodies {
let mut linear = linear.to_jph();
let mut angular = angular.to_jph();
unsafe {
JPH_BodyInterface_SetLinearAndAngularVelocity(
self.interface(),
id.to_raw(),
&mut linear,
&mut angular,
)
};
}
Ok(())
}
pub fn activate(&mut self) {
unsafe { JPH_Ragdoll_Activate(self.entry.ragdoll.as_ptr(), true) };
}
}
const KINEMATIC_DRIVE_RULE: &str =
"a kinematic drive must keep every part within the limits velocity bounds";
fn place_parts(body_interface: NonNull<JPH_BodyInterface>, bodies: &[BodyId], pose: &SkeletonPose) {
for (index, &id) in bodies.iter().enumerate() {
let position = pose.position(index).to_jph();
let rotation = pose.joints[index].rotation.to_jph();
unsafe {
JPH_BodyInterface_SetPositionAndRotation(
body_interface.as_ptr(),
id.to_raw(),
&position,
&rotation,
JPH_Activation_DontActivate,
)
};
}
}
impl PhysicsWorld {
pub fn create_ragdoll(
&mut self,
settings: &RagdollSettings,
pose: Option<&SkeletonPose>,
activation: Activation,
) -> Result<RagdollId, RagdollError> {
if let Some(&layer) = settings
.object_layers()
.iter()
.find(|layer| layer.get() >= self.object_layer_count)
{
return Err(RagdollError::UnknownObjectLayer(layer));
}
let parts = settings.object_layers().len();
if let Some(pose) = pose {
pose.validate(parts)?;
}
let raw = self.next_ragdoll_id;
if raw > RAGDOLL_GROUP_BASE - 2 {
return Err(RagdollError::TooManyRagdolls);
}
if !self.has_room_for_bodies(parts) {
return Err(RagdollError::TooManyBodies);
}
self.note_structure_change();
let ragdoll = unsafe {
Owned::from_raw(JPH_RagdollSettings_CreateRagdoll(
settings.as_ptr(),
self.system.as_ptr(),
RAGDOLL_GROUP_BASE + raw,
0,
))
}
.ok_or(RagdollError::TooManyBodies)?;
let bodies: Vec<BodyId> = (0..parts)
.map(|part| {
let raw = unsafe { JPH_Ragdoll_GetBodyID(ragdoll.as_ptr(), part as i32) };
BodyId::new(raw, self.tag)
})
.collect();
if let Some(pose) = pose {
place_parts(self.body_interface, &bodies, pose);
}
unsafe { JPH_Ragdoll_AddToPhysicsSystem(ragdoll.as_ptr(), activation.to_jph(), true) };
let mut next_constraint = 0;
let constraint_of_part = settings
.joints()
.iter()
.map(|joint| {
joint.map(|_| {
next_constraint += 1;
next_constraint - 1
})
})
.collect();
for body in &bodies {
self.ragdoll_bodies.insert(body.to_raw(), raw);
}
self.ragdolls.insert(
raw,
RagdollEntry {
ragdoll,
bodies,
parents: settings.parents().to_vec(),
joints: settings.joints().to_vec(),
constraint_of_part,
},
);
self.next_ragdoll_id += 1;
Ok(RagdollId::new(raw, self.tag))
}
fn ragdoll_entry(&self, id: RagdollId) -> Result<&RagdollEntry, RagdollError> {
if id.world != self.tag {
return Err(RagdollError::WrongWorld(id));
}
self.ragdolls.get(&id.raw).ok_or(RagdollError::NotFound(id))
}
pub fn ragdoll(&self, id: RagdollId) -> Result<RagdollRef<'_>, RagdollError> {
let entry = self.ragdoll_entry(id)?;
Ok(RagdollRef {
world: self,
id,
entry,
})
}
pub fn ragdoll_mut(&mut self, id: RagdollId) -> Result<RagdollMut<'_>, RagdollError> {
self.ragdoll_entry(id)?;
let body_interface = self.body_interface;
let entry = self
.ragdolls
.get_mut(&id.raw)
.unwrap_or_else(|| unreachable!("checked above"));
Ok(RagdollMut {
entry,
body_interface,
structure_epoch: &mut self.structure_epoch,
})
}
pub fn remove_ragdoll(&mut self, id: RagdollId) -> Result<(), RagdollError> {
self.ragdoll_entry(id)?;
self.note_structure_change();
let entry = self
.ragdolls
.remove(&id.raw)
.unwrap_or_else(|| unreachable!("checked above"));
let bounds: Vec<JPH_AABox> = entry
.bodies
.iter()
.map(|&body| {
let mut bounds = JPH_AABox {
min: Vec3::ZERO.to_jph(),
max: Vec3::ZERO.to_jph(),
};
with_locked_body(self.body_lock_interface, body, |locked| {
unsafe { JPH_Body_GetWorldSpaceBounds(locked.as_ptr(), &mut bounds) };
})
.unwrap_or_else(|| unreachable!("the world removes parts only with the ragdoll"));
bounds
})
.collect();
for body in &entry.bodies {
self.ragdoll_bodies.remove(&body.to_raw());
self.pending_cache_invalidations.remove(&body.to_raw());
}
self.unregister_ragdoll(entry);
for bounds in &bounds {
let (min, max) = corners(bounds);
self.wake_bodies_overlapping(min, max, None);
}
Ok(())
}
fn unregister_ragdoll(&mut self, entry: RagdollEntry) {
unsafe { JPH_Ragdoll_RemoveFromPhysicsSystem(entry.ragdoll.as_ptr(), true) };
drop(entry);
}
pub(crate) fn remove_all_ragdolls(&mut self) {
while let Some((_, entry)) = self.ragdolls.pop_first() {
for body in &entry.bodies {
self.ragdoll_bodies.remove(&body.to_raw());
}
self.unregister_ragdoll(entry);
}
}
pub fn ragdoll_ids(&self) -> impl Iterator<Item = RagdollId> + '_ {
self.ragdolls
.keys()
.map(|&raw| RagdollId::new(raw, self.tag))
}
pub fn ragdoll_count(&self) -> u32 {
self.ragdolls.len() as u32
}
pub fn contains_ragdoll(&self, id: RagdollId) -> bool {
self.ragdoll_entry(id).is_ok()
}
pub fn ragdoll_of_body(&self, body: BodyId) -> Option<RagdollId> {
if body.world != self.tag {
return None;
}
self.ragdoll_bodies
.get(&body.to_raw())
.map(|&raw| RagdollId::new(raw, self.tag))
}
pub(crate) fn is_ragdoll_body(&self, body: BodyId) -> bool {
self.ragdoll_of_body(body).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BodySettings, Real, Shape, SwingTwistConstraintSettings, WorldSettings};
fn distance_squared(a: RVec3, b: RVec3) -> Real {
let d = [a.x - b.x, a.y - b.y, a.z - b.z];
d.iter().map(|value| value * value).sum()
}
#[test]
fn ragdoll_group_ids_stop_below_the_invalid_group() {
let mut world = PhysicsWorld::new(WorldSettings::default().gravity(Vec3::ZERO)).unwrap();
let skeleton = Skeleton::new(&[SkeletonJoint {
name: "root",
parent: None,
}])
.unwrap();
let shape = Shape::new_sphere(0.2).unwrap();
let parts = [RagdollPart {
shape: &shape,
body: BodySettings::new_dynamic(),
joint: None,
}];
let settings = RagdollSettings::new(&skeleton, &parts).unwrap();
world.next_ragdoll_id = RAGDOLL_GROUP_BASE - 2;
let id = world
.create_ragdoll(&settings, None, Activation::Activate)
.unwrap();
let part = world.ragdoll(id).unwrap().body_ids()[0];
let mut group = JPH_CollisionGroup {
groupFilter: std::ptr::null(),
groupID: 0,
subGroupID: 0,
};
unsafe {
JPH_BodyInterface_GetCollisionGroup(
world.body_interface.as_ptr(),
part.to_raw(),
&mut group,
)
};
assert_eq!(group.groupID, u32::MAX - 1);
let (epoch, bodies) = (world.structure_epoch, world.body_count());
assert_eq!(
world.create_ragdoll(&settings, None, Activation::Activate),
Err(RagdollError::TooManyRagdolls)
);
assert_eq!((world.structure_epoch, world.body_count()), (epoch, bodies));
}
#[test]
fn a_created_chain_reports_its_parts_and_joints() {
let mut world = PhysicsWorld::new(WorldSettings::default().gravity(Vec3::ZERO)).unwrap();
let skeleton = Skeleton::new(&[
SkeletonJoint {
name: "root",
parent: None,
},
SkeletonJoint {
name: "child",
parent: Some(0),
},
])
.unwrap();
let shape = Shape::new_capsule(0.3, 0.2).unwrap();
let joint = SwingTwistConstraintSettings::new(
RVec3::new(0.0, 0.5, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
)
.half_cone_angles(0.5, 0.5);
let parts: Vec<RagdollPart<'_>> = (0..2)
.map(|i| RagdollPart {
shape: &shape,
body: BodySettings::new_dynamic().position(RVec3::new(0.0, i as Real, 0.0)),
joint: (i == 1).then(|| RagdollJoint::SwingTwist(joint.clone())),
})
.collect();
let settings = RagdollSettings::new(&skeleton, &parts).unwrap();
let id = world
.create_ragdoll(&settings, None, Activation::Activate)
.unwrap();
let ragdoll = world.ragdoll(id).unwrap();
assert_eq!(ragdoll.part_count(), 2);
assert_eq!(ragdoll.joint(0), None);
assert_eq!(ragdoll.joint(2), None);
assert!(matches!(
ragdoll.joint(1),
Some(JointReading::SwingTwist { .. })
));
let pose = ragdoll.pose();
assert!(distance_squared(pose.position(1), RVec3::new(0.0, 1.0, 0.0)) < 1e-10);
assert_eq!(world.body_count(), 2);
world.remove_ragdoll(id).unwrap();
assert_eq!(world.body_count(), 0);
}
}