Skip to main content

oxijolt/ragdoll/
mapper.rs

1//! Jolt's `SkeletonMapper` between a ragdoll skeleton and a detailed animation skeleton.
2
3use std::collections::BTreeMap;
4
5use oxijolt_sys::*;
6
7use super::settings::{validate_local, JointTransform, Skeleton, SkeletonPose};
8use crate::owned::{JoltObject, Owned};
9use crate::{Quat, RVec3, RagdollError, Real, Vec3};
10
11/// A skeleton mapper, owned with the one reference `JPH_SkeletonMapper_Create` returns, which
12/// `JPH_SkeletonMapper_Destroy` releases.
13impl JoltObject for JPH_SkeletonMapper {
14    unsafe fn destroy(ptr: *mut Self) {
15        // SAFETY: the owner holds one reference (trait contract), which this releases.
16        unsafe { JPH_SkeletonMapper_Destroy(ptr) };
17    }
18}
19
20const LOCK_RULE: &str =
21    "locked joints exist and are neither the root nor the joint the ragdoll root maps to";
22const MAPPED_POSITION_RULE: &str = "the mapped pose leaves limits::MAX_POSITION";
23const MAPPED_ROTATION_RULE: &str = "the mapped pose has a rotation that is not a unit quaternion";
24
25/// One skeleton of a [`SkeletonMapper`] with its neutral pose.
26#[derive(Clone, Copy, Debug)]
27pub struct MappedSkeleton<'a> {
28    /// The skeleton.
29    pub skeleton: &'a Skeleton,
30    /// The skeleton in its neutral pose, in model space. The two neutral poses describe the
31    /// character standing in one place, so that joints of the same name are where they belong
32    /// to each other. [`SkeletonMapper::new`] does not check that they are close: neutral poses
33    /// far apart put long translations into the transforms between the skeletons, and
34    /// [`SkeletonMapper::map`] then refuses chains it can no longer turn reliably
35    /// ([`RagdollError::DegenerateChain`]).
36    ///
37    /// See [docs/limits.md#skeleton-mapper-neutral-poses].
38    ///
39    /// [docs/limits.md#skeleton-mapper-neutral-poses]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#skeleton-mapper-neutral-poses
40    pub neutral_pose: &'a SkeletonPose,
41}
42
43/// Which animation joints [`SkeletonMapper::map`] keeps at their neutral offset from their
44/// parent (Jolt `LockTranslations`). Joint constraints stretch a little under load; a lock hides
45/// that stretch in the animation pose, which then differs from the simulated bodies.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub enum TranslationLocks<'a> {
48    /// No joint is locked.
49    #[default]
50    None,
51    /// Every descendant of the animation joint the ragdoll root maps to (Jolt
52    /// `LockAllTranslations`).
53    All,
54    /// These animation joints. The animation root (joint 0) and the joint the ragdoll root maps
55    /// to are refused: their position comes from the simulation.
56    Joints(&'a [u32]),
57}
58
59/// Jolt's `SkeletonMapper`: maps poses between a ragdoll skeleton and a more detailed animation
60/// skeleton of the same character.
61///
62/// Joints are matched by name. Every ragdoll joint needs an animation joint of its name, and
63/// the animation skeleton keeps the ragdoll's hierarchy: the animation joint of a ragdoll joint
64/// sits below the animation joint of its parent, with only unmatched joints in between. The
65/// animation skeleton may also have extra joints above the ragdoll root and below its leaves.
66///
67/// - [`map`](Self::map) turns a ragdoll pose into an animation pose (Jolt `Map`), to show the
68///   simulated ragdoll on the detailed skeleton. Matched joints follow their ragdoll joint. An
69///   unmatched joint between a ragdoll joint and one of its children is a chain: the chain's
70///   start turns so that the chain, laid out by the local animation pose, points at the child's
71///   ragdoll joint. Jolt builds one chain per start joint, from the child with the longest path
72///   (the lowest ragdoll joint index on a tie); unmatched joints below the start's other children
73///   keep their local transforms, as do all other unmatched joints, extra roots included (relative
74///   to the pose's root offset). Translation locks apply last.
75/// - [`map_reverse`](Self::map_reverse) turns an animation pose into a ragdoll pose (Jolt
76///   `MapReverse`) from the matched joints only, as a target for
77///   [`RagdollMut::set_pose`](crate::RagdollMut::set_pose) or the drives.
78///
79/// The mapper copies what it needs and keeps no reference to the skeletons or poses. It never
80/// changes after [`new`](Self::new); both mappings are pure functions of their inputs.
81pub struct SkeletonMapper {
82    mapper: Owned<JPH_SkeletonMapper>,
83    ragdoll_joint_count: u32,
84    animation_joint_count: u32,
85}
86
87impl std::fmt::Debug for SkeletonMapper {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("SkeletonMapper")
90            .field("ragdoll_joint_count", &self.ragdoll_joint_count)
91            .field("animation_joint_count", &self.animation_joint_count)
92            .finish_non_exhaustive()
93    }
94}
95
96// SAFETY: the Jolt mapper is never changed after `new`; `Map`, `MapReverse`, `GetMappedJointIdx`
97// and `IsJointTranslationLocked` only read its members (SkeletonMapper.cpp:165-235), the
98// extension's temporary arrays come from Jolt's thread-safe allocator, and `RefTarget` counts
99// references atomically (Reference.h:57-86).
100unsafe impl Send for SkeletonMapper {}
101// SAFETY: as for `Send`; `&SkeletonMapper` only maps poses and reads the mapping.
102unsafe impl Sync for SkeletonMapper {}
103
104impl SkeletonMapper {
105    /// A mapper from `ragdoll` (Jolt's skeleton 1) to `animation` (skeleton 2) with `locks`.
106    ///
107    /// The animation neutral pose is re-expressed relative to the ragdoll neutral pose's root
108    /// offset. Fails with [`RagdollError::UnmappedJoint`] for a ragdoll joint without an
109    /// animation joint of its name, [`RagdollError::HierarchyMismatch`] when the hierarchies do
110    /// not correspond, and [`RagdollError::InvalidValue`] when a neutral pose does not fit its
111    /// skeleton ([`RagdollMut::set_pose`](crate::RagdollMut::set_pose)'s rules) or a lock names
112    /// a missing or refused joint.
113    pub fn new(
114        ragdoll: MappedSkeleton<'_>,
115        animation: MappedSkeleton<'_>,
116        locks: TranslationLocks<'_>,
117    ) -> Result<Self, RagdollError> {
118        let ragdoll_count = ragdoll.skeleton.names().len();
119        let animation_count = animation.skeleton.names().len();
120        ragdoll.neutral_pose.validate(ragdoll_count)?;
121        animation.neutral_pose.validate(animation_count)?;
122        let mapped = match_joints(ragdoll.skeleton, animation.skeleton)?;
123        let mask = match locks {
124            TranslationLocks::Joints(joints) => lock_mask(joints, mapped[0], animation_count)?,
125            TranslationLocks::None | TranslationLocks::All => Vec::new(),
126        };
127        let neutral1 = ragdoll
128            .neutral_pose
129            .joints
130            .iter()
131            .map(|joint| matrix(joint.translation, joint.rotation))
132            .collect::<Vec<_>>();
133        let neutral2 = reexpress(animation.neutral_pose, ragdoll.neutral_pose.root_offset)
134            .iter()
135            .map(|joint| matrix(joint.translation, joint.rotation))
136            .collect::<Vec<_>>();
137
138        // SAFETY: Jolt is initialised: a `Skeleton` exists, and `Skeleton::new` ran
139        // `ensure_initialized`. The handle takes over the one reference joltc returns.
140        let mapper = unsafe { Owned::from_raw(JPH_SkeletonMapper_Create()) }
141            .unwrap_or_else(|| unreachable!("joltc `new`s the mapper"));
142        let (count1, count2) = (ragdoll_count as u32, animation_count as u32);
143        // SAFETY: the mapper and both skeletons are live; each array holds one matrix per joint
144        // (the poses were validated against the counts) and, for joint locks, the mask one flag
145        // per animation joint. Both skeletons have at most `Skeleton::MAX_JOINTS` joints, so the stack array
146        // `LockAllTranslations` allocates is small.
147        let done = unsafe {
148            JPH_SkeletonMapper_Initialize2(
149                mapper.as_ptr(),
150                ragdoll.skeleton.as_ptr(),
151                neutral1.as_ptr(),
152                count1,
153                animation.skeleton.as_ptr(),
154                neutral2.as_ptr(),
155                count2,
156            ) && match locks {
157                TranslationLocks::None => true,
158                TranslationLocks::All => JPH_SkeletonMapper_LockAllTranslations2(
159                    mapper.as_ptr(),
160                    animation.skeleton.as_ptr(),
161                    neutral2.as_ptr(),
162                    count2,
163                ),
164                TranslationLocks::Joints(_) => JPH_SkeletonMapper_LockTranslations2(
165                    mapper.as_ptr(),
166                    animation.skeleton.as_ptr(),
167                    mask.as_ptr(),
168                    neutral2.as_ptr(),
169                    count2,
170                ),
171            }
172        };
173        if !done {
174            unreachable!("the mapper's inputs were checked against every refusal of joltc_ext");
175        }
176        Ok(Self {
177            mapper,
178            ragdoll_joint_count: count1,
179            animation_joint_count: count2,
180        })
181    }
182
183    /// Ragdoll pose to animation pose (Jolt `Map`): `ragdoll_pose` in model space and
184    /// `animation_local`, one transform per animation joint relative to its parent (the root's
185    /// relative to the root offset), give the animation pose in model space with the ragdoll
186    /// pose's root offset. `animation_local` gives the unmatched joints; matched joints come
187    /// from the ragdoll pose.
188    ///
189    /// Fails with [`RagdollError::InvalidValue`] when `ragdoll_pose` does not fit the ragdoll
190    /// skeleton ([`RagdollMut::set_pose`](crate::RagdollMut::set_pose)'s rules),
191    /// `animation_local` has the wrong count, a non-unit rotation or a translation component
192    /// above [`limits::MAX_POSITION`](crate::limits::MAX_POSITION), or the result leaves `MAX_POSITION`; with
193    /// [`RagdollError::DegenerateChain`] when a chain is too short to turn (see
194    /// [`limits::MIN_MAPPED_CHAIN_LENGTH`](crate::limits::MIN_MAPPED_CHAIN_LENGTH)).
195    pub fn map(
196        &self,
197        ragdoll_pose: &SkeletonPose,
198        animation_local: &[JointTransform],
199    ) -> Result<SkeletonPose, RagdollError> {
200        ragdoll_pose.validate(self.ragdoll_joint_count as usize)?;
201        validate_local(animation_local, self.animation_joint_count as usize)?;
202        let pose1 = matrices(&ragdoll_pose.joints);
203        let local2 = matrices(animation_local);
204        let mut out = vec![zero_matrix(); animation_local.len()];
205        let mut degenerate = -1;
206        // SAFETY: the mapper is live and only read; `pose1` and `out` hold one matrix per joint
207        // of their skeleton and `local2` one per animation joint, as the counts say.
208        let done = unsafe {
209            JPH_SkeletonMapper_Map2(
210                self.mapper.as_ptr(),
211                pose1.as_ptr(),
212                self.ragdoll_joint_count,
213                local2.as_ptr(),
214                self.animation_joint_count,
215                out.as_mut_ptr(),
216                &mut degenerate,
217            )
218        };
219        if !done {
220            return match u32::try_from(degenerate) {
221                Ok(joint) => Err(RagdollError::DegenerateChain(joint)),
222                Err(_) => unreachable!("the poses were checked against every other refusal"),
223            };
224        }
225        mapped_pose(ragdoll_pose.root_offset, &out)
226    }
227
228    /// Animation pose to ragdoll pose (Jolt `MapReverse`): each ragdoll joint from its matched
229    /// animation joint in `animation_pose` (model space), with the animation pose's root offset.
230    /// Chains, unmatched joints and locks play no part.
231    ///
232    /// Fails with [`RagdollError::InvalidValue`] when `animation_pose` does not fit the
233    /// animation skeleton ([`RagdollMut::set_pose`](crate::RagdollMut::set_pose)'s rules) or
234    /// the result leaves [`limits::MAX_POSITION`](crate::limits::MAX_POSITION).
235    pub fn map_reverse(&self, animation_pose: &SkeletonPose) -> Result<SkeletonPose, RagdollError> {
236        animation_pose.validate(self.animation_joint_count as usize)?;
237        let pose2 = matrices(&animation_pose.joints);
238        let mut out = vec![zero_matrix(); self.ragdoll_joint_count as usize];
239        // SAFETY: the mapper is live and only read; `pose2` holds one matrix per animation joint
240        // and `out` one per ragdoll joint, as the counts say.
241        let done = unsafe {
242            JPH_SkeletonMapper_MapReverse2(
243                self.mapper.as_ptr(),
244                pose2.as_ptr(),
245                self.animation_joint_count,
246                out.as_mut_ptr(),
247                self.ragdoll_joint_count,
248            )
249        };
250        if !done {
251            unreachable!("the pose was checked against every refusal of joltc_ext");
252        }
253        mapped_pose(animation_pose.root_offset, &out)
254    }
255
256    /// The animation joint matched to `ragdoll_joint`; `None` when there is no such ragdoll
257    /// joint.
258    pub fn mapped_joint(&self, ragdoll_joint: u32) -> Option<u32> {
259        if ragdoll_joint >= self.ragdoll_joint_count {
260            return None;
261        }
262        // SAFETY: the mapper is live; the getter only reads it. The index is below
263        // `Skeleton::MAX_JOINTS`, so it fits `int`.
264        let joint = unsafe {
265            JPH_SkeletonMapper_GetMappedJointIndex(self.mapper.as_ptr(), ragdoll_joint as i32)
266        };
267        u32::try_from(joint).ok()
268    }
269
270    /// Whether [`map`](Self::map) keeps `animation_joint` at its neutral offset from its parent;
271    /// `false` when there is no such joint.
272    pub fn is_translation_locked(&self, animation_joint: u32) -> bool {
273        animation_joint < self.animation_joint_count
274            // SAFETY: the mapper is live; the getter only reads it. The index is below
275            // `Skeleton::MAX_JOINTS`, so it fits `int`.
276            && unsafe {
277                JPH_SkeletonMapper_IsJointTranslationLocked(
278                    self.mapper.as_ptr(),
279                    animation_joint as i32,
280                )
281            }
282    }
283
284    /// Number of ragdoll skeleton joints.
285    pub fn ragdoll_joint_count(&self) -> u32 {
286        self.ragdoll_joint_count
287    }
288
289    /// Number of animation skeleton joints.
290    pub fn animation_joint_count(&self) -> u32 {
291        self.animation_joint_count
292    }
293}
294
295/// The animation joint of every ragdoll joint, matched by name, after checking that the
296/// animation skeleton keeps the ragdoll's hierarchy.
297fn match_joints(ragdoll: &Skeleton, animation: &Skeleton) -> Result<Vec<u32>, RagdollError> {
298    let by_name: BTreeMap<&str, u32> = animation
299        .names()
300        .iter()
301        .enumerate()
302        .map(|(index, name)| (name.as_str(), index as u32))
303        .collect();
304    let mapped = ragdoll
305        .names()
306        .iter()
307        .enumerate()
308        .map(|(joint, name)| {
309            by_name
310                .get(name.as_str())
311                .copied()
312                .ok_or(RagdollError::UnmappedJoint(joint as u32))
313        })
314        .collect::<Result<Vec<u32>, _>>()?;
315    let mut is_mapped = vec![false; animation.names().len()];
316    for &joint in &mapped {
317        is_mapped[joint as usize] = true;
318    }
319    for (joint, &animation_joint) in mapped.iter().enumerate() {
320        let expected = ragdoll.parents()[joint].map(|parent| mapped[parent as usize]);
321        if mapped_ancestor(animation.parents(), &is_mapped, animation_joint) != expected {
322            return Err(RagdollError::HierarchyMismatch(joint as u32));
323        }
324    }
325    Ok(mapped)
326}
327
328/// The nearest ancestor of `joint` that is mapped.
329fn mapped_ancestor(parents: &[Option<u32>], is_mapped: &[bool], joint: u32) -> Option<u32> {
330    let mut current = parents[joint as usize];
331    while let Some(ancestor) = current {
332        if is_mapped[ancestor as usize] {
333            return Some(ancestor);
334        }
335        current = parents[ancestor as usize];
336    }
337    None
338}
339
340/// One flag per animation joint, set for `joints`. `mapped_root` is the joint the ragdoll root
341/// maps to.
342fn lock_mask(joints: &[u32], mapped_root: u32, count: usize) -> Result<Vec<bool>, RagdollError> {
343    let mut mask = vec![false; count];
344    for &joint in joints {
345        if joint == 0 || joint == mapped_root || joint as usize >= count {
346            return Err(RagdollError::InvalidValue(LOCK_RULE));
347        }
348        mask[joint as usize] = true;
349    }
350    Ok(mask)
351}
352
353/// `pose`'s joints relative to `root_offset` instead of its own root offset. Both poses were
354/// validated, so every component is a difference of two positions in the frame plus rounding:
355/// at most `2 * limits::MAX_POSITION * (1 + 2^-22)`, finite as `f32` (docs/limits.md, "Skeleton
356/// mapper neutral poses").
357fn reexpress(pose: &SkeletonPose, root_offset: RVec3) -> Vec<JointTransform> {
358    let o = pose.root_offset;
359    // `Real` is `f32` without the `double-precision` feature, so the cast is a no-op there.
360    #[allow(clippy::unnecessary_cast)]
361    let shift = |own: Real, other: Real, t: f32| (own - other + Real::from(t)) as f32;
362    pose.joints
363        .iter()
364        .map(|joint| {
365            let t = joint.translation;
366            JointTransform {
367                translation: Vec3::new(
368                    shift(o.x, root_offset.x, t.x),
369                    shift(o.y, root_offset.y, t.y),
370                    shift(o.z, root_offset.z, t.z),
371                ),
372                rotation: joint.rotation,
373            }
374        })
375        .collect()
376}
377
378fn zero_matrix() -> JPH_Mat4 {
379    let column = JPH_Vec4 {
380        x: 0.0,
381        y: 0.0,
382        z: 0.0,
383        w: 0.0,
384    };
385    JPH_Mat4 {
386        column: [column; 4],
387    }
388}
389
390/// The matrix of a validated joint transform. The rotation is normalised first: a rotation
391/// within Rust's unit tolerance can be just outside Jolt's, which sums the squares in another
392/// order.
393fn matrix(translation: Vec3, rotation: Quat) -> JPH_Mat4 {
394    let mut result = zero_matrix();
395    // SAFETY: joltc only reads the two live locals and writes `result`; the rotation is a unit
396    // quaternion within Jolt's tolerance, as `Mat44::sRotation` asserts.
397    unsafe {
398        JPH_Mat4_RotationTranslation(
399            &mut result,
400            &rotation.normalized().to_jph(),
401            &translation.to_jph(),
402        )
403    };
404    result
405}
406
407fn matrices(joints: &[JointTransform]) -> Vec<JPH_Mat4> {
408    joints
409        .iter()
410        .map(|joint| matrix(joint.translation, joint.rotation))
411        .collect()
412}
413
414/// The pose of `matrices` at `root_offset`, if it is a valid pose.
415fn mapped_pose(root_offset: RVec3, matrices: &[JPH_Mat4]) -> Result<SkeletonPose, RagdollError> {
416    let joints = matrices
417        .iter()
418        .map(|matrix| {
419            let mut translation = Vec3::ZERO.to_jph();
420            let mut rotation = Quat::IDENTITY.to_jph();
421            // SAFETY: joltc copies the matrix and writes the two live locals.
422            unsafe {
423                JPH_Mat4_GetTranslation(matrix, &mut translation);
424                JPH_Mat4_GetQuaternion(matrix, &mut rotation);
425            }
426            JointTransform {
427                translation: Vec3::from_jph(translation),
428                rotation: Quat::from_jph(rotation).normalized(),
429            }
430        })
431        .collect();
432    let pose = SkeletonPose {
433        root_offset,
434        joints,
435    };
436    match pose.validate(matrices.len()) {
437        Ok(()) => Ok(pose),
438        Err(_) if pose.joints.iter().any(|j| !j.rotation.is_valid_rotation()) => {
439            Err(RagdollError::InvalidValue(MAPPED_ROTATION_RULE))
440        }
441        Err(_) => Err(RagdollError::InvalidValue(MAPPED_POSITION_RULE)),
442    }
443}
444
445#[cfg(test)]
446mod tests;