Skip to main content

renderling/gltf/
anime.rs

1//! Animation helpers for gltf.
2use glam::{Quat, Vec3};
3use snafu::prelude::*;
4
5use crate::{geometry::MorphTargetWeights, gltf::GltfNode, transform::NestedTransform};
6
7#[derive(Debug, Snafu)]
8pub enum InterpolationError {
9    #[snafu(display("No keyframes"))]
10    NoKeyframes,
11
12    #[snafu(display("Not enough keyframes"))]
13    NotEnoughKeyframes,
14
15    #[snafu(display("No node with index {index}"))]
16    MissingNode { index: usize },
17
18    #[snafu(display("Property with index {} is missing", index))]
19    MissingPropertyIndex { index: usize },
20
21    #[snafu(display("No previous keyframe, first is {first:?}"))]
22    NoPreviousKeyframe { first: Keyframe },
23
24    #[snafu(display("No next keyframe, last is {last:?}"))]
25    NoNextKeyframe { last: Keyframe },
26
27    #[snafu(display("Mismatched properties"))]
28    MismatchedProperties,
29}
30
31#[derive(Debug, Clone, Copy)]
32pub enum Interpolation {
33    Linear,
34    Step,
35    CubicSpline,
36}
37
38impl std::fmt::Display for Interpolation {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str(match self {
41            Interpolation::Linear => "linear",
42            Interpolation::Step => "step",
43            Interpolation::CubicSpline => "cubic spline",
44        })
45    }
46}
47
48impl From<gltf::animation::Interpolation> for Interpolation {
49    fn from(value: gltf::animation::Interpolation) -> Self {
50        match value {
51            gltf::animation::Interpolation::Linear => Interpolation::Linear,
52            gltf::animation::Interpolation::Step => Interpolation::Step,
53            gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,
54        }
55    }
56}
57
58impl Interpolation {
59    fn is_cubic_spline(&self) -> bool {
60        matches!(self, Interpolation::CubicSpline)
61    }
62}
63
64#[derive(Debug, Clone, Copy)]
65pub struct Keyframe(pub f32);
66
67#[derive(Debug)]
68pub enum TweenProperty {
69    Translation(Vec3),
70    Rotation(Quat),
71    Scale(Vec3),
72    MorphTargetWeights(Vec<f32>),
73}
74
75impl TweenProperty {
76    fn as_translation(&self) -> Option<&Vec3> {
77        match self {
78            TweenProperty::Translation(a) => Some(a),
79            _ => None,
80        }
81    }
82
83    fn as_rotation(&self) -> Option<&Quat> {
84        match self {
85            TweenProperty::Rotation(a) => Some(a),
86            _ => None,
87        }
88    }
89
90    fn as_scale(&self) -> Option<&Vec3> {
91        match self {
92            TweenProperty::Scale(a) => Some(a),
93            _ => None,
94        }
95    }
96
97    fn as_morph_target_weights(&self) -> Option<&Vec<f32>> {
98        match self {
99            TweenProperty::MorphTargetWeights(ws) => Some(ws),
100            _ => None,
101        }
102    }
103
104    pub fn description(&self) -> &'static str {
105        match self {
106            TweenProperty::Translation(_) => "translation",
107            TweenProperty::Rotation(_) => "rotation",
108            TweenProperty::Scale(_) => "scale",
109            TweenProperty::MorphTargetWeights(_) => "morph target",
110        }
111    }
112}
113
114/// Holds many keyframes worth of tweening properties.
115#[derive(Debug, Clone)]
116pub enum TweenProperties {
117    Translations(Vec<Vec3>),
118    Rotations(Vec<Quat>),
119    Scales(Vec<Vec3>),
120    MorphTargetWeights(Vec<Vec<f32>>),
121}
122
123impl TweenProperties {
124    pub fn get(&self, index: usize) -> Option<TweenProperty> {
125        match self {
126            TweenProperties::Translations(translations) => translations
127                .get(index)
128                .map(|translation| TweenProperty::Translation(*translation)),
129            TweenProperties::Rotations(rotations) => rotations
130                .get(index)
131                .map(|rotation| TweenProperty::Rotation(*rotation)),
132            TweenProperties::Scales(scales) => {
133                scales.get(index).map(|scale| TweenProperty::Scale(*scale))
134            }
135            TweenProperties::MorphTargetWeights(weights) => weights
136                .get(index)
137                .map(|weights| TweenProperty::MorphTargetWeights(weights.clone())),
138        }
139    }
140
141    pub fn get_cubic(&self, index: usize) -> Option<[TweenProperty; 3]> {
142        let start = 3 * index;
143        let end = start + 3;
144        match self {
145            TweenProperties::Translations(translations) => {
146                if let Some([p0, p1, p2]) = translations.get(start..end) {
147                    Some([
148                        TweenProperty::Translation(*p0),
149                        TweenProperty::Translation(*p1),
150                        TweenProperty::Translation(*p2),
151                    ])
152                } else {
153                    None
154                }
155            }
156            TweenProperties::Rotations(rotations) => {
157                if let Some([p0, p1, p2]) = rotations.get(start..end) {
158                    Some([
159                        TweenProperty::Rotation(*p0),
160                        TweenProperty::Rotation(*p1),
161                        TweenProperty::Rotation(*p2),
162                    ])
163                } else {
164                    None
165                }
166            }
167            TweenProperties::Scales(scales) => {
168                if let Some([p0, p1, p2]) = scales.get(start..end) {
169                    Some([
170                        TweenProperty::Scale(*p0),
171                        TweenProperty::Scale(*p1),
172                        TweenProperty::Scale(*p2),
173                    ])
174                } else {
175                    None
176                }
177            }
178            TweenProperties::MorphTargetWeights(weights) => {
179                if let Some([p0, p1, p2]) = weights.get(start..end) {
180                    Some([
181                        TweenProperty::MorphTargetWeights(p0.clone()),
182                        TweenProperty::MorphTargetWeights(p1.clone()),
183                        TweenProperty::MorphTargetWeights(p2.clone()),
184                    ])
185                } else {
186                    None
187                }
188            }
189        }
190    }
191
192    pub fn description(&self) -> &'static str {
193        match self {
194            TweenProperties::Translations(_) => "translation",
195            TweenProperties::Rotations(_) => "rotation",
196            TweenProperties::Scales(_) => "scale",
197            TweenProperties::MorphTargetWeights(_) => "morph targets",
198        }
199    }
200}
201
202#[derive(Debug, Clone)]
203pub struct Tween {
204    // Times (inputs)
205    pub keyframes: Vec<Keyframe>,
206    // Properties (outputs)
207    pub properties: TweenProperties,
208    // The type of interpolation
209    pub interpolation: Interpolation,
210    // The gltf "nodes" index of the target node this tween applies to
211    pub target_node_index: usize,
212}
213
214impl Tween {
215    /// Compute the interpolated tween property at the given time.
216    ///
217    /// If the given time is before the first keyframe or after the the last
218    /// keyframe, `Ok(None)` is returned.
219    ///
220    /// See <https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_007_Animations.md>
221    pub fn interpolate(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
222        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);
223
224        match self.interpolation {
225            Interpolation::Linear => self.interpolate_linear(time),
226            Interpolation::Step => self.interpolate_step(time),
227            Interpolation::CubicSpline => self.interpolate_cubic(time),
228        }
229    }
230
231    /// Compute the interpolated tween property at the given time.
232    ///
233    /// If the time is greater than the last keyframe, the time will be wrapped
234    /// to loop the tween.
235    ///
236    /// Returns `None` if the properties don't match.
237    pub fn interpolate_wrap(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
238        let total = self.length_in_seconds();
239        let time = time % total;
240        self.interpolate(time)
241    }
242
243    fn get_previous_keyframe(
244        &self,
245        time: f32,
246    ) -> Result<Option<(usize, &Keyframe)>, InterpolationError> {
247        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);
248        Ok(self
249            .keyframes
250            .iter()
251            .enumerate()
252            .rfind(|(_, keyframe)| keyframe.0 <= time))
253    }
254
255    fn get_next_keyframe(
256        &self,
257        time: f32,
258    ) -> Result<Option<(usize, &Keyframe)>, InterpolationError> {
259        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);
260        Ok(self
261            .keyframes
262            .iter()
263            .enumerate()
264            .find(|(_, keyframe)| keyframe.0 > time))
265    }
266
267    fn interpolate_step(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
268        if let Some((prev_keyframe_ndx, _)) = self.get_previous_keyframe(time)? {
269            self.properties
270                .get(prev_keyframe_ndx)
271                .context(MissingPropertyIndexSnafu {
272                    index: prev_keyframe_ndx,
273                })
274                .map(Some)
275        } else {
276            Ok(None)
277        }
278    }
279
280    fn interpolate_cubic(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
281        snafu::ensure!(self.keyframes.len() >= 2, NotEnoughKeyframesSnafu);
282
283        let (prev_keyframe_ndx, prev_keyframe) =
284            if let Some(prev) = self.get_previous_keyframe(time)? {
285                prev
286            } else {
287                return Ok(None);
288            };
289        let prev_time = prev_keyframe.0;
290
291        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {
292            next
293        } else {
294            return Ok(None);
295        };
296        let next_time = next_keyframe.0;
297
298        // UNWRAP: safe because we know this was found above
299        let [_, from, from_out] =
300            self.properties
301                .get_cubic(prev_keyframe_ndx)
302                .context(MissingPropertyIndexSnafu {
303                    index: prev_keyframe_ndx,
304                })?;
305        // UNWRAP: safe because we know this is either the first index or was
306        // found above
307        let [to_in, to, _] =
308            self.properties
309                .get_cubic(next_keyframe_ndx)
310                .context(MissingPropertyIndexSnafu {
311                    index: next_keyframe_ndx,
312                })?;
313
314        let delta_time = next_time - prev_time;
315        let amount = (time - prev_time) / (next_time - prev_time);
316
317        fn cubic_spline<T>(
318            previous_point: T,
319            previous_tangent: T,
320            next_point: T,
321            next_tangent: T,
322            t: f32,
323        ) -> T
324        where
325            T: std::ops::Mul<f32, Output = T> + std::ops::Add<Output = T>,
326        {
327            let t2 = t * t;
328            let t3 = t2 * t;
329            previous_point * (2.0 * t3 - 3.0 * t2 + 1.0)
330                + previous_tangent * (t3 - 2.0 * t2 + t)
331                + next_point * (-2.0 * t3 + 3.0 * t2)
332                + next_tangent * (t3 - t2)
333        }
334
335        Ok(Some(match from {
336            TweenProperty::Translation(from) => {
337                let from_out = *from_out
338                    .as_translation()
339                    .context(MismatchedPropertiesSnafu)?;
340                let to_in = *to_in.as_translation().context(MismatchedPropertiesSnafu)?;
341                let to = *to.as_translation().context(MismatchedPropertiesSnafu)?;
342                let previous_tangent = delta_time * from_out;
343                let next_tangent = delta_time * to_in;
344                TweenProperty::Translation(cubic_spline(
345                    from,
346                    previous_tangent,
347                    to,
348                    next_tangent,
349                    amount,
350                ))
351            }
352            TweenProperty::Rotation(from) => {
353                let from_out = *from_out.as_rotation().context(MismatchedPropertiesSnafu)?;
354                let to_in = *to_in.as_rotation().context(MismatchedPropertiesSnafu)?;
355                let to = *to.as_rotation().context(MismatchedPropertiesSnafu)?;
356                let previous_tangent = from_out * delta_time;
357                let next_tangent = to_in * delta_time;
358                TweenProperty::Rotation(cubic_spline(
359                    from,
360                    previous_tangent,
361                    to,
362                    next_tangent,
363                    amount,
364                ))
365            }
366            TweenProperty::Scale(from) => {
367                let from_out = *from_out.as_scale().context(MismatchedPropertiesSnafu)?;
368                let to_in = *to_in.as_scale().context(MismatchedPropertiesSnafu)?;
369                let to = *to.as_scale().context(MismatchedPropertiesSnafu)?;
370                let previous_tangent = from_out * delta_time;
371                let next_tangent = to_in * delta_time;
372                TweenProperty::Scale(cubic_spline(
373                    from,
374                    previous_tangent,
375                    to,
376                    next_tangent,
377                    amount,
378                ))
379            }
380            TweenProperty::MorphTargetWeights(from) => {
381                let from_out = from_out
382                    .as_morph_target_weights()
383                    .context(MismatchedPropertiesSnafu)?;
384                let to_in = to_in
385                    .as_morph_target_weights()
386                    .context(MismatchedPropertiesSnafu)?;
387                let to = to
388                    .as_morph_target_weights()
389                    .context(MismatchedPropertiesSnafu)?;
390
391                let weights = from
392                    .into_iter()
393                    .zip(from_out.iter().zip(to_in.iter().zip(to.iter())))
394                    .map(|(from, (from_out, (to_in, to)))| -> f32 {
395                        let previous_tangent = from_out * delta_time;
396                        let next_tangent = to_in * delta_time;
397                        cubic_spline(from, previous_tangent, *to, next_tangent, amount)
398                    });
399                TweenProperty::MorphTargetWeights(weights.collect())
400            }
401        }))
402    }
403
404    fn interpolate_linear(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
405        let last_keyframe = self.keyframes.len() - 1;
406        let last_time = self.keyframes[last_keyframe].0;
407        let time = time.min(last_time);
408        let (prev_keyframe_ndx, prev_keyframe) =
409            if let Some(prev) = self.get_previous_keyframe(time)? {
410                prev
411            } else {
412                return Ok(None);
413            };
414        let prev_time = prev_keyframe.0;
415
416        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {
417            next
418        } else {
419            return Ok(None);
420        };
421        let next_time = next_keyframe.0;
422
423        // UNWRAP: safe because we know this was found above
424        let from = self.properties.get(prev_keyframe_ndx).unwrap();
425
426        // UNWRAP: safe because we know this is either the first index or was
427        // found above
428        let to = self.properties.get(next_keyframe_ndx).unwrap();
429
430        let amount = (time - prev_time) / (next_time - prev_time);
431        Ok(Some(match from {
432            TweenProperty::Translation(a) => {
433                let b = to.as_translation().context(MismatchedPropertiesSnafu)?;
434                TweenProperty::Translation(a.lerp(*b, amount))
435            }
436            TweenProperty::Rotation(a) => {
437                let a = a.normalize();
438                let b = to
439                    .as_rotation()
440                    .context(MismatchedPropertiesSnafu)?
441                    .normalize();
442                TweenProperty::Rotation(a.slerp(b, amount))
443            }
444            TweenProperty::Scale(a) => {
445                let b = to.as_scale().context(MismatchedPropertiesSnafu)?;
446                TweenProperty::Scale(a.lerp(*b, amount))
447            }
448            TweenProperty::MorphTargetWeights(a) => {
449                let b = to
450                    .as_morph_target_weights()
451                    .context(MismatchedPropertiesSnafu)?;
452                TweenProperty::MorphTargetWeights(
453                    a.into_iter()
454                        .zip(b)
455                        .map(|(a, b)| a + (b - a) * amount)
456                        .collect(),
457                )
458            }
459        }))
460    }
461
462    pub fn length_in_seconds(&self) -> f32 {
463        if self.keyframes.is_empty() {
464            return 0.0;
465        }
466
467        let last_keyframe = self.keyframes.len() - 1;
468        self.keyframes[last_keyframe].0
469    }
470
471    pub fn get_first_keyframe_property(&self) -> Option<TweenProperty> {
472        match &self.properties {
473            TweenProperties::Translations(ts) => {
474                if self.interpolation.is_cubic_spline() {
475                    ts.get(1).copied().map(TweenProperty::Translation)
476                } else {
477                    ts.first().copied().map(TweenProperty::Translation)
478                }
479            }
480            TweenProperties::Rotations(rs) => {
481                if self.interpolation.is_cubic_spline() {
482                    rs.get(1).copied().map(TweenProperty::Rotation)
483                } else {
484                    rs.first().copied().map(TweenProperty::Rotation)
485                }
486            }
487            TweenProperties::Scales(ss) => {
488                if self.interpolation.is_cubic_spline() {
489                    ss.get(1).copied().map(TweenProperty::Scale)
490                } else {
491                    ss.first().copied().map(TweenProperty::Scale)
492                }
493            }
494            TweenProperties::MorphTargetWeights(ws) => {
495                if self.interpolation.is_cubic_spline() {
496                    ws.get(1).cloned().map(TweenProperty::MorphTargetWeights)
497                } else {
498                    ws.first().cloned().map(TweenProperty::MorphTargetWeights)
499                }
500            }
501        }
502    }
503
504    pub fn get_last_keyframe_property(&self) -> Option<TweenProperty> {
505        match &self.properties {
506            TweenProperties::Translations(ts) => {
507                if self.interpolation.is_cubic_spline() {
508                    let second_last = ts.len() - 2;
509                    ts.get(second_last).copied().map(TweenProperty::Translation)
510                } else {
511                    ts.last().copied().map(TweenProperty::Translation)
512                }
513            }
514            TweenProperties::Rotations(rs) => {
515                if self.interpolation.is_cubic_spline() {
516                    let second_last = rs.len() - 2;
517                    rs.get(second_last).copied().map(TweenProperty::Rotation)
518                } else {
519                    rs.last().copied().map(TweenProperty::Rotation)
520                }
521            }
522            TweenProperties::Scales(ss) => {
523                if self.interpolation.is_cubic_spline() {
524                    let second_last = ss.len() - 2;
525                    ss.get(second_last).copied().map(TweenProperty::Scale)
526                } else {
527                    ss.last().copied().map(TweenProperty::Scale)
528                }
529            }
530            TweenProperties::MorphTargetWeights(ws) => {
531                if self.interpolation.is_cubic_spline() {
532                    let second_last = ws.len() - 2;
533                    ws.get(second_last)
534                        .cloned()
535                        .map(TweenProperty::MorphTargetWeights)
536                } else {
537                    ws.last().cloned().map(TweenProperty::MorphTargetWeights)
538                }
539            }
540        }
541    }
542}
543
544#[derive(Clone, Debug)]
545pub struct AnimationNode {
546    pub transform: NestedTransform,
547    pub morph_weights: MorphTargetWeights,
548}
549
550impl From<&GltfNode> for (usize, AnimationNode) {
551    fn from(node: &GltfNode) -> Self {
552        (
553            node.index,
554            AnimationNode {
555                transform: node.transform.clone(),
556                morph_weights: node.weights.clone(),
557            },
558        )
559    }
560}
561
562#[derive(Debug, Snafu)]
563pub enum AnimationError {
564    #[snafu(display("Missing inputs"))]
565    MissingInputs,
566
567    #[snafu(display("Missing outputs"))]
568    MissingOutputs,
569}
570
571#[derive(Default, Debug, Clone)]
572pub struct Animation {
573    pub tweens: Vec<Tween>,
574    // The name of this animation, if any.
575    pub name: Option<String>,
576}
577
578impl Animation {
579    pub fn from_gltf(
580        buffer_data: &[gltf::buffer::Data],
581        animation: gltf::Animation,
582    ) -> Result<Self, AnimationError> {
583        let index = animation.index();
584        let name = animation.name().map(String::from);
585        log::trace!("  animation {index} {name:?}");
586        let mut r_animation = Animation {
587            name,
588            ..Default::default()
589        };
590        for (i, channel) in animation.channels().enumerate() {
591            log::trace!("  channel {i}");
592            let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
593            let inputs = reader.read_inputs().context(MissingInputsSnafu)?;
594            let outputs = reader.read_outputs().context(MissingOutputsSnafu)?;
595            let keyframes = inputs.map(Keyframe).collect::<Vec<_>>();
596            log::trace!("    with {} keyframes", keyframes.len());
597            let interpolation = channel.sampler().interpolation().into();
598            log::trace!("    using {interpolation} interpolation");
599            let index = channel.target().node().index();
600            let name = channel.target().node().name();
601            log::trace!("    of node {index} {name:?}");
602            let tween = Tween {
603                properties: match outputs {
604                    gltf::animation::util::ReadOutputs::Translations(ts) => {
605                        log::trace!("    tweens translations");
606                        TweenProperties::Translations(ts.map(Vec3::from).collect())
607                    }
608                    gltf::animation::util::ReadOutputs::Rotations(rs) => {
609                        log::trace!("    tweens rotations");
610                        TweenProperties::Rotations(rs.into_f32().map(Quat::from_array).collect())
611                    }
612                    gltf::animation::util::ReadOutputs::Scales(ss) => {
613                        log::trace!("    tweens scales");
614                        TweenProperties::Scales(ss.map(Vec3::from).collect())
615                    }
616                    gltf::animation::util::ReadOutputs::MorphTargetWeights(ws) => {
617                        log::trace!("    tweens morph target weights");
618                        let ws = ws.into_f32().collect::<Vec<_>>();
619                        let num_morph_targets = ws.len() / keyframes.len();
620                        log::trace!("      weights length  : {}", ws.len());
621                        log::trace!("      keyframes length: {}", keyframes.len());
622                        log::trace!("      morph targets   : {}", num_morph_targets);
623                        TweenProperties::MorphTargetWeights(
624                            ws.chunks_exact(num_morph_targets)
625                                .map(|chunk| chunk.to_vec())
626                                .collect(),
627                        )
628                    }
629                },
630                keyframes,
631                interpolation,
632                target_node_index: index,
633            };
634            r_animation.tweens.push(tween);
635        }
636
637        let total_time = r_animation.length_in_seconds();
638        log::trace!("  taking {total_time} seconds in total");
639        Ok(r_animation)
640    }
641
642    pub fn length_in_seconds(&self) -> f32 {
643        self.tweens
644            .iter()
645            .flat_map(|tween| tween.keyframes.iter().map(|k| k.0))
646            .max_by(f32::total_cmp)
647            .unwrap_or_default()
648    }
649
650    pub fn get_properties_at_time(
651        &self,
652        t: f32,
653    ) -> Result<Vec<(usize, TweenProperty)>, InterpolationError> {
654        let mut tweens = vec![];
655        for tween in self.tweens.iter() {
656            let prop = if let Some(prop) = tween.interpolate(t)? {
657                prop
658            } else if t >= tween.length_in_seconds() {
659                tween.get_last_keyframe_property().unwrap()
660            } else {
661                tween.get_first_keyframe_property().unwrap()
662            };
663            tweens.push((tween.target_node_index, prop));
664        }
665
666        Ok(tweens.into_iter().collect())
667    }
668
669    pub fn into_animator(
670        self,
671        nodes: impl IntoIterator<Item = (usize, AnimationNode)>,
672    ) -> Animator {
673        Animator::new(nodes, self)
674    }
675
676    pub fn target_node_indices(&self) -> impl Iterator<Item = usize> + '_ {
677        self.tweens.iter().map(|t| t.target_node_index)
678    }
679}
680
681/// Combines [`NestedTransform`] and [`Animation`] to progress an animation.
682///
683/// Applies animations to a list of `(usize, [NestedTransform])` and keeps track
684/// of how much time has elapsed.
685///
686/// To function without errors, the [`Animation`]'s tweens'
687/// [`Tween::target_node_index`] must point to the index of [`NestedTransform`].
688#[derive(Default, Debug, Clone)]
689pub struct Animator {
690    /// A time to use as the current amount of seconds elapsed in the running
691    /// of the current animation.
692    pub timestamp: f32,
693    /// All nodes under this animator's control.
694    pub nodes: rustc_hash::FxHashMap<usize, AnimationNode>,
695    /// The animation that will apply to the nodes.
696    pub animation: Animation,
697}
698
699impl Animator {
700    /// Create a new animator with the given nodes and animation.
701    pub fn new(
702        nodes: impl IntoIterator<Item = impl Into<(usize, AnimationNode)>>,
703        animation: Animation,
704    ) -> Self {
705        let nodes = nodes.into_iter().map(|n| n.into());
706        let nodes = rustc_hash::FxHashMap::from_iter(nodes);
707        Animator {
708            nodes,
709            animation,
710            ..Default::default()
711        }
712    }
713
714    /// Progress the animator's animation, applying any tweened properties to
715    /// the animator's nodes.
716    pub fn progress(&mut self, dt_seconds: f32) -> Result<(), InterpolationError> {
717        log::trace!(
718            "progressing '{}' {dt_seconds} seconds",
719            self.animation.name.as_deref().unwrap_or("")
720        );
721        let max_length_seconds = self.animation.length_in_seconds();
722        log::trace!("  total length: {max_length_seconds}s");
723        self.timestamp = (self.timestamp + dt_seconds) % max_length_seconds;
724        log::trace!("  current time: {}s", self.timestamp);
725        let properties = self.animation.get_properties_at_time(self.timestamp)?;
726        log::trace!("  {} properties", properties.len());
727        for (node_index, property) in properties.into_iter() {
728            log::trace!("    {node_index} {}", property.description());
729            // There's plenty of reasons why a node referenced by an animation
730            // might not exist in the animator's "nodes":
731            // * the node is not in this scene
732            // * business logic has removed it
733            // * ...and the beat goes on
734            // So we won't fret if we can't find it...
735            if let Some(node) = self.nodes.get(&node_index) {
736                match property {
737                    TweenProperty::Translation(translation) => {
738                        node.transform.set_local_translation(translation);
739                    }
740                    TweenProperty::Rotation(rotation) => {
741                        node.transform.set_local_rotation(rotation);
742                    }
743                    TweenProperty::Scale(scale) => {
744                        node.transform.set_local_scale(scale);
745                    }
746                    TweenProperty::MorphTargetWeights(new_weights) => {
747                        if node.morph_weights.array().is_empty() {
748                            log::error!(
749                                "animation is applied to morph targets but node {node_index} is \
750                                 missing weights"
751                            );
752                        } else {
753                            for (i, w) in new_weights.into_iter().enumerate() {
754                                node.morph_weights.set_item(i, w);
755                            }
756                        }
757                    }
758                }
759            } else {
760                log::warn!("missing node {node_index} in animation");
761            }
762        }
763        Ok(())
764    }
765}
766
767#[cfg(test)]
768mod test {
769    use crate::{context::Context, gltf::Animator, test::BlockOnFuture};
770    use glam::Vec3;
771
772    #[test]
773    fn gltf_simple_animation() {
774        let ctx = Context::headless(16, 16).block();
775        let stage = ctx
776            .new_stage()
777            .with_bloom(false)
778            .with_background_color(Vec3::ZERO.extend(1.0));
779        let projection = crate::camera::perspective(50.0, 50.0);
780        let view = crate::camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y);
781        let _camera = stage
782            .new_camera()
783            .with_projection_and_view(projection, view);
784
785        let doc = stage
786            .load_gltf_document_from_path("../../gltf/animated_triangle.gltf")
787            .unwrap();
788
789        let nodes = doc
790            .recursive_nodes_in_scene(doc.default_scene.unwrap_or_default())
791            .collect::<Vec<_>>();
792
793        let mut animator = Animator::new(nodes, doc.animations.first().unwrap().clone());
794        log::info!("animator: {animator:#?}");
795
796        let frame = ctx.get_next_frame().unwrap();
797        stage.render(&frame.view());
798        let img = frame.read_image().block().unwrap();
799        img_diff::save("animation/triangle.png", img);
800        frame.present();
801
802        let dt = 1.0 / 8.0;
803        for i in 1..=10 {
804            animator.progress(dt).unwrap();
805            let frame = ctx.get_next_frame().unwrap();
806            stage.render(&frame.view());
807            let img = frame.read_image().block().unwrap();
808            img_diff::save(format!("animation/triangle{i}.png"), img);
809            frame.present();
810        }
811    }
812}