wow_m2/animation/
bone_transform.rs

1//! Bone hierarchy transform computation for M2 skeletal animation
2//!
3//! This module computes bone transformation matrices from interpolated
4//! animation values, handling parent chain traversal, pivot points,
5//! and billboard bone modes.
6
7use super::types::{Quat, Vec3};
8
9/// 4x4 transformation matrix (column-major, like OpenGL/WebGL)
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Mat4 {
12    /// Matrix data in column-major order
13    pub data: [f32; 16],
14}
15
16impl Mat4 {
17    /// Identity matrix
18    pub const IDENTITY: Self = Self {
19        data: [
20            1.0, 0.0, 0.0, 0.0, // Column 0
21            0.0, 1.0, 0.0, 0.0, // Column 1
22            0.0, 0.0, 1.0, 0.0, // Column 2
23            0.0, 0.0, 0.0, 1.0, // Column 3
24        ],
25    };
26
27    /// Create identity matrix
28    pub fn identity() -> Self {
29        Self::IDENTITY
30    }
31
32    /// Create translation matrix
33    pub fn from_translation(v: Vec3) -> Self {
34        Self {
35            data: [
36                1.0, 0.0, 0.0, 0.0, // Column 0
37                0.0, 1.0, 0.0, 0.0, // Column 1
38                0.0, 0.0, 1.0, 0.0, // Column 2
39                v.x, v.y, v.z, 1.0, // Column 3
40            ],
41        }
42    }
43
44    /// Create scale matrix
45    pub fn from_scale(v: Vec3) -> Self {
46        Self {
47            data: [
48                v.x, 0.0, 0.0, 0.0, // Column 0
49                0.0, v.y, 0.0, 0.0, // Column 1
50                0.0, 0.0, v.z, 0.0, // Column 2
51                0.0, 0.0, 0.0, 1.0, // Column 3
52            ],
53        }
54    }
55
56    /// Create rotation matrix from quaternion
57    pub fn from_rotation(q: Quat) -> Self {
58        let x = q.x;
59        let y = q.y;
60        let z = q.z;
61        let w = q.w;
62
63        let x2 = x + x;
64        let y2 = y + y;
65        let z2 = z + z;
66
67        let xx = x * x2;
68        let xy = x * y2;
69        let xz = x * z2;
70        let yy = y * y2;
71        let yz = y * z2;
72        let zz = z * z2;
73        let wx = w * x2;
74        let wy = w * y2;
75        let wz = w * z2;
76
77        Self {
78            data: [
79                1.0 - (yy + zz),
80                xy + wz,
81                xz - wy,
82                0.0,
83                xy - wz,
84                1.0 - (xx + zz),
85                yz + wx,
86                0.0,
87                xz + wy,
88                yz - wx,
89                1.0 - (xx + yy),
90                0.0,
91                0.0,
92                0.0,
93                0.0,
94                1.0,
95            ],
96        }
97    }
98
99    /// Create transformation matrix from rotation, translation, and scale
100    pub fn from_rotation_translation_scale(rotation: Quat, translation: Vec3, scale: Vec3) -> Self {
101        let x = rotation.x;
102        let y = rotation.y;
103        let z = rotation.z;
104        let w = rotation.w;
105
106        let x2 = x + x;
107        let y2 = y + y;
108        let z2 = z + z;
109
110        let xx = x * x2;
111        let xy = x * y2;
112        let xz = x * z2;
113        let yy = y * y2;
114        let yz = y * z2;
115        let zz = z * z2;
116        let wx = w * x2;
117        let wy = w * y2;
118        let wz = w * z2;
119
120        let sx = scale.x;
121        let sy = scale.y;
122        let sz = scale.z;
123
124        Self {
125            data: [
126                (1.0 - (yy + zz)) * sx,
127                (xy + wz) * sx,
128                (xz - wy) * sx,
129                0.0,
130                (xy - wz) * sy,
131                (1.0 - (xx + zz)) * sy,
132                (yz + wx) * sy,
133                0.0,
134                (xz + wy) * sz,
135                (yz - wx) * sz,
136                (1.0 - (xx + yy)) * sz,
137                0.0,
138                translation.x,
139                translation.y,
140                translation.z,
141                1.0,
142            ],
143        }
144    }
145
146    /// Multiply two matrices (self * other)
147    pub fn mul(&self, other: &Self) -> Self {
148        let a = &self.data;
149        let b = &other.data;
150
151        let a00 = a[0];
152        let a01 = a[1];
153        let a02 = a[2];
154        let a03 = a[3];
155        let a10 = a[4];
156        let a11 = a[5];
157        let a12 = a[6];
158        let a13 = a[7];
159        let a20 = a[8];
160        let a21 = a[9];
161        let a22 = a[10];
162        let a23 = a[11];
163        let a30 = a[12];
164        let a31 = a[13];
165        let a32 = a[14];
166        let a33 = a[15];
167
168        let b00 = b[0];
169        let b01 = b[1];
170        let b02 = b[2];
171        let b03 = b[3];
172        let b10 = b[4];
173        let b11 = b[5];
174        let b12 = b[6];
175        let b13 = b[7];
176        let b20 = b[8];
177        let b21 = b[9];
178        let b22 = b[10];
179        let b23 = b[11];
180        let b30 = b[12];
181        let b31 = b[13];
182        let b32 = b[14];
183        let b33 = b[15];
184
185        Self {
186            data: [
187                b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
188                b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
189                b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
190                b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
191                b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
192                b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
193                b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
194                b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
195                b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
196                b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
197                b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
198                b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
199                b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
200                b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
201                b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
202                b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
203            ],
204        }
205    }
206
207    /// Transform a point by this matrix
208    pub fn transform_point(&self, p: Vec3) -> Vec3 {
209        let m = &self.data;
210        Vec3 {
211            x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
212            y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
213            z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14],
214        }
215    }
216
217    /// Transform a normal (direction) by this matrix (ignores translation)
218    pub fn transform_normal(&self, n: Vec3) -> Vec3 {
219        let m = &self.data;
220        Vec3 {
221            x: m[0] * n.x + m[4] * n.y + m[8] * n.z,
222            y: m[1] * n.x + m[5] * n.y + m[9] * n.z,
223            z: m[2] * n.x + m[6] * n.y + m[10] * n.z,
224        }
225    }
226
227    /// Get matrix as flat array for GPU upload
228    pub fn as_array(&self) -> &[f32; 16] {
229        &self.data
230    }
231
232    /// Get matrix as 4x3 for GPU upload (strips last row, common for skinning)
233    pub fn as_4x3(&self) -> [f32; 12] {
234        [
235            self.data[0],
236            self.data[1],
237            self.data[2],
238            self.data[4],
239            self.data[5],
240            self.data[6],
241            self.data[8],
242            self.data[9],
243            self.data[10],
244            self.data[12],
245            self.data[13],
246            self.data[14],
247        ]
248    }
249}
250
251impl Default for Mat4 {
252    fn default() -> Self {
253        Self::IDENTITY
254    }
255}
256
257impl std::ops::Mul for Mat4 {
258    type Output = Mat4;
259
260    fn mul(self, rhs: Self) -> Self::Output {
261        Mat4::mul(&self, &rhs)
262    }
263}
264
265/// Bone flags for transform computation
266#[derive(Debug, Clone, Copy, Default)]
267pub struct BoneFlags {
268    /// Don't inherit parent translation
269    pub ignore_parent_translate: bool,
270    /// Don't inherit parent scale
271    pub ignore_parent_scale: bool,
272    /// Don't inherit parent rotation
273    pub ignore_parent_rotation: bool,
274    /// Spherical billboard (always faces camera)
275    pub spherical_billboard: bool,
276    /// Cylindrical billboard locked to X axis
277    pub cylindrical_billboard_lock_x: bool,
278    /// Cylindrical billboard locked to Y axis
279    pub cylindrical_billboard_lock_y: bool,
280    /// Cylindrical billboard locked to Z axis
281    pub cylindrical_billboard_lock_z: bool,
282}
283
284impl BoneFlags {
285    /// Parse bone flags from M2 bone flags value
286    pub fn from_raw(flags: u32) -> Self {
287        Self {
288            ignore_parent_translate: (flags & 0x01) != 0,
289            ignore_parent_scale: (flags & 0x02) != 0,
290            ignore_parent_rotation: (flags & 0x04) != 0,
291            spherical_billboard: (flags & 0x08) != 0,
292            cylindrical_billboard_lock_x: (flags & 0x10) != 0,
293            cylindrical_billboard_lock_y: (flags & 0x20) != 0,
294            cylindrical_billboard_lock_z: (flags & 0x40) != 0,
295        }
296    }
297
298    /// Check if this bone uses any billboard mode
299    pub fn is_billboard(&self) -> bool {
300        self.spherical_billboard
301            || self.cylindrical_billboard_lock_x
302            || self.cylindrical_billboard_lock_y
303            || self.cylindrical_billboard_lock_z
304    }
305}
306
307/// Computed bone data for rendering
308#[derive(Debug, Clone)]
309pub struct ComputedBone {
310    /// Pre-billboard transform (position in world space)
311    pub transform: Mat4,
312    /// Post-billboard transform (includes rotation/scale)
313    pub post_billboard_transform: Mat4,
314    /// Whether this bone uses spherical billboard
315    pub is_spherical_billboard: bool,
316}
317
318impl Default for ComputedBone {
319    fn default() -> Self {
320        Self {
321            transform: Mat4::IDENTITY,
322            post_billboard_transform: Mat4::IDENTITY,
323            is_spherical_billboard: false,
324        }
325    }
326}
327
328/// Bone transform computer for hierarchical bone matrices
329///
330/// Handles the transformation chain from local bone space to model space,
331/// accounting for parent bones, pivot points, and billboard modes.
332#[allow(dead_code)]
333pub struct BoneTransformComputer {
334    /// Computed bone transforms
335    bones: Vec<ComputedBone>,
336    /// Pivot matrices (translation by pivot point)
337    pivots: Vec<Mat4>,
338    /// Anti-pivot matrices (translation by -pivot point)
339    anti_pivots: Vec<Mat4>,
340    /// Parent bone indices (-1 for root bones)
341    parents: Vec<i16>,
342    /// Bone flags
343    flags: Vec<BoneFlags>,
344    /// Scratch matrix for computation
345    scratch: Mat4,
346}
347
348impl BoneTransformComputer {
349    /// Create a new bone transform computer
350    ///
351    /// # Arguments
352    /// * `pivots` - Pivot points for each bone
353    /// * `parents` - Parent bone index for each bone (-1 for root)
354    /// * `flags` - Bone flags for each bone
355    pub fn new(pivot_points: &[Vec3], parents: &[i16], raw_flags: &[u32]) -> Self {
356        let count = pivot_points.len();
357
358        let mut pivots = Vec::with_capacity(count);
359        let mut anti_pivots = Vec::with_capacity(count);
360        let mut flags = Vec::with_capacity(count);
361        let mut bones = Vec::with_capacity(count);
362
363        // Pre-compute pivot matrices and propagate billboard flags
364        let mut is_billboard = vec![false; count];
365
366        for i in 0..count {
367            let pivot = pivot_points[i];
368            pivots.push(Mat4::from_translation(pivot));
369            anti_pivots.push(Mat4::from_translation(Vec3::new(
370                -pivot.x, -pivot.y, -pivot.z,
371            )));
372
373            let bone_flags = BoneFlags::from_raw(raw_flags[i]);
374            is_billboard[i] = bone_flags.spherical_billboard;
375            flags.push(bone_flags);
376            bones.push(ComputedBone::default());
377        }
378
379        // Propagate spherical billboard flag from parents (children inherit)
380        for i in 0..count {
381            let parent_idx = parents[i];
382            if parent_idx >= 0 && (parent_idx as usize) < count && is_billboard[parent_idx as usize]
383            {
384                is_billboard[i] = true;
385            }
386        }
387
388        // Set final billboard flags
389        for i in 0..count {
390            bones[i].is_spherical_billboard = is_billboard[i];
391        }
392
393        Self {
394            bones,
395            pivots,
396            anti_pivots,
397            parents: parents.to_vec(),
398            flags,
399            scratch: Mat4::IDENTITY,
400        }
401    }
402
403    /// Create an empty computer (no bones)
404    pub fn empty() -> Self {
405        Self {
406            bones: Vec::new(),
407            pivots: Vec::new(),
408            anti_pivots: Vec::new(),
409            parents: Vec::new(),
410            flags: Vec::new(),
411            scratch: Mat4::IDENTITY,
412        }
413    }
414
415    /// Get number of bones
416    pub fn bone_count(&self) -> usize {
417        self.bones.len()
418    }
419
420    /// Update all bone transforms from interpolated animation values
421    ///
422    /// # Arguments
423    /// * `translations` - Interpolated translation for each bone
424    /// * `rotations` - Interpolated rotation for each bone
425    /// * `scales` - Interpolated scale for each bone
426    pub fn update(&mut self, translations: &[Vec3], rotations: &[Quat], scales: &[Vec3]) {
427        let count = self.bones.len();
428        if count == 0 {
429            return;
430        }
431
432        for i in 0..count {
433            let translation = translations.get(i).copied().unwrap_or(Vec3::ZERO);
434            let rotation = rotations.get(i).copied().unwrap_or(Quat::IDENTITY);
435            let scale = scales.get(i).copied().unwrap_or(Vec3::ONE);
436
437            // Build local transform: rotation * translation * scale
438            self.scratch = Mat4::from_rotation_translation_scale(rotation, translation, scale);
439
440            // Apply pivot: pivot * localTransform
441            let local_bone_transform = self.pivots[i].mul(&self.scratch);
442
443            let parent_idx = self.parents[i];
444            let is_billboard = self.bones[i].is_spherical_billboard;
445
446            if parent_idx >= 0 && (parent_idx as usize) < count {
447                let parent_idx = parent_idx as usize;
448                // Clone parent transforms to avoid borrow issues
449                let parent_transform = self.bones[parent_idx].transform;
450                let parent_post_billboard = self.bones[parent_idx].post_billboard_transform;
451
452                if is_billboard {
453                    // Billboard bones: transform is parent * antiPivot
454                    // postBillboard accumulates the local transform
455                    self.bones[i].transform = parent_transform.mul(&self.anti_pivots[i]);
456                    self.bones[i].post_billboard_transform =
457                        parent_post_billboard.mul(&local_bone_transform);
458                } else {
459                    // Regular bones: apply antiPivot to local transform
460                    let final_local = local_bone_transform.mul(&self.anti_pivots[i]);
461                    self.bones[i].post_billboard_transform =
462                        parent_post_billboard.mul(&final_local);
463                    // For non-billboard, transform is same as post_billboard
464                    self.bones[i].transform = self.bones[i].post_billboard_transform;
465                }
466            } else {
467                // Root bone (no parent)
468                if is_billboard {
469                    self.bones[i].transform = self.anti_pivots[i];
470                    self.bones[i].post_billboard_transform = local_bone_transform;
471                } else {
472                    let final_local = local_bone_transform.mul(&self.anti_pivots[i]);
473                    self.bones[i].post_billboard_transform = final_local;
474                    self.bones[i].transform = final_local;
475                }
476            }
477        }
478    }
479
480    /// Get computed bone data
481    pub fn bones(&self) -> &[ComputedBone] {
482        &self.bones
483    }
484
485    /// Get transform for a specific bone
486    pub fn get_transform(&self, bone_index: usize) -> Mat4 {
487        self.bones
488            .get(bone_index)
489            .map(|b| b.transform)
490            .unwrap_or(Mat4::IDENTITY)
491    }
492
493    /// Get post-billboard transform for a specific bone
494    pub fn get_post_billboard_transform(&self, bone_index: usize) -> Mat4 {
495        self.bones
496            .get(bone_index)
497            .map(|b| b.post_billboard_transform)
498            .unwrap_or(Mat4::IDENTITY)
499    }
500
501    /// Get combined transform for skinning (uses post_billboard for most bones)
502    ///
503    /// For regular bones, this returns post_billboard_transform.
504    /// For billboard bones, the shader combines transform and post_billboard
505    /// with the camera rotation.
506    pub fn get_skinning_transform(&self, bone_index: usize) -> Mat4 {
507        self.bones
508            .get(bone_index)
509            .map(|b| {
510                if b.is_spherical_billboard {
511                    // Billboard bones need special handling in shader
512                    // Return identity here; shader will use both matrices
513                    b.post_billboard_transform
514                } else {
515                    b.post_billboard_transform
516                }
517            })
518            .unwrap_or(Mat4::IDENTITY)
519    }
520
521    /// Check if a bone uses spherical billboard
522    pub fn is_spherical_billboard(&self, bone_index: usize) -> bool {
523        self.bones
524            .get(bone_index)
525            .is_some_and(|b| b.is_spherical_billboard)
526    }
527
528    /// Get all bone matrices as flat array for GPU upload
529    ///
530    /// Each bone contributes 2 mat4x3 matrices (24 floats):
531    /// - transform (for billboard positioning)
532    /// - post_billboard_transform (for final skinning)
533    pub fn get_gpu_data(&self) -> Vec<f32> {
534        let mut data = Vec::with_capacity(self.bones.len() * 28); // 24 floats + 4 for flags
535
536        for bone in &self.bones {
537            // Pack billboard flag as first element
538            data.push(if bone.is_spherical_billboard {
539                1.0
540            } else {
541                0.0
542            });
543            // Padding for alignment
544            data.push(0.0);
545            data.push(0.0);
546            data.push(0.0);
547
548            // Transform matrix (4x3)
549            data.extend_from_slice(&bone.transform.as_4x3());
550
551            // Post-billboard transform matrix (4x3)
552            data.extend_from_slice(&bone.post_billboard_transform.as_4x3());
553        }
554
555        data
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_mat4_identity() {
565        let m = Mat4::identity();
566        assert_eq!(m.data[0], 1.0);
567        assert_eq!(m.data[5], 1.0);
568        assert_eq!(m.data[10], 1.0);
569        assert_eq!(m.data[15], 1.0);
570    }
571
572    #[test]
573    fn test_mat4_translation() {
574        let m = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
575        let p = m.transform_point(Vec3::ZERO);
576        assert!((p.x - 1.0).abs() < 0.001);
577        assert!((p.y - 2.0).abs() < 0.001);
578        assert!((p.z - 3.0).abs() < 0.001);
579    }
580
581    #[test]
582    fn test_mat4_scale() {
583        let m = Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0));
584        let p = m.transform_point(Vec3::new(1.0, 1.0, 1.0));
585        assert!((p.x - 2.0).abs() < 0.001);
586        assert!((p.y - 3.0).abs() < 0.001);
587        assert!((p.z - 4.0).abs() < 0.001);
588    }
589
590    #[test]
591    fn test_mat4_multiply_identity() {
592        let a = Mat4::identity();
593        let b = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
594        let c = a.mul(&b);
595        assert_eq!(c.data, b.data);
596    }
597
598    #[test]
599    fn test_bone_flags() {
600        let flags = BoneFlags::from_raw(0x08); // spherical billboard
601        assert!(flags.spherical_billboard);
602        assert!(!flags.cylindrical_billboard_lock_x);
603
604        let flags = BoneFlags::from_raw(0x10); // cylindrical X
605        assert!(!flags.spherical_billboard);
606        assert!(flags.cylindrical_billboard_lock_x);
607    }
608
609    #[test]
610    fn test_bone_transform_computer_empty() {
611        let computer = BoneTransformComputer::empty();
612        assert_eq!(computer.bone_count(), 0);
613    }
614
615    #[test]
616    fn test_bone_transform_single_bone() {
617        let pivots = vec![Vec3::ZERO];
618        let parents = vec![-1i16];
619        let flags = vec![0u32];
620
621        let mut computer = BoneTransformComputer::new(&pivots, &parents, &flags);
622
623        // Update with identity transform
624        let translations = vec![Vec3::ZERO];
625        let rotations = vec![Quat::IDENTITY];
626        let scales = vec![Vec3::ONE];
627
628        computer.update(&translations, &rotations, &scales);
629
630        let transform = computer.get_transform(0);
631        // Should be identity
632        assert!((transform.data[0] - 1.0).abs() < 0.001);
633        assert!((transform.data[5] - 1.0).abs() < 0.001);
634        assert!((transform.data[10] - 1.0).abs() < 0.001);
635    }
636
637    #[test]
638    fn test_bone_transform_with_pivot() {
639        let pivots = vec![Vec3::new(0.0, 0.0, 10.0)];
640        let parents = vec![-1i16];
641        let flags = vec![0u32];
642
643        let mut computer = BoneTransformComputer::new(&pivots, &parents, &flags);
644
645        let translations = vec![Vec3::ZERO];
646        let rotations = vec![Quat::IDENTITY];
647        let scales = vec![Vec3::ONE];
648
649        computer.update(&translations, &rotations, &scales);
650
651        let transform = computer.get_transform(0);
652        // Pivot and anti-pivot cancel out for identity rotation
653        let p = transform.transform_point(Vec3::ZERO);
654        assert!(p.x.abs() < 0.001);
655        assert!(p.y.abs() < 0.001);
656        assert!(p.z.abs() < 0.001);
657    }
658
659    #[test]
660    fn test_bone_transform_parent_chain() {
661        // Two bones: root translates by (1,0,0), child translates by (0,1,0)
662        let pivots = vec![Vec3::ZERO, Vec3::ZERO];
663        let parents = vec![-1i16, 0]; // bone 1 is child of bone 0
664        let flags = vec![0u32, 0u32];
665
666        let mut computer = BoneTransformComputer::new(&pivots, &parents, &flags);
667
668        let translations = vec![Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)];
669        let rotations = vec![Quat::IDENTITY, Quat::IDENTITY];
670        let scales = vec![Vec3::ONE, Vec3::ONE];
671
672        computer.update(&translations, &rotations, &scales);
673
674        // Root bone should translate to (1,0,0)
675        let root_transform = computer.get_transform(0);
676        let p = root_transform.transform_point(Vec3::ZERO);
677        assert!((p.x - 1.0).abs() < 0.001);
678
679        // Child bone should be at (1,1,0) = parent + local
680        let child_transform = computer.get_transform(1);
681        let p = child_transform.transform_point(Vec3::ZERO);
682        assert!((p.x - 1.0).abs() < 0.001);
683        assert!((p.y - 1.0).abs() < 0.001);
684    }
685
686    #[test]
687    fn test_billboard_inheritance() {
688        // Root is billboard, child should inherit
689        let pivots = vec![Vec3::ZERO, Vec3::ZERO];
690        let parents = vec![-1i16, 0];
691        let flags = vec![0x08, 0]; // root is spherical billboard
692
693        let computer = BoneTransformComputer::new(&pivots, &parents, &flags);
694
695        assert!(computer.is_spherical_billboard(0));
696        assert!(computer.is_spherical_billboard(1)); // inherited
697    }
698}