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            .filter(|(_, keyframe)| keyframe.0 <= time)
253            .next_back())
254    }
255
256    fn get_next_keyframe(
257        &self,
258        time: f32,
259    ) -> Result<Option<(usize, &Keyframe)>, InterpolationError> {
260        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);
261        Ok(self
262            .keyframes
263            .iter()
264            .enumerate()
265            .find(|(_, keyframe)| keyframe.0 > time))
266    }
267
268    fn interpolate_step(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
269        if let Some((prev_keyframe_ndx, _)) = self.get_previous_keyframe(time)? {
270            self.properties
271                .get(prev_keyframe_ndx)
272                .context(MissingPropertyIndexSnafu {
273                    index: prev_keyframe_ndx,
274                })
275                .map(Some)
276        } else {
277            Ok(None)
278        }
279    }
280
281    fn interpolate_cubic(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
282        snafu::ensure!(self.keyframes.len() >= 2, NotEnoughKeyframesSnafu);
283
284        let (prev_keyframe_ndx, prev_keyframe) =
285            if let Some(prev) = self.get_previous_keyframe(time)? {
286                prev
287            } else {
288                return Ok(None);
289            };
290        let prev_time = prev_keyframe.0;
291
292        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {
293            next
294        } else {
295            return Ok(None);
296        };
297        let next_time = next_keyframe.0;
298
299        // UNWRAP: safe because we know this was found above
300        let [_, from, from_out] =
301            self.properties
302                .get_cubic(prev_keyframe_ndx)
303                .context(MissingPropertyIndexSnafu {
304                    index: prev_keyframe_ndx,
305                })?;
306        // UNWRAP: safe because we know this is either the first index or was found
307        // above
308        let [to_in, to, _] =
309            self.properties
310                .get_cubic(next_keyframe_ndx)
311                .context(MissingPropertyIndexSnafu {
312                    index: next_keyframe_ndx,
313                })?;
314
315        let delta_time = next_time - prev_time;
316        let amount = (time - prev_time) / (next_time - prev_time);
317
318        fn cubic_spline<T>(
319            previous_point: T,
320            previous_tangent: T,
321            next_point: T,
322            next_tangent: T,
323            t: f32,
324        ) -> T
325        where
326            T: std::ops::Mul<f32, Output = T> + std::ops::Add<Output = T>,
327        {
328            let t2 = t * t;
329            let t3 = t2 * t;
330            previous_point * (2.0 * t3 - 3.0 * t2 + 1.0)
331                + previous_tangent * (t3 - 2.0 * t2 + t)
332                + next_point * (-2.0 * t3 + 3.0 * t2)
333                + next_tangent * (t3 - t2)
334        }
335
336        Ok(Some(match from {
337            TweenProperty::Translation(from) => {
338                let from_out = *from_out
339                    .as_translation()
340                    .context(MismatchedPropertiesSnafu)?;
341                let to_in = *to_in.as_translation().context(MismatchedPropertiesSnafu)?;
342                let to = *to.as_translation().context(MismatchedPropertiesSnafu)?;
343                let previous_tangent = delta_time * from_out;
344                let next_tangent = delta_time * to_in;
345                TweenProperty::Translation(cubic_spline(
346                    from,
347                    previous_tangent,
348                    to,
349                    next_tangent,
350                    amount,
351                ))
352            }
353            TweenProperty::Rotation(from) => {
354                let from_out = *from_out.as_rotation().context(MismatchedPropertiesSnafu)?;
355                let to_in = *to_in.as_rotation().context(MismatchedPropertiesSnafu)?;
356                let to = *to.as_rotation().context(MismatchedPropertiesSnafu)?;
357                let previous_tangent = from_out * delta_time;
358                let next_tangent = to_in * delta_time;
359                TweenProperty::Rotation(cubic_spline(
360                    from,
361                    previous_tangent,
362                    to,
363                    next_tangent,
364                    amount,
365                ))
366            }
367            TweenProperty::Scale(from) => {
368                let from_out = *from_out.as_scale().context(MismatchedPropertiesSnafu)?;
369                let to_in = *to_in.as_scale().context(MismatchedPropertiesSnafu)?;
370                let to = *to.as_scale().context(MismatchedPropertiesSnafu)?;
371                let previous_tangent = from_out * delta_time;
372                let next_tangent = to_in * delta_time;
373                TweenProperty::Scale(cubic_spline(
374                    from,
375                    previous_tangent,
376                    to,
377                    next_tangent,
378                    amount,
379                ))
380            }
381            TweenProperty::MorphTargetWeights(from) => {
382                let from_out = from_out
383                    .as_morph_target_weights()
384                    .context(MismatchedPropertiesSnafu)?;
385                let to_in = to_in
386                    .as_morph_target_weights()
387                    .context(MismatchedPropertiesSnafu)?;
388                let to = to
389                    .as_morph_target_weights()
390                    .context(MismatchedPropertiesSnafu)?;
391
392                let weights = from
393                    .into_iter()
394                    .zip(from_out.iter().zip(to_in.iter().zip(to.iter())))
395                    .map(|(from, (from_out, (to_in, to)))| -> f32 {
396                        let previous_tangent = from_out * delta_time;
397                        let next_tangent = to_in * delta_time;
398                        cubic_spline(from, previous_tangent, *to, next_tangent, amount)
399                    });
400                TweenProperty::MorphTargetWeights(weights.collect())
401            }
402        }))
403    }
404
405    fn interpolate_linear(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {
406        let last_keyframe = self.keyframes.len() - 1;
407        let last_time = self.keyframes[last_keyframe].0;
408        let time = time.min(last_time);
409        let (prev_keyframe_ndx, prev_keyframe) =
410            if let Some(prev) = self.get_previous_keyframe(time)? {
411                prev
412            } else {
413                return Ok(None);
414            };
415        let prev_time = prev_keyframe.0;
416
417        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {
418            next
419        } else {
420            return Ok(None);
421        };
422        let next_time = next_keyframe.0;
423
424        // UNWRAP: safe because we know this was found above
425        let from = self.properties.get(prev_keyframe_ndx).unwrap();
426
427        // UNWRAP: safe because we know this is either the first index or was found
428        // above
429        let to = self.properties.get(next_keyframe_ndx).unwrap();
430
431        let amount = (time - prev_time) / (next_time - prev_time);
432        Ok(Some(match from {
433            TweenProperty::Translation(a) => {
434                let b = to.as_translation().context(MismatchedPropertiesSnafu)?;
435                TweenProperty::Translation(a.lerp(*b, amount))
436            }
437            TweenProperty::Rotation(a) => {
438                let a = a.normalize();
439                let b = to
440                    .as_rotation()
441                    .context(MismatchedPropertiesSnafu)?
442                    .normalize();
443                TweenProperty::Rotation(a.slerp(b, amount))
444            }
445            TweenProperty::Scale(a) => {
446                let b = to.as_scale().context(MismatchedPropertiesSnafu)?;
447                TweenProperty::Scale(a.lerp(*b, amount))
448            }
449            TweenProperty::MorphTargetWeights(a) => {
450                let b = to
451                    .as_morph_target_weights()
452                    .context(MismatchedPropertiesSnafu)?;
453                TweenProperty::MorphTargetWeights(
454                    a.into_iter()
455                        .zip(b)
456                        .map(|(a, b)| a + (b - a) * amount)
457                        .collect(),
458                )
459            }
460        }))
461    }
462
463    pub fn length_in_seconds(&self) -> f32 {
464        if self.keyframes.is_empty() {
465            return 0.0;
466        }
467
468        let last_keyframe = self.keyframes.len() - 1;
469        self.keyframes[last_keyframe].0
470    }
471
472    pub fn get_first_keyframe_property(&self) -> Option<TweenProperty> {
473        match &self.properties {
474            TweenProperties::Translations(ts) => {
475                if self.interpolation.is_cubic_spline() {
476                    ts.get(1).copied().map(TweenProperty::Translation)
477                } else {
478                    ts.first().copied().map(TweenProperty::Translation)
479                }
480            }
481            TweenProperties::Rotations(rs) => {
482                if self.interpolation.is_cubic_spline() {
483                    rs.get(1).copied().map(TweenProperty::Rotation)
484                } else {
485                    rs.first().copied().map(TweenProperty::Rotation)
486                }
487            }
488            TweenProperties::Scales(ss) => {
489                if self.interpolation.is_cubic_spline() {
490                    ss.get(1).copied().map(TweenProperty::Scale)
491                } else {
492                    ss.first().copied().map(TweenProperty::Scale)
493                }
494            }
495            TweenProperties::MorphTargetWeights(ws) => {
496                if self.interpolation.is_cubic_spline() {
497                    ws.get(1).cloned().map(TweenProperty::MorphTargetWeights)
498                } else {
499                    ws.first().cloned().map(TweenProperty::MorphTargetWeights)
500                }
501            }
502        }
503    }
504
505    pub fn get_last_keyframe_property(&self) -> Option<TweenProperty> {
506        match &self.properties {
507            TweenProperties::Translations(ts) => {
508                if self.interpolation.is_cubic_spline() {
509                    let second_last = ts.len() - 2;
510                    ts.get(second_last).copied().map(TweenProperty::Translation)
511                } else {
512                    ts.last().copied().map(TweenProperty::Translation)
513                }
514            }
515            TweenProperties::Rotations(rs) => {
516                if self.interpolation.is_cubic_spline() {
517                    let second_last = rs.len() - 2;
518                    rs.get(second_last).copied().map(TweenProperty::Rotation)
519                } else {
520                    rs.last().copied().map(TweenProperty::Rotation)
521                }
522            }
523            TweenProperties::Scales(ss) => {
524                if self.interpolation.is_cubic_spline() {
525                    let second_last = ss.len() - 2;
526                    ss.get(second_last).copied().map(TweenProperty::Scale)
527                } else {
528                    ss.last().copied().map(TweenProperty::Scale)
529                }
530            }
531            TweenProperties::MorphTargetWeights(ws) => {
532                if self.interpolation.is_cubic_spline() {
533                    let second_last = ws.len() - 2;
534                    ws.get(second_last)
535                        .cloned()
536                        .map(TweenProperty::MorphTargetWeights)
537                } else {
538                    ws.last().cloned().map(TweenProperty::MorphTargetWeights)
539                }
540            }
541        }
542    }
543}
544
545#[derive(Clone, Debug)]
546pub struct AnimationNode {
547    pub transform: NestedTransform,
548    pub morph_weights: MorphTargetWeights,
549}
550
551impl From<&GltfNode> for (usize, AnimationNode) {
552    fn from(node: &GltfNode) -> Self {
553        (
554            node.index,
555            AnimationNode {
556                transform: node.transform.clone(),
557                morph_weights: node.weights.clone(),
558            },
559        )
560    }
561}
562
563#[derive(Debug, Snafu)]
564pub enum AnimationError {
565    #[snafu(display("Missing inputs"))]
566    MissingInputs,
567
568    #[snafu(display("Missing outputs"))]
569    MissingOutputs,
570}
571
572#[derive(Default, Debug, Clone)]
573pub struct Animation {
574    pub tweens: Vec<Tween>,
575    // The name of this animation, if any.
576    pub name: Option<String>,
577}
578
579impl Animation {
580    pub fn from_gltf(
581        buffer_data: &[gltf::buffer::Data],
582        animation: gltf::Animation,
583    ) -> Result<Self, AnimationError> {
584        let index = animation.index();
585        let name = animation.name().map(String::from);
586        log::trace!("  animation {index} {name:?}");
587        let mut r_animation = Animation {
588            name,
589            ..Default::default()
590        };
591        for (i, channel) in animation.channels().enumerate() {
592            log::trace!("  channel {i}");
593            let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
594            let inputs = reader.read_inputs().context(MissingInputsSnafu)?;
595            let outputs = reader.read_outputs().context(MissingOutputsSnafu)?;
596            let keyframes = inputs.map(Keyframe).collect::<Vec<_>>();
597            log::trace!("    with {} keyframes", keyframes.len());
598            let interpolation = channel.sampler().interpolation().into();
599            log::trace!("    using {interpolation} interpolation");
600            let index = channel.target().node().index();
601            let name = channel.target().node().name();
602            log::trace!("    of node {index} {name:?}");
603            let tween = Tween {
604                properties: match outputs {
605                    gltf::animation::util::ReadOutputs::Translations(ts) => {
606                        log::trace!("    tweens translations");
607                        TweenProperties::Translations(ts.map(Vec3::from).collect())
608                    }
609                    gltf::animation::util::ReadOutputs::Rotations(rs) => {
610                        log::trace!("    tweens rotations");
611                        TweenProperties::Rotations(rs.into_f32().map(Quat::from_array).collect())
612                    }
613                    gltf::animation::util::ReadOutputs::Scales(ss) => {
614                        log::trace!("    tweens scales");
615                        TweenProperties::Scales(ss.map(Vec3::from).collect())
616                    }
617                    gltf::animation::util::ReadOutputs::MorphTargetWeights(ws) => {
618                        log::trace!("    tweens morph target weights");
619                        let ws = ws.into_f32().collect::<Vec<_>>();
620                        let num_morph_targets = ws.len() / keyframes.len();
621                        log::trace!("      weights length  : {}", ws.len());
622                        log::trace!("      keyframes length: {}", keyframes.len());
623                        log::trace!("      morph targets   : {}", num_morph_targets);
624                        TweenProperties::MorphTargetWeights(
625                            ws.chunks_exact(num_morph_targets)
626                                .map(|chunk| chunk.to_vec())
627                                .collect(),
628                        )
629                    }
630                },
631                keyframes,
632                interpolation,
633                target_node_index: index,
634            };
635            r_animation.tweens.push(tween);
636        }
637
638        let total_time = r_animation.length_in_seconds();
639        log::trace!("  taking {total_time} seconds in total");
640        Ok(r_animation)
641    }
642
643    pub fn length_in_seconds(&self) -> f32 {
644        self.tweens
645            .iter()
646            .flat_map(|tween| tween.keyframes.iter().map(|k| k.0))
647            .max_by(f32::total_cmp)
648            .unwrap_or_default()
649    }
650
651    pub fn get_properties_at_time(
652        &self,
653        t: f32,
654    ) -> Result<Vec<(usize, TweenProperty)>, InterpolationError> {
655        let mut tweens = vec![];
656        for tween in self.tweens.iter() {
657            let prop = if let Some(prop) = tween.interpolate(t)? {
658                prop
659            } else if t >= tween.length_in_seconds() {
660                tween.get_last_keyframe_property().unwrap()
661            } else {
662                tween.get_first_keyframe_property().unwrap()
663            };
664            tweens.push((tween.target_node_index, prop));
665        }
666
667        Ok(tweens.into_iter().collect())
668    }
669
670    pub fn into_animator(
671        self,
672        nodes: impl IntoIterator<Item = (usize, AnimationNode)>,
673    ) -> Animator {
674        Animator::new(nodes, self)
675    }
676
677    pub fn target_node_indices(&self) -> impl Iterator<Item = usize> + '_ {
678        self.tweens.iter().map(|t| t.target_node_index)
679    }
680}
681
682/// Combines [`NestedTransform`] and [`Animation`] to progress an animation.
683///
684/// Applies animations to a list of `(usize, [NestedTransform])` and keeps track
685/// of how much time has elapsed.
686///
687/// To function without errors, the [`Animation`]'s tweens'
688/// [`Tween::target_node_index`] must point to the index of [`NestedTransform`].
689#[derive(Default, Debug, Clone)]
690pub struct Animator {
691    /// A time to use as the current amount of seconds elapsed in the running
692    /// of the current animation.
693    pub timestamp: f32,
694    /// All nodes under this animator's control.
695    pub nodes: rustc_hash::FxHashMap<usize, AnimationNode>,
696    /// The animation that will apply to the nodes.
697    pub animation: Animation,
698}
699
700impl Animator {
701    /// Create a new animator with the given nodes and animation.
702    pub fn new(
703        nodes: impl IntoIterator<Item = impl Into<(usize, AnimationNode)>>,
704        animation: Animation,
705    ) -> Self {
706        let nodes = nodes.into_iter().map(|n| n.into());
707        let nodes = rustc_hash::FxHashMap::from_iter(nodes);
708        Animator {
709            nodes,
710            animation,
711            ..Default::default()
712        }
713    }
714
715    /// Progress the animator's animation, applying any tweened properties to
716    /// the animator's nodes.
717    pub fn progress(&mut self, dt_seconds: f32) -> Result<(), InterpolationError> {
718        log::trace!(
719            "progressing '{}' {dt_seconds} seconds",
720            self.animation.name.as_deref().unwrap_or("")
721        );
722        let max_length_seconds = self.animation.length_in_seconds();
723        log::trace!("  total length: {max_length_seconds}s");
724        self.timestamp = (self.timestamp + dt_seconds) % max_length_seconds;
725        log::trace!("  current time: {}s", self.timestamp);
726        let properties = self.animation.get_properties_at_time(self.timestamp)?;
727        log::trace!("  {} properties", properties.len());
728        for (node_index, property) in properties.into_iter() {
729            log::trace!("    {node_index} {}", property.description());
730            // There's plenty of reasons why a node referenced by an animation might not
731            // exist in the animator's "nodes":
732            // * the node is not in this scene
733            // * business logic has removed it
734            // * ...and the beat goes on
735            // So we won't fret if we can't find it...
736            if let Some(node) = self.nodes.get(&node_index) {
737                match property {
738                    TweenProperty::Translation(translation) => {
739                        node.transform.set_local_translation(translation);
740                    }
741                    TweenProperty::Rotation(rotation) => {
742                        node.transform.set_local_rotation(rotation);
743                    }
744                    TweenProperty::Scale(scale) => {
745                        node.transform.set_local_scale(scale);
746                    }
747                    TweenProperty::MorphTargetWeights(new_weights) => {
748                        if node.morph_weights.array().is_empty() {
749                            log::error!("animation is applied to morph targets but node {node_index} is missing weights");
750                        } else {
751                            for (i, w) in new_weights.into_iter().enumerate() {
752                                node.morph_weights.set_item(i, w);
753                            }
754                        }
755                    }
756                }
757            } else {
758                log::warn!("missing node {node_index} in animation");
759            }
760        }
761        Ok(())
762    }
763}
764
765#[cfg(test)]
766mod test {
767    use crate::{context::Context, gltf::Animator, test::BlockOnFuture};
768    use glam::Vec3;
769
770    #[test]
771    fn gltf_simple_animation() {
772        let ctx = Context::headless(16, 16).block();
773        let stage = ctx
774            .new_stage()
775            .with_bloom(false)
776            .with_background_color(Vec3::ZERO.extend(1.0));
777        let projection = crate::camera::perspective(50.0, 50.0);
778        let view = crate::camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y);
779        let _camera = stage
780            .new_camera()
781            .with_projection_and_view(projection, view);
782
783        let doc = stage
784            .load_gltf_document_from_path("../../gltf/animated_triangle.gltf")
785            .unwrap();
786
787        let nodes = doc
788            .nodes_in_scene(doc.default_scene.unwrap_or_default())
789            .collect::<Vec<_>>();
790
791        let mut animator = Animator::new(nodes, doc.animations.first().unwrap().clone());
792        log::info!("animator: {animator:#?}");
793
794        let frame = ctx.get_next_frame().unwrap();
795        stage.render(&frame.view());
796        let img = frame.read_image().block().unwrap();
797        img_diff::save("animation/triangle.png", img);
798        frame.present();
799
800        let dt = 1.0 / 8.0;
801        for i in 1..=10 {
802            animator.progress(dt).unwrap();
803            let frame = ctx.get_next_frame().unwrap();
804            stage.render(&frame.view());
805            let img = frame.read_image().block().unwrap();
806            img_diff::save(format!("animation/triangle{i}.png"), img);
807            frame.present();
808        }
809    }
810}