Skip to main content

oxihuman_morph/
motion_warp.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Motion time-warping, speed scaling, and blend between motion clips.
5
6#[allow(dead_code)]
7pub struct MotionFrame {
8    pub time: f32,
9    pub pose: Vec<f32>,
10}
11
12#[allow(dead_code)]
13pub struct MotionClip {
14    pub name: String,
15    pub frames: Vec<MotionFrame>,
16    pub fps: f32,
17}
18
19#[allow(dead_code)]
20pub struct WarpCurve {
21    pub keys: Vec<(f32, f32)>,
22}
23
24#[allow(dead_code)]
25pub enum WarpMode {
26    Linear,
27    Bezier,
28    Hold,
29}
30
31#[allow(dead_code)]
32pub struct WarpedClip {
33    pub frames: Vec<MotionFrame>,
34    pub original_duration: f32,
35    pub warped_duration: f32,
36}
37
38#[allow(dead_code)]
39pub fn clip_duration(clip: &MotionClip) -> f32 {
40    if clip.frames.is_empty() {
41        return 0.0;
42    }
43    clip.frames[clip.frames.len() - 1].time - clip.frames[0].time
44}
45
46#[allow(dead_code)]
47pub fn sample_clip(clip: &MotionClip, time: f32) -> Vec<f32> {
48    if clip.frames.is_empty() {
49        return Vec::new();
50    }
51    if clip.frames.len() == 1 {
52        return clip.frames[0].pose.clone();
53    }
54    let first = &clip.frames[0];
55    let last = &clip.frames[clip.frames.len() - 1];
56    if time <= first.time {
57        return first.pose.clone();
58    }
59    if time >= last.time {
60        return last.pose.clone();
61    }
62    // find surrounding frames
63    let mut lo = 0usize;
64    let mut hi = clip.frames.len() - 1;
65    while lo + 1 < hi {
66        let mid = (lo + hi) / 2;
67        if clip.frames[mid].time <= time {
68            lo = mid;
69        } else {
70            hi = mid;
71        }
72    }
73    let fa = &clip.frames[lo];
74    let fb = &clip.frames[hi];
75    let span = fb.time - fa.time;
76    let t = if span.abs() < 1e-9 {
77        0.0
78    } else {
79        (time - fa.time) / span
80    };
81    pose_lerp(&fa.pose, &fb.pose, t)
82}
83
84#[allow(dead_code)]
85pub fn warp_time(curve: &WarpCurve, t: f32) -> f32 {
86    if curve.keys.is_empty() {
87        return t;
88    }
89    if curve.keys.len() == 1 {
90        return curve.keys[0].1;
91    }
92    let first = curve.keys[0];
93    let last = curve.keys[curve.keys.len() - 1];
94    if t <= first.0 {
95        return first.1;
96    }
97    if t >= last.0 {
98        return last.1;
99    }
100    let mut lo = 0usize;
101    let mut hi = curve.keys.len() - 1;
102    while lo + 1 < hi {
103        let mid = (lo + hi) / 2;
104        if curve.keys[mid].0 <= t {
105            lo = mid;
106        } else {
107            hi = mid;
108        }
109    }
110    let (t0, v0) = curve.keys[lo];
111    let (t1, v1) = curve.keys[hi];
112    let span = t1 - t0;
113    let alpha = if span.abs() < 1e-9 {
114        0.0
115    } else {
116        (t - t0) / span
117    };
118    v0 + (v1 - v0) * alpha
119}
120
121#[allow(dead_code)]
122pub fn apply_warp(clip: &MotionClip, curve: &WarpCurve, output_fps: f32) -> WarpedClip {
123    let orig_duration = clip_duration(clip);
124    if clip.frames.is_empty() || output_fps <= 0.0 {
125        return WarpedClip {
126            frames: Vec::new(),
127            original_duration: orig_duration,
128            warped_duration: 0.0,
129        };
130    }
131    let warped_end = warp_time(curve, orig_duration);
132    let warped_duration = warped_end;
133    let frame_count = ((warped_duration * output_fps).round() as usize).max(1);
134    let mut frames = Vec::with_capacity(frame_count);
135    for i in 0..frame_count {
136        let warped_t = i as f32 / output_fps;
137        let orig_t = warp_time(curve, warped_t);
138        let pose = sample_clip(clip, orig_t);
139        frames.push(MotionFrame {
140            time: warped_t,
141            pose,
142        });
143    }
144    WarpedClip {
145        frames,
146        original_duration: orig_duration,
147        warped_duration,
148    }
149}
150
151#[allow(dead_code)]
152pub fn speed_scale_clip(clip: &MotionClip, factor: f32) -> WarpedClip {
153    let orig_duration = clip_duration(clip);
154    let factor = factor.max(1e-6);
155    let warped_duration = orig_duration / factor;
156    let output_fps = clip.fps.max(1.0);
157    let frame_count = ((warped_duration * output_fps).round() as usize).max(1);
158    let mut frames = Vec::with_capacity(frame_count);
159    for i in 0..frame_count {
160        let warped_t = i as f32 / output_fps;
161        let orig_t = warped_t * factor;
162        let pose = sample_clip(clip, orig_t);
163        frames.push(MotionFrame {
164            time: warped_t,
165            pose,
166        });
167    }
168    WarpedClip {
169        frames,
170        original_duration: orig_duration,
171        warped_duration,
172    }
173}
174
175#[allow(dead_code)]
176pub fn reverse_clip(clip: &MotionClip) -> MotionClip {
177    let duration = clip_duration(clip);
178    let frames: Vec<MotionFrame> = clip
179        .frames
180        .iter()
181        .rev()
182        .map(|f| MotionFrame {
183            time: duration - f.time,
184            pose: f.pose.clone(),
185        })
186        .collect();
187    MotionClip {
188        name: format!("{}_reversed", clip.name),
189        frames,
190        fps: clip.fps,
191    }
192}
193
194#[allow(dead_code)]
195pub fn trim_clip(clip: &MotionClip, start: f32, end: f32) -> MotionClip {
196    let frames: Vec<MotionFrame> = clip
197        .frames
198        .iter()
199        .filter(|f| f.time >= start && f.time <= end)
200        .map(|f| MotionFrame {
201            time: f.time - start,
202            pose: f.pose.clone(),
203        })
204        .collect();
205    MotionClip {
206        name: clip.name.clone(),
207        frames,
208        fps: clip.fps,
209    }
210}
211
212#[allow(dead_code)]
213pub fn blend_clips(
214    a: &MotionClip,
215    b: &MotionClip,
216    blend_weight: f32,
217    output_fps: f32,
218) -> MotionClip {
219    let t = blend_weight.clamp(0.0, 1.0);
220    let dur_a = clip_duration(a);
221    let dur_b = clip_duration(b);
222    let duration = dur_a * (1.0 - t) + dur_b * t;
223    let fps = output_fps.max(1.0);
224    let frame_count = ((duration * fps).round() as usize).max(1);
225    let mut frames = Vec::with_capacity(frame_count);
226    for i in 0..frame_count {
227        let time = i as f32 / fps;
228        let pa = sample_clip(a, time);
229        let pb = sample_clip(b, time);
230        let pose = pose_lerp(&pa, &pb, t);
231        frames.push(MotionFrame { time, pose });
232    }
233    MotionClip {
234        name: format!("blend_{}_{}", a.name, b.name),
235        frames,
236        fps,
237    }
238}
239
240#[allow(dead_code)]
241pub fn concat_clips(clips: &[MotionClip]) -> MotionClip {
242    let mut frames = Vec::new();
243    let mut offset = 0.0f32;
244    let fps = clips.first().map(|c| c.fps).unwrap_or(30.0);
245    for clip in clips {
246        for f in &clip.frames {
247            frames.push(MotionFrame {
248                time: f.time + offset,
249                pose: f.pose.clone(),
250            });
251        }
252        offset += clip_duration(clip);
253    }
254    MotionClip {
255        name: "concat".to_string(),
256        frames,
257        fps,
258    }
259}
260
261#[allow(dead_code)]
262pub fn loop_clip(clip: &MotionClip, loops: u32) -> MotionClip {
263    if loops == 0 {
264        return MotionClip {
265            name: clip.name.clone(),
266            frames: Vec::new(),
267            fps: clip.fps,
268        };
269    }
270    let duration = clip_duration(clip);
271    let mut frames = Vec::new();
272    for l in 0..loops {
273        let offset = duration * l as f32;
274        for f in &clip.frames {
275            frames.push(MotionFrame {
276                time: f.time + offset,
277                pose: f.pose.clone(),
278            });
279        }
280    }
281    MotionClip {
282        name: format!("{}_loop{}", clip.name, loops),
283        frames,
284        fps: clip.fps,
285    }
286}
287
288#[allow(dead_code)]
289pub fn identity_warp_curve() -> WarpCurve {
290    WarpCurve {
291        keys: vec![(0.0, 0.0), (1.0, 1.0)],
292    }
293}
294
295#[allow(dead_code)]
296pub fn linear_warp_curve(speed_factor: f32) -> WarpCurve {
297    let factor = speed_factor.max(1e-6);
298    WarpCurve {
299        keys: vec![(0.0, 0.0), (1.0, 1.0 / factor)],
300    }
301}
302
303#[allow(dead_code)]
304pub fn pose_lerp(a: &[f32], b: &[f32], t: f32) -> Vec<f32> {
305    let len = a.len().min(b.len());
306    let mut out = Vec::with_capacity(len);
307    for i in 0..len {
308        out.push(a[i] + (b[i] - a[i]) * t);
309    }
310    out
311}
312
313#[allow(dead_code)]
314fn make_test_clip(name: &str, frames: usize, fps: f32, joints: usize) -> MotionClip {
315    let dt = 1.0 / fps;
316    MotionClip {
317        name: name.to_string(),
318        frames: (0..frames)
319            .map(|i| MotionFrame {
320                time: i as f32 * dt,
321                pose: vec![i as f32; joints],
322            })
323            .collect(),
324        fps,
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_clip_duration() {
334        let clip = make_test_clip("test", 31, 30.0, 4);
335        let d = clip_duration(&clip);
336        assert!((d - 1.0).abs() < 0.01);
337    }
338
339    #[test]
340    fn test_clip_duration_empty() {
341        let clip = MotionClip {
342            name: "empty".to_string(),
343            frames: Vec::new(),
344            fps: 30.0,
345        };
346        assert_eq!(clip_duration(&clip), 0.0);
347    }
348
349    #[test]
350    fn test_sample_at_t0_gives_first_frame() {
351        let clip = make_test_clip("test", 10, 30.0, 3);
352        let pose = sample_clip(&clip, 0.0);
353        assert_eq!(pose, vec![0.0_f32, 0.0, 0.0]);
354    }
355
356    #[test]
357    fn test_sample_at_end_gives_last_frame() {
358        let clip = make_test_clip("test", 5, 30.0, 2);
359        let d = clip_duration(&clip);
360        let pose = sample_clip(&clip, d);
361        assert_eq!(pose, vec![4.0_f32, 4.0]);
362    }
363
364    #[test]
365    fn test_speed_scale_halves_duration() {
366        let clip = make_test_clip("test", 31, 30.0, 2);
367        let orig_dur = clip_duration(&clip);
368        let warped = speed_scale_clip(&clip, 2.0);
369        assert!((warped.warped_duration - orig_dur / 2.0).abs() < 0.05);
370    }
371
372    #[test]
373    fn test_speed_scale_doubles_duration() {
374        let clip = make_test_clip("test", 31, 30.0, 2);
375        let orig_dur = clip_duration(&clip);
376        let warped = speed_scale_clip(&clip, 0.5);
377        assert!((warped.warped_duration - orig_dur * 2.0).abs() < 0.05);
378    }
379
380    #[test]
381    fn test_reverse_clip_inverts() {
382        let clip = make_test_clip("test", 5, 30.0, 1);
383        let rev = reverse_clip(&clip);
384        assert_eq!(rev.frames.len(), 5);
385        // Last frame of original becomes first of reversed
386        let orig_last_pose = clip.frames.last().expect("should succeed").pose.clone();
387        assert!((rev.frames[0].time).abs() < 1e-4);
388        // original last frame value
389        assert!((rev.frames[0].pose[0] - orig_last_pose[0]).abs() < 1e-4);
390    }
391
392    #[test]
393    fn test_trim_clip_shrinks() {
394        let clip = make_test_clip("test", 31, 30.0, 2);
395        let trimmed = trim_clip(&clip, 0.0, 0.5);
396        let dur = clip_duration(&trimmed);
397        assert!(dur <= 0.5 + 0.04);
398    }
399
400    #[test]
401    fn test_blend_identical_clips() {
402        let a = make_test_clip("a", 10, 30.0, 3);
403        let b = make_test_clip("b", 10, 30.0, 3);
404        let blended = blend_clips(&a, &b, 0.5, 30.0);
405        let pa = sample_clip(&a, 0.0);
406        let pb = sample_clip(&blended, 0.0);
407        for (va, vb) in pa.iter().zip(pb.iter()) {
408            assert!((va - vb).abs() < 1e-4);
409        }
410    }
411
412    #[test]
413    fn test_concat_clips() {
414        let a = make_test_clip("a", 31, 30.0, 2);
415        let b = make_test_clip("b", 31, 30.0, 2);
416        let dur_a = clip_duration(&a);
417        let dur_b = clip_duration(&b);
418        let cat = concat_clips(&[a, b]);
419        let dur = clip_duration(&cat);
420        assert!((dur - (dur_a + dur_b)).abs() < 0.05);
421    }
422
423    #[test]
424    fn test_loop_doubles_duration() {
425        let clip = make_test_clip("test", 31, 30.0, 2);
426        let orig_dur = clip_duration(&clip);
427        let looped = loop_clip(&clip, 2);
428        let loop_dur = clip_duration(&looped);
429        assert!((loop_dur - orig_dur * 2.0).abs() < 0.05);
430    }
431
432    #[test]
433    fn test_loop_zero() {
434        let clip = make_test_clip("test", 5, 30.0, 2);
435        let looped = loop_clip(&clip, 0);
436        assert!(looped.frames.is_empty());
437    }
438
439    #[test]
440    fn test_identity_warp() {
441        let curve = identity_warp_curve();
442        assert!((warp_time(&curve, 0.5) - 0.5).abs() < 1e-4);
443    }
444
445    #[test]
446    fn test_linear_warp_curve() {
447        let curve = linear_warp_curve(2.0);
448        // at t=1, output = 0.5
449        assert!((warp_time(&curve, 1.0) - 0.5).abs() < 1e-4);
450    }
451
452    #[test]
453    fn test_pose_lerp() {
454        let a = vec![0.0_f32, 0.0, 0.0];
455        let b = vec![2.0_f32, 4.0, 6.0];
456        let result = pose_lerp(&a, &b, 0.5);
457        assert!((result[0] - 1.0).abs() < 1e-5);
458        assert!((result[1] - 2.0).abs() < 1e-5);
459        assert!((result[2] - 3.0).abs() < 1e-5);
460    }
461}