Skip to content

Commit 79a13e0

Browse files
authored
Merge pull request #111 from SlimeYummy/feat/motion-blend
motion blend
2 parents 5a6926a + a640809 commit 79a13e0

File tree

8 files changed

+262
-1
lines changed

8 files changed

+262
-1
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232
steps:
3333
- uses: actions/checkout@v3
3434
- name: Build
35-
run: cd ./demo && cargo build --release
35+
run: cd ./demo && cargo build

resource/motion_blend/animation1.ozz

10.6 KB
Binary file not shown.

resource/motion_blend/animation2.ozz

7.53 KB
Binary file not shown.

resource/motion_blend/motion1.ozz

358 Bytes
Binary file not shown.

resource/motion_blend/motion2.ozz

261 Bytes
Binary file not shown.

resource/motion_blend/skeleton.ozz

3.73 KB
Binary file not shown.

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub mod ik_aim_job;
5757
pub mod ik_two_bone_job;
5858
pub mod local_to_model_job;
5959
pub mod math;
60+
pub mod motion_blending_job;
6061
#[cfg(all(feature = "wasm", feature = "nodejs"))]
6162
pub mod nodejs;
6263
pub mod sampling_job;
@@ -77,6 +78,9 @@ pub use ik_aim_job::IKAimJob;
7778
pub use ik_two_bone_job::IKTwoBoneJob;
7879
pub use local_to_model_job::{LocalToModelJob, LocalToModelJobArc, LocalToModelJobRc, LocalToModelJobRef};
7980
pub use math::{SoaQuat, SoaTransform, SoaVec3};
81+
pub use motion_blending_job::{
82+
MotionBlendingJob, MotionBlendingJobArc, MotionBlendingJobRc, MotionBlendingJobRef, MotionBlendingLayer,
83+
};
8084
pub use sampling_job::{
8185
InterpSoaFloat3, InterpSoaQuaternion, SamplingContext, SamplingJob, SamplingJobArc, SamplingJobRc, SamplingJobRef,
8286
};

src/motion_blending_job.rs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//!
2+
//! Motion Blending Job.
3+
//!
4+
5+
use glam::{Quat, Vec3A, Vec4};
6+
use glam_ext::Transform3A;
7+
8+
use crate::base::OzzError;
9+
10+
/// Defines a layer of blending input data and its weight.
11+
#[derive(Debug, Default, Clone)]
12+
pub struct MotionBlendingLayer {
13+
/// Blending weight of this layer.
14+
///
15+
/// Negative values are considered as 0.
16+
/// Normalization is performed at the end of the blending stage, so weight can be in any range,
17+
/// even though range [0:1] is optimal.
18+
pub weight: f32,
19+
20+
/// The motion delta transform to be blended.
21+
pub delta: Transform3A,
22+
}
23+
24+
///
25+
/// MotionBlendingJob is in charge of blending delta motions according to their respective weight.
26+
///
27+
/// MotionBlendingJob is usually done to blend the motion resulting from the motion extraction
28+
/// process, in parallel to blending animations.
29+
///
30+
#[derive(Debug, Default)]
31+
pub struct MotionBlendingJob {
32+
layers: Vec<MotionBlendingLayer>,
33+
output: Transform3A,
34+
}
35+
36+
pub type MotionBlendingJobRef = MotionBlendingJob;
37+
pub type MotionBlendingJobRc = MotionBlendingJob;
38+
pub type MotionBlendingJobArc = MotionBlendingJob;
39+
40+
impl MotionBlendingJob {
41+
/// Gets layers of `MotionBlendingJob`.
42+
#[inline]
43+
pub fn layers(&self) -> &[MotionBlendingLayer] {
44+
&self.layers
45+
}
46+
47+
/// Gets mutable layers of `MotionBlendingJob`.
48+
///
49+
/// Job input layers, can be empty. The range of layers that must be blended.
50+
#[inline]
51+
pub fn layers_mut(&mut self) -> &mut Vec<MotionBlendingLayer> {
52+
&mut self.layers
53+
}
54+
55+
/// Gets output of `MotionBlendingJob`.
56+
#[inline]
57+
pub fn output(&self) -> Transform3A {
58+
self.output
59+
}
60+
61+
/// Sets output of `MotionBlendingJob`.
62+
///
63+
/// The range of output transforms to be filled with blended layer transforms during job execution.
64+
#[inline]
65+
pub fn set_output(&mut self, output: Transform3A) {
66+
self.output = output;
67+
}
68+
69+
/// Clears output of `MotionBlendingJob`.
70+
#[inline]
71+
pub fn clear_output(&mut self) {
72+
self.output = Default::default();
73+
}
74+
75+
/// Validates `MotionBlendingJob` parameters.
76+
pub fn validate(&self) -> bool {
77+
true
78+
}
79+
80+
/// Runs job's blending task.
81+
/// The validate job before any operation is performed.
82+
pub fn run(&mut self) -> Result<(), OzzError> {
83+
self.output = Transform3A::IDENTITY;
84+
85+
let mut acc_weight = 0.0; // Accumulates weights for normalization
86+
let mut dl = 0.0f32; // Weighted translation lengths
87+
let mut dt = Vec3A::ZERO; // Weighted translations directions
88+
let mut dr = Vec4::ZERO; // Weighted rotations
89+
90+
for layer in &self.layers {
91+
let weight = layer.weight;
92+
if weight <= 0.0 {
93+
continue;
94+
}
95+
acc_weight += weight;
96+
97+
// Decomposes translation into a normalized vector and a length, to limit
98+
// lerp error while interpolating vector length.
99+
let len = layer.delta.translation.length();
100+
dl += len * weight;
101+
let denom = if len == 0.0 { 0.0 } else { weight / len };
102+
dt += layer.delta.translation * denom;
103+
104+
// Accumulate weighted rotation (NLerp)
105+
let rot_vec = Vec4::from(layer.delta.rotation);
106+
let dot = dr.dot(rot_vec);
107+
let signed_weight = weight.copysign(if dot >= 0.0 { 1.0 } else { -1.0 });
108+
dr += rot_vec * signed_weight;
109+
}
110+
111+
// Normalizes translation and re-applies interpolated length.
112+
let denom = dt.length() * acc_weight;
113+
let norm = if denom == 0.0 { 0.0 } else { dl / denom };
114+
self.output.translation = dt * norm;
115+
116+
if dr.length_squared() != 0.0 {
117+
self.output.rotation = Quat::from_vec4(dr).normalize();
118+
} else {
119+
self.output.rotation = Quat::IDENTITY;
120+
}
121+
122+
self.output.scale = Vec3A::ONE;
123+
Ok(())
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod motion_blending_tests {
129+
use glam::Vec3;
130+
use wasm_bindgen_test::*;
131+
132+
use super::*;
133+
134+
#[test]
135+
#[wasm_bindgen_test]
136+
fn test_empty() {
137+
let mut job = MotionBlendingJob::default();
138+
job.run().unwrap();
139+
assert_eq!(job.output(), Transform3A::IDENTITY);
140+
}
141+
142+
#[test]
143+
#[wasm_bindgen_test]
144+
fn test_run() {
145+
let mut job = MotionBlendingJob::default();
146+
147+
// No layer
148+
job.run().unwrap();
149+
assert_eq!(job.output(), Transform3A::IDENTITY);
150+
151+
// With layers
152+
job.layers_mut().push(MotionBlendingLayer {
153+
weight: 0.0,
154+
delta: Transform3A::new(
155+
Vec3::new(2.0, 0.0, 0.0),
156+
Quat::from_xyzw(0.70710677, 0.0, 0.0, 0.70710677),
157+
Vec3::ONE,
158+
),
159+
});
160+
job.layers_mut().push(MotionBlendingLayer {
161+
weight: 0.0,
162+
delta: Transform3A::new(
163+
Vec3::new(0.0, 0.0, 3.0),
164+
Quat::from_xyzw(0.0, -0.70710677, 0.0, -0.70710677),
165+
Vec3::ONE,
166+
),
167+
});
168+
169+
// 0 weights
170+
job.layers_mut()[0].weight = 0.0;
171+
job.layers_mut()[1].weight = 0.0;
172+
job.run().unwrap();
173+
assert_eq!(job.output(), Transform3A::IDENTITY);
174+
175+
// One non 0 weights
176+
job.layers_mut()[0].weight = 0.8;
177+
job.layers_mut()[1].weight = 0.0;
178+
job.run().unwrap();
179+
assert!(job.output().abs_diff_eq(
180+
Transform3A::new(
181+
Vec3::new(2.0, 0.0, 0.0),
182+
Quat::from_xyzw(0.70710677, 0.0, 0.0, 0.70710677),
183+
Vec3::ONE,
184+
),
185+
1e-6
186+
));
187+
188+
// one negative weights
189+
job.layers_mut()[0].weight = 0.8;
190+
job.layers_mut()[1].weight = -1.0;
191+
job.run().unwrap();
192+
assert!(job.output().abs_diff_eq(
193+
Transform3A::new(
194+
Vec3::new(2.0, 0.0, 0.0),
195+
Quat::from_xyzw(0.70710677, 0.0, 0.0, 0.70710677),
196+
Vec3::ONE,
197+
),
198+
1e-6
199+
));
200+
201+
// Two non 0 weights
202+
job.layers_mut()[0].weight = 0.8;
203+
job.layers_mut()[1].weight = 0.2;
204+
job.run().unwrap();
205+
assert!(job.output().abs_diff_eq(
206+
Transform3A::new(
207+
Vec3::new(2.134313, 0.0, 0.533578),
208+
Quat::from_xyzw(0.6172133, 0.1543033, 0.0, 0.7715167),
209+
Vec3::ONE,
210+
),
211+
1e-6
212+
));
213+
214+
// Non normalized weights
215+
job.layers_mut()[0].weight = 8.0;
216+
job.layers_mut()[1].weight = 2.0;
217+
job.run().unwrap();
218+
assert!(job.output().abs_diff_eq(
219+
Transform3A::new(
220+
Vec3::new(2.134313, 0.0, 0.533578),
221+
Quat::from_xyzw(0.6172133, 0.1543033, 0.0, 0.7715167),
222+
Vec3::ONE,
223+
),
224+
1e-6
225+
));
226+
227+
// 0 length translation
228+
job.layers_mut()[0].delta.translation = Vec3A::ZERO;
229+
job.layers_mut()[1].delta.translation = Vec3A::new(0.0, 0.0, 2.0);
230+
job.layers_mut()[0].weight = 0.8;
231+
job.layers_mut()[1].weight = 0.2;
232+
job.run().unwrap();
233+
assert!(job.output().abs_diff_eq(
234+
Transform3A::new(
235+
Vec3::new(0.0, 0.0, 0.4),
236+
Quat::from_xyzw(0.6172133, 0.1543033, 0.0, 0.7715167),
237+
Vec3::ONE,
238+
),
239+
1e-6
240+
));
241+
242+
// Opposed translations
243+
job.layers_mut()[0].delta.translation = Vec3A::new(0.0, 0.0, -2.0);
244+
job.layers_mut()[1].delta.translation = Vec3A::new(0.0, 0.0, 2.0);
245+
job.layers_mut()[0].weight = 1.0;
246+
job.layers_mut()[1].weight = 1.0;
247+
job.run().unwrap();
248+
assert!(job.output().abs_diff_eq(
249+
Transform3A::new(
250+
Vec3::new(0.0, 0.0, 0.0),
251+
Quat::from_xyzw(0.408248, 0.408248, 0.0, 0.816496),
252+
Vec3::ONE,
253+
),
254+
1e-6
255+
));
256+
}
257+
}

0 commit comments

Comments
 (0)