use std::collections::BTreeMap;
use oxijolt_sys::*;
use super::settings::{validate_local, JointTransform, Skeleton, SkeletonPose};
use crate::owned::{JoltObject, Owned};
use crate::{Quat, RVec3, RagdollError, Real, Vec3};
impl JoltObject for JPH_SkeletonMapper {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_SkeletonMapper_Destroy(ptr) };
}
}
const LOCK_RULE: &str =
"locked joints exist and are neither the root nor the joint the ragdoll root maps to";
const MAPPED_POSITION_RULE: &str = "the mapped pose leaves limits::MAX_POSITION";
const MAPPED_ROTATION_RULE: &str = "the mapped pose has a rotation that is not a unit quaternion";
#[derive(Clone, Copy, Debug)]
pub struct MappedSkeleton<'a> {
pub skeleton: &'a Skeleton,
pub neutral_pose: &'a SkeletonPose,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TranslationLocks<'a> {
#[default]
None,
All,
Joints(&'a [u32]),
}
pub struct SkeletonMapper {
mapper: Owned<JPH_SkeletonMapper>,
ragdoll_joint_count: u32,
animation_joint_count: u32,
}
impl std::fmt::Debug for SkeletonMapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SkeletonMapper")
.field("ragdoll_joint_count", &self.ragdoll_joint_count)
.field("animation_joint_count", &self.animation_joint_count)
.finish_non_exhaustive()
}
}
unsafe impl Send for SkeletonMapper {}
unsafe impl Sync for SkeletonMapper {}
impl SkeletonMapper {
pub fn new(
ragdoll: MappedSkeleton<'_>,
animation: MappedSkeleton<'_>,
locks: TranslationLocks<'_>,
) -> Result<Self, RagdollError> {
let ragdoll_count = ragdoll.skeleton.names().len();
let animation_count = animation.skeleton.names().len();
ragdoll.neutral_pose.validate(ragdoll_count)?;
animation.neutral_pose.validate(animation_count)?;
let mapped = match_joints(ragdoll.skeleton, animation.skeleton)?;
let mask = match locks {
TranslationLocks::Joints(joints) => lock_mask(joints, mapped[0], animation_count)?,
TranslationLocks::None | TranslationLocks::All => Vec::new(),
};
let neutral1 = ragdoll
.neutral_pose
.joints
.iter()
.map(|joint| matrix(joint.translation, joint.rotation))
.collect::<Vec<_>>();
let neutral2 = reexpress(animation.neutral_pose, ragdoll.neutral_pose.root_offset)
.iter()
.map(|joint| matrix(joint.translation, joint.rotation))
.collect::<Vec<_>>();
let mapper = unsafe { Owned::from_raw(JPH_SkeletonMapper_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the mapper"));
let (count1, count2) = (ragdoll_count as u32, animation_count as u32);
let done = unsafe {
JPH_SkeletonMapper_Initialize2(
mapper.as_ptr(),
ragdoll.skeleton.as_ptr(),
neutral1.as_ptr(),
count1,
animation.skeleton.as_ptr(),
neutral2.as_ptr(),
count2,
) && match locks {
TranslationLocks::None => true,
TranslationLocks::All => JPH_SkeletonMapper_LockAllTranslations2(
mapper.as_ptr(),
animation.skeleton.as_ptr(),
neutral2.as_ptr(),
count2,
),
TranslationLocks::Joints(_) => JPH_SkeletonMapper_LockTranslations2(
mapper.as_ptr(),
animation.skeleton.as_ptr(),
mask.as_ptr(),
neutral2.as_ptr(),
count2,
),
}
};
if !done {
unreachable!("the mapper's inputs were checked against every refusal of joltc_ext");
}
Ok(Self {
mapper,
ragdoll_joint_count: count1,
animation_joint_count: count2,
})
}
pub fn map(
&self,
ragdoll_pose: &SkeletonPose,
animation_local: &[JointTransform],
) -> Result<SkeletonPose, RagdollError> {
ragdoll_pose.validate(self.ragdoll_joint_count as usize)?;
validate_local(animation_local, self.animation_joint_count as usize)?;
let pose1 = matrices(&ragdoll_pose.joints);
let local2 = matrices(animation_local);
let mut out = vec![zero_matrix(); animation_local.len()];
let mut degenerate = -1;
let done = unsafe {
JPH_SkeletonMapper_Map2(
self.mapper.as_ptr(),
pose1.as_ptr(),
self.ragdoll_joint_count,
local2.as_ptr(),
self.animation_joint_count,
out.as_mut_ptr(),
&mut degenerate,
)
};
if !done {
return match u32::try_from(degenerate) {
Ok(joint) => Err(RagdollError::DegenerateChain(joint)),
Err(_) => unreachable!("the poses were checked against every other refusal"),
};
}
mapped_pose(ragdoll_pose.root_offset, &out)
}
pub fn map_reverse(&self, animation_pose: &SkeletonPose) -> Result<SkeletonPose, RagdollError> {
animation_pose.validate(self.animation_joint_count as usize)?;
let pose2 = matrices(&animation_pose.joints);
let mut out = vec![zero_matrix(); self.ragdoll_joint_count as usize];
let done = unsafe {
JPH_SkeletonMapper_MapReverse2(
self.mapper.as_ptr(),
pose2.as_ptr(),
self.animation_joint_count,
out.as_mut_ptr(),
self.ragdoll_joint_count,
)
};
if !done {
unreachable!("the pose was checked against every refusal of joltc_ext");
}
mapped_pose(animation_pose.root_offset, &out)
}
pub fn mapped_joint(&self, ragdoll_joint: u32) -> Option<u32> {
if ragdoll_joint >= self.ragdoll_joint_count {
return None;
}
let joint = unsafe {
JPH_SkeletonMapper_GetMappedJointIndex(self.mapper.as_ptr(), ragdoll_joint as i32)
};
u32::try_from(joint).ok()
}
pub fn is_translation_locked(&self, animation_joint: u32) -> bool {
animation_joint < self.animation_joint_count
&& unsafe {
JPH_SkeletonMapper_IsJointTranslationLocked(
self.mapper.as_ptr(),
animation_joint as i32,
)
}
}
pub fn ragdoll_joint_count(&self) -> u32 {
self.ragdoll_joint_count
}
pub fn animation_joint_count(&self) -> u32 {
self.animation_joint_count
}
}
fn match_joints(ragdoll: &Skeleton, animation: &Skeleton) -> Result<Vec<u32>, RagdollError> {
let by_name: BTreeMap<&str, u32> = animation
.names()
.iter()
.enumerate()
.map(|(index, name)| (name.as_str(), index as u32))
.collect();
let mapped = ragdoll
.names()
.iter()
.enumerate()
.map(|(joint, name)| {
by_name
.get(name.as_str())
.copied()
.ok_or(RagdollError::UnmappedJoint(joint as u32))
})
.collect::<Result<Vec<u32>, _>>()?;
let mut is_mapped = vec![false; animation.names().len()];
for &joint in &mapped {
is_mapped[joint as usize] = true;
}
for (joint, &animation_joint) in mapped.iter().enumerate() {
let expected = ragdoll.parents()[joint].map(|parent| mapped[parent as usize]);
if mapped_ancestor(animation.parents(), &is_mapped, animation_joint) != expected {
return Err(RagdollError::HierarchyMismatch(joint as u32));
}
}
Ok(mapped)
}
fn mapped_ancestor(parents: &[Option<u32>], is_mapped: &[bool], joint: u32) -> Option<u32> {
let mut current = parents[joint as usize];
while let Some(ancestor) = current {
if is_mapped[ancestor as usize] {
return Some(ancestor);
}
current = parents[ancestor as usize];
}
None
}
fn lock_mask(joints: &[u32], mapped_root: u32, count: usize) -> Result<Vec<bool>, RagdollError> {
let mut mask = vec![false; count];
for &joint in joints {
if joint == 0 || joint == mapped_root || joint as usize >= count {
return Err(RagdollError::InvalidValue(LOCK_RULE));
}
mask[joint as usize] = true;
}
Ok(mask)
}
fn reexpress(pose: &SkeletonPose, root_offset: RVec3) -> Vec<JointTransform> {
let o = pose.root_offset;
#[allow(clippy::unnecessary_cast)]
let shift = |own: Real, other: Real, t: f32| (own - other + Real::from(t)) as f32;
pose.joints
.iter()
.map(|joint| {
let t = joint.translation;
JointTransform {
translation: Vec3::new(
shift(o.x, root_offset.x, t.x),
shift(o.y, root_offset.y, t.y),
shift(o.z, root_offset.z, t.z),
),
rotation: joint.rotation,
}
})
.collect()
}
fn zero_matrix() -> JPH_Mat4 {
let column = JPH_Vec4 {
x: 0.0,
y: 0.0,
z: 0.0,
w: 0.0,
};
JPH_Mat4 {
column: [column; 4],
}
}
fn matrix(translation: Vec3, rotation: Quat) -> JPH_Mat4 {
let mut result = zero_matrix();
unsafe {
JPH_Mat4_RotationTranslation(
&mut result,
&rotation.normalized().to_jph(),
&translation.to_jph(),
)
};
result
}
fn matrices(joints: &[JointTransform]) -> Vec<JPH_Mat4> {
joints
.iter()
.map(|joint| matrix(joint.translation, joint.rotation))
.collect()
}
fn mapped_pose(root_offset: RVec3, matrices: &[JPH_Mat4]) -> Result<SkeletonPose, RagdollError> {
let joints = matrices
.iter()
.map(|matrix| {
let mut translation = Vec3::ZERO.to_jph();
let mut rotation = Quat::IDENTITY.to_jph();
unsafe {
JPH_Mat4_GetTranslation(matrix, &mut translation);
JPH_Mat4_GetQuaternion(matrix, &mut rotation);
}
JointTransform {
translation: Vec3::from_jph(translation),
rotation: Quat::from_jph(rotation).normalized(),
}
})
.collect();
let pose = SkeletonPose {
root_offset,
joints,
};
match pose.validate(matrices.len()) {
Ok(()) => Ok(pose),
Err(_) if pose.joints.iter().any(|j| !j.rotation.is_valid_rotation()) => {
Err(RagdollError::InvalidValue(MAPPED_ROTATION_RULE))
}
Err(_) => Err(RagdollError::InvalidValue(MAPPED_POSITION_RULE)),
}
}
#[cfg(test)]
mod tests;