use alloc::string::String;
use alloc::vec::Vec;
use crate::ecs::asset_id::AssetId;
use crate::ecs::{SkinnedMeshHandle, de_opt_skinned_mesh_handle};
use crate::gfx::skeleton::{self as skinning, JointPose};
#[derive(Debug, Clone)]
pub struct Keyframe {
pub time: f32,
pub pose: JointPose,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct KeyframeFlat {
time: f32,
#[serde(flatten)]
pose: JointPose,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct KeyframePlain {
time: f32,
pose: JointPose,
}
impl serde::Serialize for Keyframe {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
KeyframeFlat {
time: self.time,
pose: self.pose,
}
.serialize(s)
} else {
KeyframePlain {
time: self.time,
pose: self.pose,
}
.serialize(s)
}
}
}
impl<'de> serde::Deserialize<'de> for Keyframe {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
if d.is_human_readable() {
let k = KeyframeFlat::deserialize(d)?;
Ok(Self {
time: k.time,
pose: k.pose,
})
} else {
let k = KeyframePlain::deserialize(d)?;
Ok(Self {
time: k.time,
pose: k.pose,
})
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AnimationTrack {
pub joint: usize,
pub keyframes: Vec<Keyframe>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Animation {
#[serde(skip)]
pub asset_id: AssetId,
#[serde(deserialize_with = "de_opt_skinned_mesh_handle")]
pub target: Option<SkinnedMeshHandle>,
pub source: String,
pub animation_index: u32,
pub animation_name: String,
pub sample_rate: f32,
pub duration: f32,
pub looping: bool,
pub weight: f32,
pub fade_in_secs: f32,
pub root_motion: bool,
pub root_motion_y: bool,
pub root_track: Vec<crate::gfx::root_motion::RootKey>,
pub tracks: Vec<AnimationTrack>,
pub morph_track: Vec<MorphKey>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct MorphKey {
pub time: f32,
pub weights: Vec<f32>,
}
impl Default for Animation {
fn default() -> Self {
Self {
asset_id: AssetId::default(),
target: None,
source: String::new(),
animation_index: 0,
animation_name: String::new(),
sample_rate: 30.0,
duration: 1.0,
looping: true,
weight: 1.0,
fade_in_secs: 0.0,
root_motion: false,
root_motion_y: false,
root_track: Vec::new(),
tracks: Vec::new(),
morph_track: Vec::new(),
}
}
}
impl Animation {
pub fn to_clip(&self) -> skinning::AnimationClip {
skinning::AnimationClip {
duration: self.duration.max(1e-3),
looping: self.looping,
tracks: self
.tracks
.iter()
.map(|t| skinning::JointTrack {
joint: t.joint,
keys: t
.keyframes
.iter()
.map(|k| skinning::Keyframe {
time: k.time,
pose: k.pose,
})
.collect(),
})
.collect(),
morph_keys: self
.morph_track
.iter()
.map(|k| (k.time, k.weights.clone()))
.collect(),
root: (!self.root_track.is_empty()).then(|| crate::gfx::root_motion::RootTrack {
keys: self.root_track.clone(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialises_with_defaults() {
let a: Animation = serde_json::from_str("{}").unwrap();
assert_eq!(a.duration, 1.0);
assert!(a.looping);
assert_eq!(a.weight, 1.0);
assert!(a.tracks.is_empty());
assert_eq!(a.source, "");
assert_eq!(a.animation_index, 0);
assert_eq!(a.animation_name, "");
}
#[test]
fn deserialises_glb_source_fields() {
crate::test_support::reset_interner();
let json = r#"{
"target":"hero",
"source":"models/hero.glb",
"animation_index":2,
"animation_name":"Walk",
"looping":false
}"#;
let a: Animation = serde_json::from_str(json).unwrap();
assert_eq!(a.source, "models/hero.glb");
assert_eq!(a.animation_index, 2);
assert_eq!(a.animation_name, "Walk");
assert!(!a.looping);
}
#[test]
fn deserialises_inline_tracks() {
crate::test_support::reset_interner();
let json = r#"{
"target":"flag",
"duration":2.0,
"tracks":[{"joint":0,"keyframes":[{"time":0.0,"rotation_deg":[0,30,0]}]}]
}"#;
let a: Animation = serde_json::from_str(json).unwrap();
assert_eq!(a.duration, 2.0);
assert_eq!(a.tracks.len(), 1);
assert_eq!(a.tracks[0].joint, 0);
}
#[test]
fn to_clip_floors_duration_so_runtime_loop_does_not_divide_by_zero() {
let a = Animation {
duration: 0.0,
..Default::default()
};
let clip = a.to_clip();
assert!(clip.duration >= 1e-3);
}
}