wow_m2/
model_animation_resolver.rs

1/// Animation data resolver for M2 models
2/// This module provides functionality to extract actual animation keyframe data
3/// from M2 bone tracks, including bind pose transformations.
4use crate::{
5    M2Model, Result,
6    chunks::{
7        bone::M2Bone,
8        m2_track_resolver::{M2TrackQuatExt, M2TrackVec3Ext},
9    },
10    common::C3Vector,
11};
12use std::io::{Cursor, Read, Seek};
13
14/// Resolved bone animation data with actual keyframe values
15#[derive(Debug, Clone)]
16pub struct ResolvedBoneAnimation {
17    /// Bone ID
18    pub bone_id: i32,
19    /// Parent bone ID  
20    pub parent_bone: i16,
21    /// Bone pivot point
22    pub pivot: C3Vector,
23    /// Translation keyframes (timestamps, values)
24    pub translation: Option<(Vec<u32>, Vec<C3Vector>)>,
25    /// Rotation keyframes (timestamps, quaternions as compressed format)
26    pub rotation: Option<(Vec<u32>, Vec<[i16; 4]>)>,
27    /// Scale keyframes (timestamps, values)
28    pub scale: Option<(Vec<u32>, Vec<C3Vector>)>,
29    /// Bind pose translation (first keyframe or default)
30    pub bind_pose_translation: C3Vector,
31    /// Bind pose rotation (first keyframe or identity)
32    pub bind_pose_rotation: [f32; 4],
33    /// Bind pose scale (first keyframe or 1,1,1)
34    pub bind_pose_scale: C3Vector,
35}
36
37impl ResolvedBoneAnimation {
38    /// Extract bind pose from the first keyframe or use defaults
39    pub fn from_bone<R: Read + Seek>(bone: &M2Bone, reader: &mut R) -> Result<Self> {
40        // Try to resolve translation track
41        let (translation, bind_pose_translation) = if bone.translation.has_data() {
42            let (timestamps, values, _ranges) = bone.translation.resolve_data(reader)?;
43            let bind_trans = if !values.is_empty() {
44                values[0]
45            } else {
46                C3Vector {
47                    x: 0.0,
48                    y: 0.0,
49                    z: 0.0,
50                }
51            };
52            (Some((timestamps, values)), bind_trans)
53        } else {
54            (
55                None,
56                C3Vector {
57                    x: 0.0,
58                    y: 0.0,
59                    z: 0.0,
60                },
61            )
62        };
63
64        // Try to resolve rotation track
65        let (rotation, bind_pose_rotation) = if bone.rotation.has_data() {
66            let (timestamps, values, _ranges) = bone.rotation.resolve_data(reader)?;
67
68            // Convert M2CompQuat to simple i16 arrays for storage
69            let quat_values: Vec<[i16; 4]> = values.iter().map(|q| [q.x, q.y, q.z, q.w]).collect();
70
71            let bind_rot = if !values.is_empty() {
72                // Convert compressed quaternion to float quaternion for bind pose
73                let q = &values[0];
74                let quat = [
75                    q.x as f32 / 32767.0,
76                    q.y as f32 / 32767.0,
77                    q.z as f32 / 32767.0,
78                    q.w as f32 / 32767.0,
79                ];
80
81                // Check if quaternion is all zeros (invalid) and use identity instead
82                if quat == [0.0, 0.0, 0.0, 0.0] {
83                    [0.0, 0.0, 0.0, 1.0] // Identity quaternion
84                } else {
85                    quat
86                }
87            } else {
88                [0.0, 0.0, 0.0, 1.0] // Identity quaternion
89            };
90
91            (Some((timestamps, quat_values)), bind_rot)
92        } else {
93            (None, [0.0, 0.0, 0.0, 1.0])
94        };
95
96        // Try to resolve scale track
97        let (scale, bind_pose_scale) = if bone.scale.has_data() {
98            let (timestamps, values, _ranges) = bone.scale.resolve_data(reader)?;
99            let bind_scale = if !values.is_empty() {
100                values[0]
101            } else {
102                C3Vector {
103                    x: 1.0,
104                    y: 1.0,
105                    z: 1.0,
106                }
107            };
108            (Some((timestamps, values)), bind_scale)
109        } else {
110            (
111                None,
112                C3Vector {
113                    x: 1.0,
114                    y: 1.0,
115                    z: 1.0,
116                },
117            )
118        };
119
120        Ok(ResolvedBoneAnimation {
121            bone_id: bone.bone_id,
122            parent_bone: bone.parent_bone,
123            pivot: bone.pivot,
124            translation,
125            rotation,
126            scale,
127            bind_pose_translation,
128            bind_pose_rotation,
129            bind_pose_scale,
130        })
131    }
132
133    /// Check if this bone has any animation data
134    pub fn has_animation(&self) -> bool {
135        self.translation.is_some() || self.rotation.is_some() || self.scale.is_some()
136    }
137
138    /// Get the number of translation keyframes
139    pub fn translation_keyframe_count(&self) -> usize {
140        self.translation.as_ref().map(|(t, _)| t.len()).unwrap_or(0)
141    }
142
143    /// Get the number of rotation keyframes
144    pub fn rotation_keyframe_count(&self) -> usize {
145        self.rotation.as_ref().map(|(t, _)| t.len()).unwrap_or(0)
146    }
147
148    /// Get the number of scale keyframes
149    pub fn scale_keyframe_count(&self) -> usize {
150        self.scale.as_ref().map(|(t, _)| t.len()).unwrap_or(0)
151    }
152}
153
154/// Extension trait for M2Model to resolve animation data
155pub trait M2ModelAnimationExt {
156    /// Resolve all bone animation data from the model
157    fn resolve_bone_animations(&self, data: &[u8]) -> Result<Vec<ResolvedBoneAnimation>>;
158
159    /// Get bind pose for all bones (no animation, just rest position)
160    fn get_bind_pose(&self, data: &[u8]) -> Result<Vec<ResolvedBoneAnimation>>;
161}
162
163impl M2ModelAnimationExt for M2Model {
164    fn resolve_bone_animations(&self, data: &[u8]) -> Result<Vec<ResolvedBoneAnimation>> {
165        let mut cursor = Cursor::new(data);
166        let mut resolved = Vec::with_capacity(self.bones.len());
167
168        for bone in &self.bones {
169            resolved.push(ResolvedBoneAnimation::from_bone(bone, &mut cursor)?);
170        }
171
172        Ok(resolved)
173    }
174
175    fn get_bind_pose(&self, data: &[u8]) -> Result<Vec<ResolvedBoneAnimation>> {
176        // Same as resolve_bone_animations but we already extract bind pose
177        self.resolve_bone_animations(data)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::chunks::bone::M2BoneFlags;
185    use crate::chunks::m2_track::{M2TrackQuat, M2TrackVec3};
186
187    #[test]
188    fn test_bind_pose_defaults() {
189        let bone = M2Bone {
190            bone_id: 0,
191            flags: M2BoneFlags::TRANSFORMED,
192            parent_bone: -1,
193            submesh_id: 0,
194            unknown: [0, 0],
195            bone_name_crc: None,
196            translation: M2TrackVec3 {
197                base: crate::chunks::m2_track::M2TrackBase {
198                    interpolation_type: crate::chunks::animation::M2InterpolationType::None,
199                    global_sequence: 0xFFFF,
200                },
201                ranges: None,
202                timestamps: crate::common::M2Array::new(0, 0),
203                values: crate::common::M2Array::new(0, 0),
204            },
205            rotation: M2TrackQuat {
206                base: crate::chunks::m2_track::M2TrackBase {
207                    interpolation_type: crate::chunks::animation::M2InterpolationType::None,
208                    global_sequence: 0xFFFF,
209                },
210                ranges: None,
211                timestamps: crate::common::M2Array::new(0, 0),
212                values: crate::common::M2Array::new(0, 0),
213            },
214            scale: M2TrackVec3 {
215                base: crate::chunks::m2_track::M2TrackBase {
216                    interpolation_type: crate::chunks::animation::M2InterpolationType::None,
217                    global_sequence: 0xFFFF,
218                },
219                ranges: None,
220                timestamps: crate::common::M2Array::new(0, 0),
221                values: crate::common::M2Array::new(0, 0),
222            },
223            pivot: C3Vector {
224                x: 1.0,
225                y: 2.0,
226                z: 3.0,
227            },
228        };
229
230        let data = vec![0u8; 1000];
231        let mut cursor = Cursor::new(&data);
232
233        let resolved = ResolvedBoneAnimation::from_bone(&bone, &mut cursor).unwrap();
234
235        // Check defaults
236        assert_eq!(
237            resolved.bind_pose_translation,
238            C3Vector {
239                x: 0.0,
240                y: 0.0,
241                z: 0.0
242            }
243        );
244        assert_eq!(resolved.bind_pose_rotation, [0.0, 0.0, 0.0, 1.0]);
245        assert_eq!(
246            resolved.bind_pose_scale,
247            C3Vector {
248                x: 1.0,
249                y: 1.0,
250                z: 1.0
251            }
252        );
253        assert_eq!(
254            resolved.pivot,
255            C3Vector {
256                x: 1.0,
257                y: 2.0,
258                z: 3.0
259            }
260        );
261        assert!(!resolved.has_animation());
262    }
263}