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 .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 let [_, from, from_out] =
301 self.properties
302 .get_cubic(prev_keyframe_ndx)
303 .context(MissingPropertyIndexSnafu {
304 index: prev_keyframe_ndx,
305 })?;
306 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 let from = self.properties.get(prev_keyframe_ndx).unwrap();
426
427 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 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#[derive(Default, Debug, Clone)]
690pub struct Animator {
691 pub timestamp: f32,
694 pub nodes: rustc_hash::FxHashMap<usize, AnimationNode>,
696 pub animation: Animation,
698}
699
700impl Animator {
701 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 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 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}