mirror of
https://github.com/storytold/bevy-mocap.git
synced 2026-10-09 00:09:45 +00:00
generate animation clip
This commit is contained in:
+127
-7
@@ -1,9 +1,15 @@
|
||||
use std::{num::ParseFloatError, str::FromStr};
|
||||
|
||||
|
||||
use bevy::{
|
||||
animation::{AnimationClip, EntityPath, Keyframes, VariableCurve},
|
||||
asset::{Asset, AssetLoader, AsyncReadExt},
|
||||
reflect::Reflect,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Reflect, Default,
|
||||
)]
|
||||
pub struct Timestamp {
|
||||
hour: u8,
|
||||
minute: u8,
|
||||
@@ -83,13 +89,13 @@ impl FromStr for Timestamp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Reflect)]
|
||||
pub struct CSVFrame {
|
||||
pub timestamp: Timestamp,
|
||||
pub morphs: Vec<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Reflect, Asset)]
|
||||
pub struct CSVAnimation {
|
||||
pub frames: Vec<CSVFrame>,
|
||||
pub morph_names: Vec<String>,
|
||||
@@ -196,9 +202,87 @@ impl CSVAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CSVAnimation> for VariableCurve {
|
||||
fn from(value: CSVAnimation) -> Self {
|
||||
let mut keyframe_timestamps = Vec::with_capacity(value.frames.len());
|
||||
let mut keyframes = Vec::with_capacity(value.frames.len() * value.morph_names.len());
|
||||
|
||||
let seconds_per_frame = 1.0 / (value.fps as f32);
|
||||
for (frame, data) in value.frames.iter().enumerate() {
|
||||
let time = (frame as f32) * seconds_per_frame;
|
||||
let mut morphs = data.morphs.clone();
|
||||
keyframe_timestamps.push(time);
|
||||
keyframes.append(&mut morphs);
|
||||
}
|
||||
|
||||
Self {
|
||||
keyframe_timestamps,
|
||||
keyframes: Keyframes::Weights(keyframes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CSVAnimation> for AnimationClip {
|
||||
fn from(val: CSVAnimation) -> Self {
|
||||
let mut clip = Self::default();
|
||||
clip.add_curve_to_path(EntityPath::default(), val.into());
|
||||
|
||||
clip
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CSVAnimationLoader;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum CSVAnimationLoaderError {
|
||||
Io(std::io::Error),
|
||||
CSVParseError(CSVParseError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CSVAnimationLoaderError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{self:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CSVAnimationLoaderError {}
|
||||
|
||||
impl AssetLoader for CSVAnimationLoader {
|
||||
type Asset = AnimationClip;
|
||||
|
||||
type Settings = ();
|
||||
|
||||
type Error = CSVAnimationLoaderError;
|
||||
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut bevy::asset::io::Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
_load_context: &'a mut bevy::asset::LoadContext,
|
||||
) -> bevy::utils::BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let mut csv = String::new();
|
||||
reader
|
||||
.read_to_string(&mut csv)
|
||||
.await
|
||||
.map_err(CSVAnimationLoaderError::Io)?;
|
||||
let csv_animation =
|
||||
CSVAnimation::parse(&csv).map_err(CSVAnimationLoaderError::CSVParseError)?;
|
||||
Ok(csv_animation.into())
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
&["morph_anim.csv"]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
use bevy::animation::{AnimationClip, EntityPath, Keyframes};
|
||||
|
||||
use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError};
|
||||
#[test]
|
||||
@@ -324,7 +408,43 @@ mod tests {
|
||||
00:00:00:00.00, 0, 0.1, 0.5",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(&result.frames.first().unwrap().morphs[0] - 0.1 < 0.0001);
|
||||
assert!(&result.frames.first().unwrap().morphs[1] - 0.5 < 0.0001);
|
||||
assert!((&result.frames.first().unwrap().morphs[0] - 0.1).abs() < 0.0001);
|
||||
assert!((&result.frames.first().unwrap().morphs[1] - 0.5).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_convert_an_animation_into_an_animation_clip() {
|
||||
let animation = CSVAnimation::parse(
|
||||
"timecode, blendshapecount, shape_1, shape_2
|
||||
00:00:00:00.00, 0, 1.0, 0
|
||||
00:00:00:01.00, 0, 0, -0.4
|
||||
00:00:00:02.00, 0, 0, 0
|
||||
00:00:01:00.00, 0, 0, 0
|
||||
00:00:01:01.00, 0, 0, 0",
|
||||
)
|
||||
.unwrap();
|
||||
let clip: AnimationClip = animation.into();
|
||||
assert!(clip.duration() - 1.33333 < 0.1);
|
||||
let curves = clip
|
||||
.get_curves_by_path(&EntityPath::default())
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
assert_eq!(curves.keyframe_timestamps.len(), 5);
|
||||
assert!((curves.keyframe_timestamps[0] - 0.0).abs() < 0.001);
|
||||
assert!((curves.keyframe_timestamps[1] - 0.33333).abs() < 0.001);
|
||||
assert!((curves.keyframe_timestamps[2] - 0.66666).abs() < 0.01);
|
||||
assert!((curves.keyframe_timestamps[3] - 1.0).abs() < 0.01);
|
||||
assert!((curves.keyframe_timestamps[4] - 1.33333).abs() < 0.01);
|
||||
|
||||
let Keyframes::Weights(keyframes) = &curves.keyframes else {
|
||||
panic!("Keyframes aren't weights")
|
||||
};
|
||||
|
||||
assert_eq!(keyframes.len(), 10);
|
||||
assert!((keyframes[0] - 1.0).abs() < 0.0001);
|
||||
assert!((keyframes[1] - 0.0).abs() < 0.0001);
|
||||
assert!((keyframes[2] - 0.0).abs() < 0.0001);
|
||||
assert!((keyframes[3] + 0.4).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user