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;