mmd_anim_runtime/
append_primitive.rs1use 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 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}