Skip to main content

mmd_anim_runtime/
append_primitive.rs

1use glam::{Quat, Vec3A};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct AppendPrimitiveInput {
5    pub source_position_offset: Vec3A,
6    pub source_rotation: Quat,
7    pub ratio: f32,
8    pub affect_rotation: bool,
9    pub affect_translation: bool,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct AppendPrimitiveOutput {
14    pub position_offset: Vec3A,
15    pub rotation: Quat,
16}
17
18impl Default for AppendPrimitiveOutput {
19    fn default() -> Self {
20        Self {
21            position_offset: Vec3A::ZERO,
22            rotation: Quat::IDENTITY,
23        }
24    }
25}
26
27pub fn solve_append_transform(input: AppendPrimitiveInput) -> AppendPrimitiveOutput {
28    let rotation = if input.affect_rotation {
29        Quat::IDENTITY
30            .slerp(input.source_rotation, input.ratio)
31            .normalize()
32    } else {
33        Quat::IDENTITY
34    };
35    let position_offset = if input.affect_translation {
36        input.source_position_offset * input.ratio
37    } else {
38        Vec3A::ZERO
39    };
40
41    AppendPrimitiveOutput {
42        position_offset,
43        rotation,
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
52        let delta = (actual - expected).abs();
53        assert!(
54            delta.x < 1.0e-5 && delta.y < 1.0e-5 && delta.z < 1.0e-5,
55            "actual={actual:?} expected={expected:?} delta={delta:?}"
56        );
57    }
58
59    #[test]
60    fn solves_rotation_translation_and_ratio() {
61        let output = solve_append_transform(AppendPrimitiveInput {
62            source_position_offset: Vec3A::new(2.0, 4.0, -6.0),
63            source_rotation: Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
64            ratio: 0.5,
65            affect_rotation: true,
66            affect_translation: true,
67        });
68
69        assert_vec3a_near(output.position_offset, Vec3A::new(1.0, 2.0, -3.0));
70        assert_vec3a_near(
71            output.rotation.mul_vec3a(Vec3A::X),
72            Vec3A::new(
73                std::f32::consts::FRAC_1_SQRT_2,
74                std::f32::consts::FRAC_1_SQRT_2,
75                0.0,
76            ),
77        );
78    }
79
80    #[test]
81    fn disabled_channels_return_identity_offsets() {
82        let output = solve_append_transform(AppendPrimitiveInput {
83            source_position_offset: Vec3A::new(2.0, 0.0, 0.0),
84            source_rotation: Quat::from_rotation_y(1.0),
85            ratio: 1.0,
86            affect_rotation: false,
87            affect_translation: false,
88        });
89
90        assert_vec3a_near(output.position_offset, Vec3A::ZERO);
91        assert_eq!(output.rotation, Quat::IDENTITY);
92    }
93
94    #[test]
95    fn dependency_order_characterization_keeps_known_append_delta_bounded() {
96        let source_local = Quat::from_rotation_z(0.8);
97        let upstream = solve_append_transform(AppendPrimitiveInput {
98            source_position_offset: Vec3A::new(2.0, 0.0, 0.0),
99            source_rotation: source_local,
100            ratio: 0.5,
101            affect_rotation: true,
102            affect_translation: true,
103        });
104
105        let correct_order = solve_append_transform(AppendPrimitiveInput {
106            source_position_offset: upstream.position_offset,
107            source_rotation: upstream.rotation,
108            ratio: 0.5,
109            affect_rotation: true,
110            affect_translation: true,
111        });
112        let dependency_order = solve_append_transform(AppendPrimitiveInput {
113            source_position_offset: Vec3A::new(2.0, 0.0, 0.0),
114            source_rotation: source_local,
115            ratio: 0.5,
116            affect_rotation: true,
117            affect_translation: true,
118        });
119
120        let position_delta =
121            (correct_order.position_offset - dependency_order.position_offset).length();
122        let angular_delta = correct_order
123            .rotation
124            .angle_between(dependency_order.rotation);
125
126        assert!(
127            position_delta > 0.0 && angular_delta > 0.0,
128            "fixture must characterize a non-zero strict-order vs dependency-order delta"
129        );
130        assert!(
131            position_delta <= 0.5 && angular_delta <= 0.21,
132            "characterization budget widened unexpectedly: position_delta={position_delta} angular_delta={angular_delta}"
133        );
134    }
135
136    #[test]
137    fn append_primitive_is_bit_deterministic_in_current_process_profile() {
138        // The workspace currently enables glam fast-math; this test claims bit
139        // identity only for repeated solves in the same process/build profile.
140        let input = AppendPrimitiveInput {
141            source_position_offset: Vec3A::new(0.25, -0.5, 1.25),
142            source_rotation: Quat::from_rotation_y(0.7),
143            ratio: 0.375,
144            affect_rotation: true,
145            affect_translation: true,
146        };
147        let expected = solve_append_transform(input);
148        for _ in 0..32 {
149            let actual = solve_append_transform(input);
150            assert_eq!(
151                actual.position_offset.to_array().map(f32::to_bits),
152                expected.position_offset.to_array().map(f32::to_bits)
153            );
154            assert_eq!(
155                actual.rotation.to_array().map(f32::to_bits),
156                expected.rotation.to_array().map(f32::to_bits)
157            );
158        }
159    }
160}