1use 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#[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 pub keyframes: Vec<Keyframe>,
206 pub properties: TweenProperties,
208 pub interpolation: Interpolation,
210 pub target_node_index: usize,
212}
213
214impl Tween {
215 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 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 let [_, from, from_out] =
300 self.properties
301 .get_cubic(prev_keyframe_ndx)
302 .context(MissingPropertyIndexSnafu {
303 index: prev_keyframe_ndx,
304 })?;
305 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 let from = self.properties.get(prev_keyframe_ndx).unwrap();
425
426 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 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#[derive(Default, Debug, Clone)]
689pub struct Animator {
690 pub timestamp: f32,
693 pub nodes: rustc_hash::FxHashMap<usize, AnimationNode>,
695 pub animation: Animation,
697}
698
699impl Animator {
700 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 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 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}