diff --git a/src/animation.rs b/src/animation.rs index c0848f4..bda1d7f 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -55,7 +55,7 @@ pub struct QuaternionKey { // largest: 2 => The largest component of the quaternion. // sign: 1 => The sign of the largest component. 1 for negative. bit_field: u16, - pub value: [i16; 3], // The quantized value of the 3 smallest components. + value: [i16; 3], // The quantized value of the 3 smallest components. } impl QuaternionKey { @@ -67,10 +67,6 @@ impl QuaternionKey { }; } - pub fn ratio(&self) -> f32 { - return self.ratio; - } - pub fn track(&self) -> u16 { return self.bit_field >> 3; } @@ -188,29 +184,45 @@ impl ArchiveReader for QuaternionKey { } } +/// +/// Defines a runtime skeletal animation clip. +/// +/// The runtime animation data structure stores animation keyframes, for all the +/// joints of a skeleton. +/// +/// For each transformation type (translation, rotation and scale), Animation +/// structure stores a single array of keyframes that contains all the tracks +/// required to animate all the joints of a skeleton, matching breadth-first +/// joints order of the runtime skeleton structure. In order to optimize cache +/// coherency when sampling the animation, Keyframes in this array are sorted by +/// time, then by track number. +/// #[derive(Debug)] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] pub struct Animation { - pub(crate) duration: f32, - pub(crate) num_tracks: usize, - pub(crate) name: String, - pub(crate) translations: Vec, - pub(crate) rotations: Vec, - pub(crate) scales: Vec, + duration: f32, + num_tracks: usize, + name: String, + translations: Vec, + rotations: Vec, + scales: Vec, } +/// Defines the version of the `Animation` archive. impl ArchiveVersion for Animation { fn version() -> u32 { return 6; } } +/// Defines the tag of the `Animation` archive. impl ArchiveTag for Animation { fn tag() -> &'static str { return "ozz-animation"; } } +/// Read `Animation` from `IArchive`. impl ArchiveReader for Animation { fn read(archive: &mut IArchive) -> Result { if !archive.test_tag::()? { @@ -249,45 +261,83 @@ impl ArchiveReader for Animation { } impl Animation { + /// Reads an `Animation` from a file. pub fn from_file>(path: P) -> Result { let mut archive = IArchive::new(path)?; return Animation::read(&mut archive); } + /// Reads an `Animation` from a `IArchive`. pub fn from_reader(archive: &mut IArchive) -> Result { return Animation::read(archive); } + + /// Creates a new `Animation` from raw data. + pub fn from_raw( + duration: f32, + num_tracks: usize, + name: String, + translations: Vec, + rotations: Vec, + scales: Vec, + ) -> Animation { + return Animation { + duration, + num_tracks, + name, + translations, + rotations, + scales, + }; + } } impl Animation { + /// Gets the animation clip duration. + #[inline] pub fn duration(&self) -> f32 { return self.duration; } + /// Gets the number of animated tracks. + #[inline] pub fn num_tracks(&self) -> usize { return self.num_tracks; } + /// Gets the number of animated tracks (aligned to 4 * SoA). + #[inline] pub fn num_aligned_tracks(&self) -> usize { return (self.num_tracks + 3) & !0x3; } + /// Gets the number of SoA elements matching the number of tracks of `Animation`. + /// This value is useful to allocate SoA runtime data structures. + #[inline] pub fn num_soa_tracks(&self) -> usize { return (self.num_tracks + 3) / 4; } + /// Gets animation name. + #[inline] pub fn name(&self) -> &str { return &self.name; } + /// Gets the buffer of translations keys. + #[inline] pub fn translations(&self) -> &[Float3Key] { return &self.translations; } + /// Gets the buffer of rotation keys. + #[inline] pub fn rotations(&self) -> &[QuaternionKey] { return &self.rotations; } + /// Gets the buffer of scale keys. + #[inline] pub fn scales(&self) -> &[Float3Key] { return &self.scales; } diff --git a/src/archive.rs b/src/archive.rs index 7c4564e..983103e 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -14,16 +14,21 @@ pub trait ArchiveTag { fn tag() -> &'static str; } +/// Implements `ArchiveReader` to read `T` from IArchive. pub trait ArchiveReader { fn read(archive: &mut IArchive) -> Result; } +/// Implements input archive concept used to load/de-serialize data. +/// Endianness conversions are automatically performed according to the Archive +/// and the native formats. pub struct IArchive { file: File, endian_swap: bool, } impl IArchive { + /// Creates an archive from a file. pub fn new>(path: P) -> Result { let mut file = File::open(path)?; @@ -49,10 +54,13 @@ impl IArchive { return self.read::(); } + /// Reads a value from the archive. pub fn read>(&mut self) -> Result { return T::read(self); } + /// Reads a vector from the archive. + /// * `count` - The number of elements to read. pub fn read_vec>(&mut self, count: usize) -> Result, OzzError> { let mut buffer = Vec::with_capacity(count); for _ in 0..count { @@ -61,6 +69,8 @@ impl IArchive { return Ok(buffer); } + /// Reads a string from the archive. + /// * `count` - The number of characters to read. If 0, the string is null-terminated. pub fn read_string(&mut self, count: usize) -> Result { if count != 0 { let buffer = self.read_vec::(count)?; diff --git a/src/base.rs b/src/base.rs index 7ba83b7..ac10421 100644 --- a/src/base.rs +++ b/src/base.rs @@ -24,7 +24,18 @@ pub enum OzzError { InvalidVersion, } +/// Defines the maximum number of joints. +/// This is limited in order to control the number of bits required to store +/// a joint index. Limiting the number of joints also helps handling worst +/// size cases, like when it is required to allocate an array of joints on +/// the stack. pub const SKELETON_MAX_JOINTS: i32 = 1024; + +/// Defines the maximum number of SoA elements required to store the maximum +/// number of joints. +pub const SKELETON_MAX_SOA_JOINTS: i32 = (SKELETON_MAX_JOINTS + 3) / 4; + +/// Defines the index of the parent of the root joint (which has no parent in fact) pub const SKELETON_NO_PARENT: i32 = -1; pub trait OzzRef diff --git a/src/blending_job.rs b/src/blending_job.rs index 9b44899..a2776a8 100644 --- a/src/blending_job.rs +++ b/src/blending_job.rs @@ -11,10 +11,24 @@ use crate::skeleton::Skeleton; const ZERO: f32x4 = f32x4::from_array([0.0; 4]); const ONE: f32x4 = f32x4::from_array([1.0; 4]); +/// Defines a layer of blending input data (local space transforms) and parameters (weights). #[derive(Debug, Clone)] pub struct BlendingLayer> { + /// Buffer to store local space transforms, that are usually outputted from a `SamplingJob`. pub transform: I, + + /// Blending weight of this layer. Negative values are considered as 0. + /// Normalization is performed during the blending stage so weight can be in + /// any range, even though range 0.0-1.0 is optimal. pub weight: f32, + + /// Optional buffer to store blending weight for each joint in this layer. + /// If both `joint_weights` are empty in `BlendingJob` then per joint weight blending is disabled. + /// When a layer doesn't specifies per joint weights, then it is implicitly considered as being 1.0. + /// This default value is a reference value for the normalization process, which implies that the + /// range of values for joint weights should be 0.0-1.0. + /// Negative weight values are considered as 0, but positive ones aren't clamped because they could + /// exceed 1.0 if all layers contains valid joint weights. pub joint_weights: Vec, } @@ -48,6 +62,20 @@ impl> BlendingLayer { } } +/// +/// `BlendingJob` is in charge of blending (mixing) multiple poses +/// (the result of a sampled animation) according to their respective weight, +/// into one output pose. +/// +/// The number of transforms/joints blended by the job is defined by the number +/// of transforms of the rest pose (note that this is a SoA format). This means +/// that all buffers must be at least as big as the rest pose buffer. +/// +/// Partial animation blending is supported through optional joint weights that +/// can be specified with layers joint_weights buffer. Unspecified joint weights +/// are considered as a unit weight of 1.0, allowing to mix full and partial +/// blend operations in a single pass. +/// #[derive(Debug)] pub struct BlendingJob, I = Rc>>, O = Rc>>> where @@ -97,18 +125,33 @@ where I: OzzBuf, O: OzzBuf, { + /// Gets threshold of `BlendingJob`. pub fn threshold(&self) -> f32 { return self.threshold; } + /// Set threshold of `BlendingJob`. + /// + /// The job blends the rest pose to the output when the accumulated weight of + /// all layers is less than this threshold value. + /// Must be greater than 0.0. pub fn set_threshold(&mut self, threshold: f32) { self.threshold = threshold; } + /// Gets skeleton of `BlendingJob`. pub fn skeleton(&self) -> Option<&S> { return self.skeleton.as_ref(); } + /// Set skeleton of `BlendingJob`. + /// + /// The skeleton that will be used during job. + /// + /// The rest pose size of this skeleton defines the number of transforms to blend. + /// + /// It is used when the accumulated weight for a bone on all layers is + /// less than the threshold value, in order to fall back on valid transforms. pub fn set_skeleton(&mut self, skeleton: S) { self.verified = false; let joint_rest_poses = skeleton.joint_rest_poses().len(); @@ -118,43 +161,58 @@ where self.skeleton = Some(skeleton); } + /// Clears skeleton of `BlendingJob`. pub fn clear_skeleton(&mut self) { self.verified = false; self.skeleton = None; } + /// Gets output of `BlendingJob`. pub fn output(&self) -> Option<&O> { return self.output.as_ref(); } + /// Sets output of `BlendingJob`. + /// + /// The range of output transforms to be filled with blended layer transforms during job execution. pub fn set_output(&mut self, output: O) { self.verified = false; self.output = Some(output); } + /// Clears output of `BlendingJob`. pub fn clear_output(&mut self) { self.verified = false; self.output = None; } + /// Gets layers of `BlendingJob`. pub fn layers(&self) -> &[BlendingLayer] { return &self.layers; } + /// Gets mutable layers of `BlendingJob`. + /// + /// Job input layers, can be empty or nullptr. The range of layers that must be blended. pub fn layers_mut(&mut self) -> &mut Vec> { self.verified = false; // TODO: more efficient way to avoid verification return &mut self.layers; } + /// Gets additive layers of `BlendingJob`. pub fn additive_layers(&self) -> &[BlendingLayer] { return &self.additive_layers; } + /// Gets mutable additive layers of `BlendingJob`. + /// + /// Job input additive layers, can be empty or nullptr. The range of layers that must be added to the output. pub fn additive_layers_mut(&mut self) -> &mut Vec> { self.verified = false; // TODO: more efficient way to avoid verification return &mut self.additive_layers; } + /// Validates `BlendingJob` parameters. pub fn validate(&self) -> bool { let skeleton = match &self.skeleton { Some(skeleton) => skeleton, @@ -199,6 +257,8 @@ where return res; } + /// Runs job's blending task. + /// The job call `validate()` to validate job before any operation is performed. pub fn run(&mut self) -> Result<(), OzzError> { if !self.verified { if !self.validate() { @@ -658,11 +718,11 @@ mod blending_tests { joint_rest_poses[1].rotation = SoaQuat::splat_col([0.0, 0.0, 0.0, 1.0]); joint_rest_poses[1].scale = joint_rest_poses[0].scale.mul_num(f32x4::splat(2.0)); - let skeleton = Rc::new(Skeleton { + let skeleton = Rc::new(Skeleton::from_raw( joint_rest_poses, - joint_parents: vec![0; 8], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + vec![0; 8], + HashMap::with_hasher(DeterministicState::new()), + )); execute_test( &skeleton, @@ -719,11 +779,11 @@ mod blending_tests { ), }, ]; - let skeleton = Rc::new(Skeleton { - joint_rest_poses: rest_poses, - joint_parents: vec![0; 8], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + let skeleton = Rc::new(Skeleton::from_raw( + rest_poses, + vec![0; 8], + HashMap::with_hasher(DeterministicState::new()), + )); { layers[0].weight = -0.07; @@ -818,11 +878,11 @@ mod blending_tests { scale: SoaVec3::new([0.0, 2.0, 4.0, 6.0], [8.0, 10.0, 12.0, 14.0], [16.0, 18.0, 20.0, 22.0]), }, ]; - let skeleton = Rc::new(Skeleton { - joint_rest_poses: rest_poses, - joint_parents: vec![0; 8], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + let skeleton = Rc::new(Skeleton::from_raw( + rest_poses, + vec![0; 8], + HashMap::with_hasher(DeterministicState::new()), + )); { layers[0].weight = 0.5; @@ -881,11 +941,11 @@ mod blending_tests { let mut joint_rest_poses = vec![IDENTITY]; joint_rest_poses[0].scale = SoaVec3::new([0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0], [8.0, 9.0, 10.0, 11.0]); - return Rc::new(Skeleton { + return Rc::new(Skeleton::from_raw( joint_rest_poses, - joint_parents: vec![0; 4], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + vec![0; 4], + HashMap::with_hasher(DeterministicState::new()), + )); } #[test] fn test_normalize() { @@ -1098,11 +1158,11 @@ mod blending_tests { #[test] fn test_additive_weight() { - let skeleton = Rc::new(Skeleton { - joint_rest_poses: vec![IDENTITY; 1], - joint_parents: vec![0; 4], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + let skeleton = Rc::new(Skeleton::from_raw( + vec![IDENTITY; 1], + vec![0; 4], + HashMap::with_hasher(DeterministicState::new()), + )); let mut input1 = vec![IDENTITY; 1]; input1[0].translation = SoaVec3::new([0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0], [8.0, 9.0, 10.0, 11.0]); @@ -1268,11 +1328,11 @@ mod blending_tests { #[test] fn test_additive_joint_weight() { - let skeleton = Rc::new(Skeleton { - joint_rest_poses: vec![IDENTITY; 1], - joint_parents: vec![0; 4], - joint_names: HashMap::with_hasher(DeterministicState::new()), - }); + let skeleton = Rc::new(Skeleton::from_raw( + vec![IDENTITY; 1], + vec![0; 4], + HashMap::with_hasher(DeterministicState::new()), + )); let mut input1 = vec![IDENTITY; 1]; input1[0].translation = SoaVec3::new([0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0], [8.0, 9.0, 10.0, 11.0]); diff --git a/src/local_to_model_job.rs b/src/local_to_model_job.rs index fe01463..ae7a0a3 100644 --- a/src/local_to_model_job.rs +++ b/src/local_to_model_job.rs @@ -184,7 +184,7 @@ where let soa_end = (idx + 4) & !3; while idx < soa_end && process { let parent = skeleton.joint_parent(idx); - if parent == Skeleton::no_parent() { + if parent as i32 == SKELETON_NO_PARENT { output[idx] = AosMat4::mul(&self.root, &aos_matrices[idx & 3]).into(); } else { output[idx] = AosMat4::mul(&output[parent as usize].into(), &aos_matrices[idx & 3]).into(); @@ -270,8 +270,8 @@ mod local_to_model_tests { // j1 j3 // | / \ // j2 j4 j5 - return Rc::new(Skeleton { - joint_rest_poses: vec![ + return Rc::new(Skeleton::from_raw( + vec![ SoaTransform { translation: SoaVec3::splat_col([0.0; 3]), rotation: SoaQuat::splat_col([0.0, 0.0, 0.0, 1.0]), @@ -279,8 +279,8 @@ mod local_to_model_tests { }; 2 ], - joint_parents: vec![-1, 0, 1, 0, 3, 3], - joint_names: (|| { + vec![-1, 0, 1, 0, 3, 3], + (|| { let mut map = HashMap::with_hasher(DeterministicState::new()); map.insert("j0".into(), 0); map.insert("j1".into(), 1); @@ -290,7 +290,7 @@ mod local_to_model_tests { map.insert("j5".into(), 5); return map; })(), - }); + )); } fn new_input1() -> Rc>> { @@ -329,8 +329,8 @@ mod local_to_model_tests { // j2 j4 j6 // | // j5 - return Rc::new(Skeleton { - joint_rest_poses: vec![ + return Rc::new(Skeleton::from_raw( + vec![ SoaTransform { translation: SoaVec3::splat_col([0.0; 3]), rotation: SoaQuat::splat_col([0.0, 0.0, 0.0, 1.0]), @@ -338,8 +338,8 @@ mod local_to_model_tests { }; 2 ], - joint_parents: vec![-1, 0, 1, 0, 3, 4, 3, -1], - joint_names: (|| { + vec![-1, 0, 1, 0, 3, 4, 3, -1], + (|| { let mut map = HashMap::with_hasher(DeterministicState::new()); map.insert("j0".into(), 0); map.insert("j1".into(), 1); @@ -351,7 +351,7 @@ mod local_to_model_tests { map.insert("j7".into(), 7); return map; })(), - }); + )); } fn new_input2() -> Rc>> { diff --git a/src/math.rs b/src/math.rs index 314514a..d9f199b 100644 --- a/src/math.rs +++ b/src/math.rs @@ -703,7 +703,6 @@ impl AosMat4 { // // SoaVec3 // - #[repr(C)] #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct SoaMat4 { diff --git a/src/sampling_job.rs b/src/sampling_job.rs index 7c5375e..0cdb6d1 100644 --- a/src/sampling_job.rs +++ b/src/sampling_job.rs @@ -9,6 +9,7 @@ use crate::animation::{Animation, Float3Key, QuaternionKey}; use crate::base::{OzzBuf, OzzError, OzzRef}; use crate::math::{f32_clamp_or_max, SoaQuat, SoaTransform, SoaVec3}; +/// Soa hot `SoaVec3` data to interpolate. #[repr(C)] #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct InterpSoaFloat3 { @@ -43,6 +44,7 @@ impl rkyv::Deserialize for Inter } } +/// Soa hot `SoaQuat` data to interpolate. #[repr(C)] #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct InterpSoaQuaternion { @@ -135,6 +137,8 @@ impl Default for SamplingContextInner { } } +/// Declares the context object used by the workload to take advantage of the +/// frame coherency of animation sampling. pub struct SamplingContext(*mut SamplingContextInner); impl Debug for SamplingContext { @@ -203,6 +207,9 @@ impl SamplingContext { return unsafe { &mut *self.0 }; } + /// Create a new `SamplingContext` + /// + /// * `max_tracks` - The maximum number of tracks that the context can handle. pub fn new(max_tracks: usize) -> SamplingContext { let max_soa_tracks = (max_tracks + 3) / 4; let max_tracks = max_soa_tracks * 4; @@ -250,12 +257,16 @@ impl SamplingContext { }; } + /// Create a new `SamplingContext` from an `Animation`. + /// + /// * `animation` - The animation to sample. Use `animation.num_tracks()` as max_tracks. pub fn from_animation(animation: &Animation) -> SamplingContext { let mut ctx = SamplingContext::new(animation.num_tracks()); ctx.inner_mut().animation_id = animation as *const _ as u64; return ctx; } + /// Clear the `SamplingContext`. pub fn clear(&mut self) { self.inner_mut().animation_id = 0; self.inner_mut().translation_cursor = 0; @@ -263,36 +274,44 @@ impl SamplingContext { self.inner_mut().scale_cursor = 0; } + /// Clone the `SamplingContext` without the animation id. Usually used for serialization. pub fn clone_without_animation_id(&self) -> SamplingContext { let mut ctx = self.clone(); ctx.set_animation_id(0); return ctx; } + /// The memory size of the context in bytes. pub fn size(&self) -> usize { return self.inner().size; } + /// The maximum number of SoA tracks that the context can handle. pub fn max_soa_tracks(&self) -> usize { return self.inner().max_soa_tracks; } + /// The maximum number of tracks that the context can handle. pub fn max_tracks(&self) -> usize { return self.inner().max_tracks; } + /// The number of tracks that are outdated. pub fn num_outdated(&self) -> usize { return self.inner().num_outdated; } + /// The unique identifier of the animation that the context is sampling. pub fn animation_id(&self) -> u64 { return self.inner().animation_id; } + /// Set the unique identifier of the animation that the context is sampling. pub fn set_animation_id(&mut self, id: u64) { self.inner_mut().animation_id = id; } + /// The current time ratio in the animation. pub fn ratio(&self) -> f32 { return self.inner().ratio; } @@ -301,6 +320,7 @@ impl SamplingContext { self.inner_mut().ratio = ratio; } + /// Soa hot data to interpolate. pub fn translations(&self) -> &[InterpSoaFloat3] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.translations_ptr, inner.max_soa_tracks) }; @@ -311,6 +331,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.translations_ptr, inner.max_soa_tracks) }; } + /// Soa hot data to interpolate. pub fn rotations(&self) -> &[InterpSoaQuaternion] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.rotations_ptr, inner.max_soa_tracks) }; @@ -321,6 +342,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.rotations_ptr, inner.max_soa_tracks) }; } + /// Soa hot data to interpolate. pub fn scales(&self) -> &[InterpSoaFloat3] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.scales_ptr, inner.max_soa_tracks) }; @@ -331,6 +353,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.scales_ptr, inner.max_soa_tracks) }; } + /// The keys in the animation that are valid for the current time ratio. pub fn translation_keys(&self) -> &[i32] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.translation_keys_ptr, inner.max_tracks * 2) }; @@ -341,6 +364,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.translation_keys_ptr, inner.max_tracks * 2) }; } + /// The keys in the animation that are valid for the current time ratio. pub fn rotation_keys(&self) -> &[i32] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.rotation_keys_ptr, inner.max_tracks * 2) }; @@ -351,6 +375,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.rotation_keys_ptr, inner.max_tracks * 2) }; } + /// The keys in the animation that are valid for the current time ratio. pub fn scale_keys(&self) -> &[i32] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.scale_keys_ptr, inner.max_tracks * 2) }; @@ -361,6 +386,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.scale_keys_ptr, inner.max_tracks * 2) }; } + /// Current cursors in the animation. 0 means that the context is invalid. pub fn translation_cursor(&self) -> usize { return self.inner().translation_cursor; } @@ -369,6 +395,7 @@ impl SamplingContext { self.inner_mut().translation_cursor = cursor; } + /// Current cursors in the animation. 0 means that the context is invalid. pub fn rotation_cursor(&self) -> usize { return self.inner().rotation_cursor; } @@ -377,6 +404,7 @@ impl SamplingContext { self.inner_mut().rotation_cursor = cursor; } + /// Current cursors in the animation. 0 means that the context is invalid. pub fn scale_cursor(&self) -> usize { return self.inner().scale_cursor; } @@ -385,6 +413,7 @@ impl SamplingContext { self.inner_mut().scale_cursor = cursor; } + /// Outdated soa entries. One bit per soa entry (32 joints per byte). pub fn outdated_translations(&self) -> &[u8] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.outdated_translations_ptr, inner.num_outdated) }; @@ -395,6 +424,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.outdated_translations_ptr, inner.num_outdated) }; } + /// Outdated soa entries. One bit per soa entry (32 joints per byte). pub fn outdated_rotations(&self) -> &[u8] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.outdated_rotations_ptr, inner.num_outdated) }; @@ -405,6 +435,7 @@ impl SamplingContext { return unsafe { std::slice::from_raw_parts_mut(inner.outdated_rotations_ptr, inner.num_outdated) }; } + /// Outdated soa entries. One bit per soa entry (32 joints per byte). pub fn outdated_scales(&self) -> &[u8] { let inner = self.inner(); return unsafe { std::slice::from_raw_parts(inner.outdated_scales_ptr, inner.num_outdated) }; @@ -655,6 +686,17 @@ impl bytecheck::CheckBytes for ArchivedSamplingContext { } } +/// +/// Samples an animation at a given time ratio in the unit interval 0.0-1.0 (where 0.0 is the beginning of +/// the animation, 1.0 is the end), to output the corresponding posture in local-space. +/// +/// `SamplingJob` uses `SamplingContext` to store intermediate values (decompressed animation keyframes...) +/// while sampling. +/// This context also stores pre-computed values that allows drastic optimization while playing/sampling the +/// animation forward. +/// Backward sampling works, but isn't optimized through the context. The job does not owned the buffers +/// (in/output) and will thus not delete them during job's destruction. +/// #[derive(Debug)] pub struct SamplingJob, O = Rc>>> where @@ -689,56 +731,81 @@ where A: OzzRef, O: OzzBuf, { + /// Gets animation to sample of `SamplingJob`. pub fn animation(&self) -> Option<&A> { return self.animation.as_ref(); } + /// Sets animation to sample of `SamplingJob`. pub fn set_animation(&mut self, animation: A) { self.verified = false; self.animation = Some(animation); } + /// Clears animation to sample of `SamplingJob`. pub fn clear_animation(&mut self) { self.verified = false; self.animation = None; } + /// Gets context of `SamplingJob`. See [SamplingContext]. pub fn context(&self) -> Option<&SamplingContext> { return self.context.as_ref(); } + /// Sets context of `SamplingJob`. See [SamplingContext]. pub fn set_context(&mut self, ctx: SamplingContext) { self.verified = false; self.context = Some(ctx); } + /// Clears context of `SamplingJob`. See [SamplingContext]. pub fn clear_context(&mut self) { self.verified = false; self.context = None; } + /// Gets output of `SamplingJob`. pub fn output(&self) -> Option<&O> { return self.output.as_ref(); } + /// Sets output of `SamplingJob`. + /// + /// The output range to be filled with sampled joints during job execution. + /// + /// If there are less joints in the animation compared to the output range, then remaining + /// `SoaTransform` are left unchanged. + /// If there are more joints in the animation, then the last joints are not sampled. pub fn set_output(&mut self, output: O) { self.verified = false; self.output = Some(output); } + /// Clears output of `SamplingJob`. pub fn clear_output(&mut self) { self.verified = false; self.output = None; } + /// Gets the time ratio of `SamplingJob`. pub fn ratio(&self) -> f32 { return self.ratio; } + /// Sets the time ratio of `SamplingJob`. + /// + /// Time ratio in the unit interval 0.0-1.0 used to sample animation (where 0 is the beginning of + /// the animation, 1 is the end). It should be computed as the current time in the animation, + /// divided by animation duration. + /// + /// This ratio is clamped before job execution in order to resolves any approximation issue on range + /// bounds. pub fn set_ratio(&mut self, ratio: f32) { self.ratio = f32_clamp_or_max(ratio, 0.0f32, 1.0f32); } + /// Validates `SamplingJob` parameters. pub fn validate(&self) -> bool { let animation = match &self.animation { Some(animation) => animation, @@ -767,6 +834,8 @@ where return true; } + /// Runs job's sampling task. + /// The job call `validate()` to validate job before any operation is performed. pub fn run(&mut self) -> Result<(), OzzError> { if !self.verified { if !self.validate() { @@ -1160,14 +1229,14 @@ mod sampling_tests { scales: Vec, frames: Vec>, ) { - let animation = Rc::new(Animation { + let animation = Rc::new(Animation::from_raw( duration, - num_tracks: T, - name: String::new(), + T, + String::new(), translations, rotations, scales, - }); + )); let mut job = SamplingJob::default(); job.set_animation(animation); job.set_context(SamplingContext::new(T)); @@ -1410,23 +1479,23 @@ mod sampling_tests { translations[0] = Float3Key::new(0.0, 0, [f16(1.0), f16(-1.0), f16(5.0)]); translations[4] = Float3Key::new(1.0, 0, [f16(1.0), f16(-1.0), f16(5.0)]); - let animation1 = Rc::new(Animation { - duration: 46.0, - num_tracks: 1, - name: String::new(), - translations: translations.clone(), - rotations: new_rotations(), - scales: new_scales(), - }); - - let animation2 = Rc::new(Animation { - duration: 46.0, - num_tracks: 1, - name: String::new(), - translations: translations.clone(), - rotations: new_rotations(), - scales: new_scales(), - }); + let animation1 = Rc::new(Animation::from_raw( + 46.0, + 1, + String::new(), + translations.clone(), + new_rotations(), + new_scales(), + )); + + let animation2 = Rc::new(Animation::from_raw( + 46.0, + 1, + String::new(), + translations.clone(), + new_rotations(), + new_scales(), + )); let mut job = SamplingJob::default(); job.set_animation(animation1.clone()); diff --git a/src/skeleton.rs b/src/skeleton.rs index 71ece59..f1ad4dc 100644 --- a/src/skeleton.rs +++ b/src/skeleton.rs @@ -5,26 +5,40 @@ use crate::archive::{ArchiveReader, ArchiveTag, ArchiveVersion, IArchive}; use crate::math::SoaTransform; use crate::{DeterministicState, OzzError}; +/// +/// This runtime skeleton data structure provides a const-only access to joint +/// hierarchy, joint names and rest-pose. +/// +/// Joint names, rest-poses and hierarchy information are all stored in separate +/// arrays of data (as opposed to joint structures for the RawSkeleton), in order +/// to closely match with the way runtime algorithms use them. Joint hierarchy is +/// packed as an array of parent jont indices (16 bits), stored in depth-first +/// order. This is enough to traverse the whole joint hierarchy. Use +/// iter_depth_first() to implement a depth-first traversal utility. +/// #[derive(Debug)] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] pub struct Skeleton { - pub(crate) joint_rest_poses: Vec, - pub(crate) joint_parents: Vec, - pub(crate) joint_names: HashMap, + joint_rest_poses: Vec, + joint_parents: Vec, + joint_names: HashMap, } +/// Defines the version of the `Skeleton` archive. impl ArchiveVersion for Skeleton { fn version() -> u32 { return 2; } } +/// Defines the tag of the `Skeleton` archive. impl ArchiveTag for Skeleton { fn tag() -> &'static str { return "ozz-skeleton"; } } +/// Reads `Skeleton` from `IArchive`. impl ArchiveReader for Skeleton { fn read(archive: &mut IArchive) -> Result { if !archive.test_tag::()? { @@ -68,73 +82,95 @@ impl ArchiveReader for Skeleton { } impl Skeleton { + /// Reads a `Skeleton` from a file. pub fn from_file>(path: P) -> Result { let mut archive = IArchive::new(path)?; return Skeleton::read(&mut archive); } + /// Reads a `Skeleton` from a reader. pub fn from_reader(reader: &mut IArchive) -> Result { return Skeleton::read(reader); } -} - -impl Skeleton { - #[inline(always)] - pub fn max_joints() -> usize { - return 1024; - } - #[inline(always)] - pub fn max_soa_joints() -> usize { - return (Self::max_joints() + 3) / 4; - } - - #[inline(always)] - pub fn no_parent() -> i16 { - return -1; + /// Creates a `Skeleton` from raw data. + pub fn from_raw( + joint_rest_poses: Vec, + joint_parents: Vec, + joint_names: HashMap, + ) -> Skeleton { + return Skeleton { + joint_rest_poses, + joint_parents, + joint_names, + }; } +} +impl Skeleton { + /// Gets the number of joints of `Skeleton`. + #[inline] pub fn num_joints(&self) -> usize { return self.joint_parents.len(); } + /// Gets the number of joints of `Skeleton` (aligned to 4 * SoA). + #[inline] pub fn num_aligned_joints(&self) -> usize { return (self.num_joints() + 3) & !0x3; } + /// Gets the number of soa elements matching the number of joints of `Skeleton`. + /// This value is useful to allocate SoA runtime data structures. + #[inline] pub fn num_soa_joints(&self) -> usize { return (self.joint_parents.len() + 3) / 4; } + /// Gets joint's rest poses. Rest poses are stored in soa format. + #[inline] pub fn joint_rest_poses(&self) -> &[SoaTransform] { return &self.joint_rest_poses; } + /// Gets joint's parent indices range. + #[inline] pub fn joint_parents(&self) -> &[i16] { return &self.joint_parents; } + /// Gets joint's parent by index. + #[inline] pub fn joint_parent(&self, idx: usize) -> i16 { return self.joint_parents[idx]; } + /// Gets joint's name map. + #[inline] pub fn joint_names(&self) -> &HashMap { return &self.joint_names; } + /// Gets joint's index by name. + #[inline] pub fn joint_by_name(&self, name: &str) -> Option { return self.joint_names.get(name).map(|idx| *idx); } - pub fn index_joint(&self, idx: i16) -> Option<&SoaTransform> { - return self.joint_rest_poses.get(idx as usize); - } - + /// Test if a joint is a leaf. + /// + /// * `joint` - `joint` must be in range [0, num joints]. + /// Joint is a leaf if it's the last joint, or next joint's parent isn't `joint`. + #[inline] pub fn is_leaf(&self, joint: i16) -> bool { let next = (joint + 1) as usize; return next == self.num_joints() || self.joint_parents()[next] != joint; } + /// Iterates through the joint hierarchy in depth-first order. + /// + /// * `from` - The joint index to start from. If negative, the iteration starts from the root. + /// * `f` - The function to call for each joint. The function takes arguments `(joint: i16, parent: i16)`. pub fn iter_depth_first(&self, from: i16, mut f: F) where F: FnMut(i16, i16), @@ -148,6 +184,9 @@ impl Skeleton { } } + /// Iterates through the joint hierarchy in reverse depth-first order. + /// + /// * `f` - The function to call for each joint. The function takes arguments `(joint: i16, parent: i16)`. pub fn iter_depth_first_reverse(&self, mut f: F) where F: FnMut(i16, i16),