use std::collections::BTreeSet;
use std::ffi::CString;
use oxijolt_sys::*;
use super::PART_MOTION_RULE;
use crate::body::{
has_finite_inverse, mass_properties, CreationSettings, INERTIA_RULE, STATIC_SHAPE_RULE,
};
use crate::limits::{self, MASS_RULE};
use crate::owned::{JoltObject, Owned};
use crate::world::ensure_initialized;
use crate::{
AllowedDofs, BodyError, BodySettings, HingeConstraintSettings, MotionType, ObjectLayer, Quat,
RVec3, RagdollError, Real, Shape, SixDofConstraintSettings, SpringSettings,
SwingTwistConstraintSettings, Vec3,
};
impl JoltObject for JPH_Skeleton {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_Skeleton_Destroy(ptr) };
}
}
impl JoltObject for JPH_RagdollSettings {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_RagdollSettings_Destroy(ptr) };
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SkeletonJoint<'a> {
pub name: &'a str,
pub parent: Option<u32>,
}
pub struct Skeleton {
skeleton: Owned<JPH_Skeleton>,
names: Vec<String>,
parents: Vec<Option<u32>>,
}
impl std::fmt::Debug for Skeleton {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Skeleton")
.field("names", &self.names)
.field("parents", &self.parents)
.finish_non_exhaustive()
}
}
unsafe impl Send for Skeleton {}
unsafe impl Sync for Skeleton {}
impl Skeleton {
pub const MAX_JOINTS: usize = 1024;
pub fn new(joints: &[SkeletonJoint<'_>]) -> Result<Self, RagdollError> {
let invalid = |what| Err(RagdollError::InvalidValue(what));
if joints.is_empty() || joints.len() > Self::MAX_JOINTS {
return invalid("a skeleton has between 1 and 1024 joints");
}
let mut names = BTreeSet::new();
let mut c_names = Vec::with_capacity(joints.len());
for (index, joint) in joints.iter().enumerate() {
let valid_parent = match joint.parent {
None => index == 0,
Some(parent) => (parent as usize) < index,
};
if !valid_parent {
return invalid("joint 0 is the only root and every parent precedes its child");
}
if !names.insert(joint.name) {
return invalid("skeleton joint names must be unique");
}
let Ok(name) = CString::new(joint.name) else {
return invalid("skeleton joint names must not contain NUL");
};
c_names.push(name);
}
if !ensure_initialized() {
return Err(RagdollError::InitFailed);
}
let skeleton = unsafe { Owned::from_raw(JPH_Skeleton_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the skeleton"));
for (joint, name) in joints.iter().zip(&c_names) {
let parent = joint.parent.map_or(-1, |parent| parent as i32);
unsafe { JPH_Skeleton_AddJoint2(skeleton.as_ptr(), name.as_ptr(), parent) };
}
Ok(Self {
skeleton,
names: joints.iter().map(|joint| joint.name.to_owned()).collect(),
parents: joints.iter().map(|joint| joint.parent).collect(),
})
}
pub fn joint_count(&self) -> u32 {
self.names.len() as u32
}
pub fn joint(&self, index: u32) -> Option<SkeletonJoint<'_>> {
let index = index as usize;
Some(SkeletonJoint {
name: self.names.get(index)?,
parent: self.parents[index],
})
}
pub fn joint_index(&self, name: &str) -> Option<u32> {
self.names
.iter()
.position(|joint| joint == name)
.map(|index| index as u32)
}
pub(super) fn as_ptr(&self) -> *mut JPH_Skeleton {
self.skeleton.as_ptr()
}
pub(super) fn names(&self) -> &[String] {
&self.names
}
pub(super) fn parents(&self) -> &[Option<u32>] {
&self.parents
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum RagdollJoint {
SwingTwist(SwingTwistConstraintSettings),
Hinge(HingeConstraintSettings),
SixDof(SixDofConstraintSettings),
}
impl RagdollJoint {
fn kind(&self) -> JointKind {
match self {
Self::SwingTwist(_) => JointKind::SwingTwist,
Self::Hinge(_) => JointKind::Hinge,
Self::SixDof(_) => JointKind::SixDof,
}
}
fn validate(&self) -> Result<(), &'static str> {
match self {
Self::SwingTwist(settings) => settings.validate(),
Self::Hinge(settings) => settings.validate(),
Self::SixDof(settings) => settings.validate(),
}
}
fn springs(&self) -> Box<dyn Iterator<Item = SpringSettings> + '_> {
match self {
Self::SwingTwist(settings) => Box::new(settings.springs()),
Self::Hinge(settings) => Box::new(settings.springs()),
Self::SixDof(settings) => Box::new(settings.springs()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum JointKind {
SwingTwist,
Hinge,
SixDof,
}
impl JointKind {
pub(crate) fn sub_type(self) -> JPH_ConstraintSubType {
match self {
Self::SwingTwist => JPH_ConstraintSubType_SwingTwist,
Self::Hinge => JPH_ConstraintSubType_Hinge,
Self::SixDof => JPH_ConstraintSubType_SixDOF,
}
}
}
#[derive(Clone, Debug)]
pub struct RagdollPart<'a> {
pub shape: &'a Shape,
pub body: BodySettings,
pub joint: Option<RagdollJoint>,
}
pub struct RagdollSettings {
settings: Owned<JPH_RagdollSettings>,
parents: Vec<Option<u32>>,
joints: Vec<Option<JointKind>>,
object_layers: Vec<ObjectLayer>,
}
impl std::fmt::Debug for RagdollSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RagdollSettings")
.field("parents", &self.parents)
.field("object_layers", &self.object_layers)
.finish_non_exhaustive()
}
}
unsafe impl Send for RagdollSettings {}
unsafe impl Sync for RagdollSettings {}
impl RagdollSettings {
pub fn new(skeleton: &Skeleton, parts: &[RagdollPart<'_>]) -> Result<Self, RagdollError> {
Self::build(skeleton, parts, false)
}
pub fn new_stabilized(
skeleton: &Skeleton,
parts: &[RagdollPart<'_>],
) -> Result<Self, RagdollError> {
Self::build(skeleton, parts, true)
}
fn build(
skeleton: &Skeleton,
parts: &[RagdollPart<'_>],
stabilize: bool,
) -> Result<Self, RagdollError> {
validate_parts(skeleton, parts)?;
let bound = effective_mass_bound(parts, stabilize);
let springs_fit = parts
.iter()
.filter_map(|part| part.joint.as_ref())
.flat_map(RagdollJoint::springs)
.all(|spring| spring.fits_effective_mass(bound));
if !springs_fit {
return Err(RagdollError::InvalidValue(
"joint spring stiffness or damping exceeds limits::MAX_SPRING_COEFFICIENT",
));
}
let (settings, table) = unsafe {
(
Owned::from_raw(JPH_RagdollSettings_Create())
.unwrap_or_else(|| unreachable!("joltc `new`s the settings")),
Owned::from_raw(JPH_GroupFilterTable_Create(parts.len() as u32))
.unwrap_or_else(|| unreachable!("joltc `new`s the table")),
)
};
let count = parts.len() as u32;
unsafe {
JPH_RagdollSettings_SetSkeleton(settings.as_ptr(), skeleton.skeleton.as_ptr());
JPH_RagdollSettings_ResizeParts(settings.as_ptr(), count as i32);
for a in 0..count {
for b in a + 1..count {
JPH_GroupFilterTable_DisableCollision(table.as_ptr(), a, b);
}
}
}
for (index, part) in parts.iter().enumerate() {
let creation =
CreationSettings::new(part.shape, &part.body).map_err(RagdollError::Body)?;
let group = JPH_CollisionGroup {
groupFilter: table.as_ptr().cast(),
groupID: 0,
subGroupID: index as u32,
};
unsafe {
JPH_BodyCreationSettings_SetCollisionGroup(creation.as_ptr(), &group);
JPH_RagdollSettings_SetPart(settings.as_ptr(), index as i32, creation.as_ptr());
match &part.joint {
None => {}
Some(RagdollJoint::SwingTwist(joint)) => {
JPH_RagdollSettings_SetPartToParentSwingTwist(
settings.as_ptr(),
index as i32,
&joint.to_jph(),
)
}
Some(RagdollJoint::Hinge(joint)) => JPH_RagdollSettings_SetPartToParentHinge(
settings.as_ptr(),
index as i32,
&joint.to_jph(),
),
Some(RagdollJoint::SixDof(joint)) => JPH_RagdollSettings_SetPartToParentSixDOF(
settings.as_ptr(),
index as i32,
&joint.to_jph(),
),
}
}
}
unsafe {
if stabilize && !JPH_RagdollSettings_Stabilize(settings.as_ptr()) {
return Err(RagdollError::InvalidValue(
"Jolt could not stabilize the part inertias",
));
}
JPH_RagdollSettings_CalculateConstraintPriorities(settings.as_ptr(), 0);
JPH_RagdollSettings_CalculateBodyIndexToConstraintIndex(settings.as_ptr());
JPH_RagdollSettings_CalculateConstraintIndexToBodyIdxPair(settings.as_ptr());
}
drop(table);
Ok(Self {
settings,
parents: skeleton.parents.clone(),
joints: parts
.iter()
.map(|part| part.joint.as_ref().map(RagdollJoint::kind))
.collect(),
object_layers: parts.iter().map(|part| part.body.object_layer).collect(),
})
}
pub fn part_count(&self) -> u32 {
self.parents.len() as u32
}
pub(super) fn as_ptr(&self) -> *mut JPH_RagdollSettings {
self.settings.as_ptr()
}
pub(super) fn parents(&self) -> &[Option<u32>] {
&self.parents
}
pub(super) fn joints(&self) -> &[Option<JointKind>] {
&self.joints
}
pub(super) fn object_layers(&self) -> &[ObjectLayer] {
&self.object_layers
}
}
fn validate_parts(skeleton: &Skeleton, parts: &[RagdollPart<'_>]) -> Result<(), RagdollError> {
let invalid = |what| Err(RagdollError::InvalidValue(what));
if parts.len() != skeleton.names.len() {
return invalid("a ragdoll has exactly one part per skeleton joint");
}
for (index, part) in parts.iter().enumerate() {
if part.joint.is_some() != (index > 0) {
return invalid("the root part has no joint and every other part has one");
}
part.body.validate_values().map_err(|error| match error {
BodyError::InvalidValue(what) => RagdollError::InvalidValue(what),
error => RagdollError::Body(error),
})?;
if part.body.motion_type == MotionType::Static {
return invalid(PART_MOTION_RULE);
}
if part.body.allowed_dofs != AllowedDofs::ALL {
return invalid("ragdoll parts keep all six degrees of freedom");
}
if part.body.user_data != 0 {
return invalid("ragdoll parts take no user data: Jolt gives every part the ragdoll's");
}
if part.body.collision_group.is_some() {
return invalid("ragdoll parts use the ragdoll's own collision group");
}
if unsafe { JPH_Shape_MustBeStatic(part.shape.as_ptr()) } {
return invalid(STATIC_SHAPE_RULE);
}
let properties = mass_properties(part.shape, part.body.mass);
if !has_finite_inverse(&properties) {
return invalid(INERTIA_RULE);
}
if !limits::is_mass(properties.mass) {
return invalid(MASS_RULE);
}
if let Some(joint) = &part.joint {
joint.validate().map_err(RagdollError::InvalidValue)?;
}
}
Ok(())
}
fn effective_mass_bound(parts: &[RagdollPart<'_>], stabilize: bool) -> f64 {
let masses: Vec<(f64, f64)> = parts
.iter()
.map(|part| {
let properties = mass_properties(part.shape, part.body.mass);
let [x, y, z, _] = properties.inertia.column;
let trace = f64::from(x.x) + f64::from(y.y) + f64::from(z.z);
(f64::from(properties.mass), trace)
})
.collect();
if stabilize {
let total: f64 = masses.iter().map(|(mass, _)| mass).sum();
let ratio = masses
.iter()
.map(|(mass, trace)| trace / mass)
.fold(0.0, f64::max);
total.max(2.0 * total * ratio)
} else {
masses
.iter()
.map(|&(mass, trace)| mass.max(trace))
.fold(0.0, f64::max)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct JointTransform {
pub translation: Vec3,
pub rotation: Quat,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SkeletonPose {
pub root_offset: RVec3,
pub joints: Vec<JointTransform>,
}
impl SkeletonPose {
pub(crate) fn position(&self, index: usize) -> RVec3 {
let t = self.joints[index].translation;
RVec3::new(
self.root_offset.x + Real::from(t.x),
self.root_offset.y + Real::from(t.y),
self.root_offset.z + Real::from(t.z),
)
}
pub(crate) fn validate(&self, joint_count: usize) -> Result<(), RagdollError> {
let invalid = |what| Err(RagdollError::InvalidValue(what));
if self.joints.len() != joint_count {
return invalid("a pose has exactly one transform per skeleton joint");
}
if !limits::is_in_frame(self.root_offset) {
return invalid("pose root offset must be finite and within limits::MAX_POSITION");
}
for (index, joint) in self.joints.iter().enumerate() {
if !joint.translation.is_finite() {
return invalid("pose translations must be finite");
}
if !joint.rotation.is_valid_rotation() {
return invalid("pose rotations must be finite unit quaternions");
}
if !limits::is_in_frame(self.position(index)) {
return invalid("pose positions must be finite and within limits::MAX_POSITION");
}
}
Ok(())
}
}
pub(crate) fn validate_local(
joints: &[JointTransform],
joint_count: usize,
) -> Result<(), RagdollError> {
let invalid = |what| Err(RagdollError::InvalidValue(what));
if joints.len() != joint_count {
return invalid("a pose has exactly one transform per skeleton joint");
}
for joint in joints {
let t = joint.translation;
if !limits::is_in_frame(RVec3::new(
Real::from(t.x),
Real::from(t.y),
Real::from(t.z),
)) {
return invalid("local translations must be finite and within limits::MAX_POSITION");
}
if !joint.rotation.is_valid_rotation() {
return invalid("pose rotations must be finite unit quaternions");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MotorSettings, SixDofAxis, SixDofConstraintAxis, SwingType};
fn joint(name: &str, parent: Option<u32>) -> SkeletonJoint<'_> {
SkeletonJoint { name, parent }
}
fn chain() -> Skeleton {
Skeleton::new(&[
joint("root", None),
joint("child", Some(0)),
joint("grandchild", Some(1)),
])
.unwrap()
}
#[track_caller]
fn assert_invalid<T>(result: Result<T, RagdollError>) {
assert!(
matches!(result, Err(RagdollError::InvalidValue(_))),
"accepted"
);
}
#[test]
fn skeletons_are_validated() {
let skeleton = chain();
assert_eq!(skeleton.joint_count(), 3);
assert_eq!(skeleton.joint(1), Some(joint("child", Some(0))));
assert_eq!(skeleton.joint(3), None);
assert_eq!(skeleton.joint_index("grandchild"), Some(2));
assert_eq!(skeleton.joint_index("nobody"), None);
assert!(unsafe { JPH_Skeleton_AreJointsCorrectlyOrdered(skeleton.skeleton.as_ptr()) });
assert_invalid(Skeleton::new(&[]));
assert_invalid(Skeleton::new(&[joint("root", Some(0))]));
assert_invalid(Skeleton::new(&[joint("a", None), joint("b", None)]));
assert_invalid(Skeleton::new(&[joint("a", None), joint("b", Some(1))]));
assert_invalid(Skeleton::new(&[joint("a", None), joint("a", Some(0))]));
assert_invalid(Skeleton::new(&[joint("a\0b", None)]));
let names: Vec<String> = (0..=Skeleton::MAX_JOINTS)
.map(|i| format!("j{i}"))
.collect();
let too_many: Vec<SkeletonJoint<'_>> = names
.iter()
.enumerate()
.map(|(i, name)| joint(name, (i > 0).then_some(0)))
.collect();
assert_invalid(Skeleton::new(&too_many));
assert!(Skeleton::new(&too_many[..Skeleton::MAX_JOINTS]).is_ok());
}
fn capsule() -> Shape {
Shape::new_capsule(0.3, 0.2).unwrap()
}
fn twist() -> RagdollJoint {
RagdollJoint::SwingTwist(
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),
)
}
fn parts(shape: &Shape) -> Vec<RagdollPart<'_>> {
(0..3)
.map(|i| RagdollPart {
shape,
body: BodySettings::new_dynamic().position(RVec3::new(0.0, i as Real, 0.0)),
joint: (i > 0).then(twist),
})
.collect()
}
#[test]
fn ragdoll_settings_are_validated() {
let skeleton = chain();
let shape = capsule();
let settings = RagdollSettings::new(&skeleton, &parts(&shape)).unwrap();
assert_eq!(settings.part_count(), 3);
assert_eq!(settings.parents(), &[None, Some(0), Some(1)]);
assert_eq!(
settings.joints(),
&[
None,
Some(JointKind::SwingTwist),
Some(JointKind::SwingTwist)
]
);
let edit = |change: &dyn Fn(&mut Vec<RagdollPart<'_>>)| {
let mut parts = parts(&shape);
change(&mut parts);
RagdollSettings::new(&skeleton, &parts).map(|_| ())
};
assert_invalid(edit(&|parts| {
parts.pop();
}));
assert_invalid(edit(&|parts| parts[0].joint = Some(twist())));
assert_invalid(edit(&|parts| parts[2].joint = None));
assert_invalid(edit(&|parts| parts[1].body = BodySettings::new_static()));
assert_invalid(edit(&|parts| {
parts[1].body = BodySettings::new_dynamic().mass(1.0e-39)
}));
assert_invalid(edit(&|parts| {
parts[1].body = BodySettings::new_dynamic().friction(-1.0)
}));
assert_invalid(edit(&|parts| {
parts[1].joint = Some(RagdollJoint::Hinge(
HingeConstraintSettings::default().limits(0.5, 1.0),
))
}));
assert_invalid(edit(&|parts| {
parts[2].joint = Some(RagdollJoint::SixDof(
SixDofConstraintSettings::default()
.axis(
SixDofConstraintAxis::RotationZ,
SixDofAxis::Limited {
min: -0.2,
max: 0.5,
},
)
.motor(
SixDofConstraintAxis::RotationZ,
MotorSettings::default().torque_limits(1.0, 0.0),
),
))
}));
let pyramid = SixDofConstraintSettings::default()
.swing_type(SwingType::Pyramid)
.axis(
SixDofConstraintAxis::RotationZ,
SixDofAxis::Limited {
min: -0.2,
max: 0.5,
},
);
assert_eq!(
edit(&|parts| parts[2].joint = Some(RagdollJoint::SixDof(pyramid.clone()))),
Ok(())
);
assert_eq!(
edit(&|parts| parts[1].body = BodySettings::new_kinematic()),
Ok(())
);
}
#[test]
fn heightfield_parts_are_rejected() {
let skeleton = Skeleton::new(&[joint("root", None)]).unwrap();
let height_field =
Shape::new_height_field(33, &[0.0; 33 * 33], &crate::HeightFieldSettings::default())
.unwrap();
let part = RagdollPart {
shape: &height_field,
body: BodySettings::new_dynamic(),
joint: None,
};
assert_invalid(RagdollSettings::new(&skeleton, &[part]));
}
#[test]
fn stabilized_settings_build() {
let skeleton = chain();
let shape = capsule();
let mut parts = parts(&shape);
parts[2].body = parts[2].body.clone().mass(30.0);
assert!(RagdollSettings::new_stabilized(&skeleton, &parts).is_ok());
}
fn pose() -> SkeletonPose {
SkeletonPose {
root_offset: RVec3::new(1.0, 2.0, 3.0),
joints: vec![
JointTransform {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
};
3
],
}
}
#[test]
fn poses_are_validated() {
assert_eq!(pose().validate(3), Ok(()));
assert_invalid(pose().validate(2));
let mut bad = pose();
bad.root_offset.x = Real::INFINITY;
assert_invalid(bad.validate(3));
let mut bad = pose();
bad.joints[1].translation.y = f32::NAN;
assert_invalid(bad.validate(3));
let mut bad = pose();
bad.joints[2].rotation = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
assert_invalid(bad.validate(3));
let mut far = pose();
far.root_offset.x = Real::MAX;
assert_invalid(far.validate(3));
let mut edge = pose();
edge.root_offset = RVec3::new(limits::MAX_POSITION, 0.0, 0.0);
assert_eq!(edge.validate(3), Ok(()));
edge.joints[1].translation.x = 1.0;
assert_invalid(edge.validate(3));
assert_eq!(pose().position(0), RVec3::new(1.0, 2.0, 3.0));
}
#[test]
fn joint_kinds_map_to_jolts_subtypes() {
assert_eq!(
JointKind::SwingTwist.sub_type(),
JPH_ConstraintSubType_SwingTwist
);
assert_eq!(JointKind::Hinge.sub_type(), JPH_ConstraintSubType_Hinge);
assert_eq!(JointKind::SixDof.sub_type(), JPH_ConstraintSubType_SixDOF);
}
}