mirror of
https://github.com/storytold/bevy-mocap.git
synced 2026-10-09 00:09:45 +00:00
added bone support
This commit is contained in:
@@ -11,6 +11,7 @@ bevy = { version = "0.12", default-features = false, features = ["bevy_pbr", "be
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
csv = "1.3"
|
||||
serde_json = "1"
|
||||
itertools = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
bevy = { version = "0.12", default-features = true }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+544
-549
File diff suppressed because it is too large
Load Diff
@@ -50,5 +50,18 @@
|
||||
["cheekSquintRight", "CheekSquintRight"],
|
||||
["noseSneerLeft", "NoseSneerLeft"],
|
||||
["noseSneerRight", "NoseSneerRight"],
|
||||
["tongueOut", "TongueOut"]
|
||||
["tongueOut", "TongueOut"],
|
||||
[{
|
||||
"path": ["pascal_Head"],
|
||||
"axis": "Y"
|
||||
}, "HeadYaw"],
|
||||
[{
|
||||
"path": ["pascal_Head"],
|
||||
"axis": "X",
|
||||
"offset": -45
|
||||
}, "HeadPitch"],
|
||||
[{
|
||||
"path": ["pascal_Head"],
|
||||
"axis": "Z"
|
||||
}, "HeadRoll"]
|
||||
]
|
||||
+197
-29
@@ -1,14 +1,9 @@
|
||||
use std::{num::ParseFloatError, path::PathBuf, str::FromStr, string::FromUtf8Error};
|
||||
|
||||
use bevy::{
|
||||
animation::{AnimationClip, EntityPath, Keyframes, VariableCurve},
|
||||
app::Plugin,
|
||||
asset::{Asset, AssetApp, AssetLoader, AsyncReadExt, ReadAssetBytesError},
|
||||
core::Name,
|
||||
log::info,
|
||||
reflect::Reflect,
|
||||
utils::hashbrown::HashMap,
|
||||
animation::{AnimationClip, EntityPath, Keyframes, VariableCurve}, app::Plugin, asset::{Asset, AssetApp, AssetLoader, AsyncReadExt, ReadAssetBytesError}, core::Name, log::info, math::{Quat, Vec3}, reflect::Reflect, transform::components::Transform, utils::hashbrown::HashMap
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
@@ -210,34 +205,112 @@ impl CSVAnimation {
|
||||
fn generate_clip(
|
||||
self,
|
||||
name: &str,
|
||||
mapping: Option<&[(impl ToString, impl ToString)]>,
|
||||
mapping: Option<&CSVAnimationMapping>,
|
||||
) -> AnimationClip {
|
||||
let path = EntityPath {
|
||||
parts: vec![Name::new(name.to_owned())],
|
||||
};
|
||||
let mut clip = AnimationClip::default();
|
||||
clip.add_curve_to_path(path, self.generate_animation_curve(mapping));
|
||||
|
||||
clip
|
||||
}
|
||||
|
||||
fn generate_animation_curve(
|
||||
self,
|
||||
mapping: Option<&[(impl ToString, impl ToString)]>,
|
||||
) -> VariableCurve {
|
||||
|
||||
let csv_morph_order: HashMap<&str, usize> = self
|
||||
.morph_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, val)| (val.as_str(), i))
|
||||
.collect();
|
||||
|
||||
clip.add_curve_to_path(path, self.generate_morph_curve(mapping, &csv_morph_order));
|
||||
|
||||
if let Some(mapping) = mapping {
|
||||
self.generate_bone_curves(&mut clip, mapping, &csv_morph_order);
|
||||
}
|
||||
|
||||
clip
|
||||
}
|
||||
|
||||
fn generate_bone_curves(&self, clip: &mut AnimationClip, mapping: &CSVAnimationMapping, csv_morph_order: &HashMap<&str, usize>) {
|
||||
let bones = mapping.0.iter().filter_map(|(k, v)| {
|
||||
match k {
|
||||
MappingType::Morph(_) => None,
|
||||
MappingType::Bone(mapping) => Some((mapping.path.clone(), (mapping.clone(), v.clone()))),
|
||||
}
|
||||
}).group_by(|(key, _)| key.clone());
|
||||
|
||||
for (bone, mappings) in bones.into_iter() {
|
||||
let curve = self.generate_bone_curve(mappings.map(|(_, v)| v), csv_morph_order);
|
||||
let path = EntityPath {
|
||||
parts: bone.into_iter().map(Name::new).collect()
|
||||
};
|
||||
clip.add_curve_to_path(path, curve);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_bone_curve(&self, mappings: impl Iterator<Item = (BoneMapping, String)>, csv_morph_order: &HashMap<&str, usize>) -> VariableCurve {
|
||||
let mut keyframe_timestamps = Vec::with_capacity(self.frames.len());
|
||||
let mut keyframes = Vec::with_capacity(self.frames.len());
|
||||
let mappings = mappings.collect::<Vec<_>>();
|
||||
|
||||
let seconds_per_frame = 1.0 / self.fps;
|
||||
for (frame, data) in self.frames.iter().enumerate() {
|
||||
let time = (frame as f32) * seconds_per_frame;
|
||||
keyframe_timestamps.push(time);
|
||||
|
||||
let mut transform = Transform::from_rotation(Quat::from_euler(bevy::math::EulerRot::XYZ, 0., 0., 0.));
|
||||
|
||||
|
||||
println!("TIME: {time}");
|
||||
for (mapping, csv_key) in mappings.iter() {
|
||||
let result = csv_morph_order
|
||||
.get(csv_key.as_str())
|
||||
.copied()
|
||||
.and_then(|i| data.morphs.get(i))
|
||||
.copied()
|
||||
.unwrap_or(0f32);
|
||||
println!("Mapping {csv_key} - {mapping:?} -> {result}");
|
||||
|
||||
let axis = match mapping.axis {
|
||||
Axis::X => Vec3::X,
|
||||
Axis::Y => Vec3::Y,
|
||||
Axis::Z => Vec3::Z,
|
||||
Axis::Vec(v) => v,
|
||||
};
|
||||
let offset = mapping.offset.unwrap_or(0f32).to_radians();
|
||||
|
||||
let result = result * 180f32.to_radians() + offset;
|
||||
|
||||
println!("Rotating {axis:?} by {result:?}");
|
||||
println!("From {}", transform.rotation.xyz());
|
||||
transform.rotate_axis(axis, result);
|
||||
println!("To {}", transform.rotation.xyz());
|
||||
}
|
||||
|
||||
|
||||
keyframes.push(transform.rotation)
|
||||
}
|
||||
|
||||
VariableCurve {
|
||||
keyframe_timestamps,
|
||||
keyframes: Keyframes::Rotation(keyframes),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_morph_curve(
|
||||
&self,
|
||||
mapping: Option<&CSVAnimationMapping>,
|
||||
csv_morph_order: &HashMap<&str, usize>
|
||||
) -> VariableCurve {
|
||||
let (num_mesh_morphs, mesh_to_csv_morph_order) = match mapping {
|
||||
Some(mapping) => {
|
||||
let num_mesh_morphs: usize = mapping.len();
|
||||
let mesh_to_csv_morph_order: Vec<Option<usize>> = mapping
|
||||
.0
|
||||
.iter()
|
||||
.map(|(_, v)| csv_morph_order.get(v.to_string().as_str()).copied())
|
||||
.filter_map(|(mapping, v)| match mapping {
|
||||
MappingType::Morph(_) => Some(csv_morph_order.get(v.to_string().as_str()).copied()),
|
||||
_ => None
|
||||
})
|
||||
.collect();
|
||||
let num_mesh_morphs: usize = mesh_to_csv_morph_order.len();
|
||||
(num_mesh_morphs, mesh_to_csv_morph_order)
|
||||
}
|
||||
None => (
|
||||
@@ -282,7 +355,30 @@ struct ClipInfo {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct CSVAnimationMapping(Vec<(String, String)>);
|
||||
struct CSVAnimationMapping(Vec<(MappingType, String)>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum MappingType {
|
||||
Morph(String),
|
||||
Bone(BoneMapping)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BoneMapping {
|
||||
path: Vec<String>,
|
||||
axis: Axis,
|
||||
offset: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Axis {
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
#[serde(untagged)]
|
||||
Vec(Vec3)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CSVAnimationLoader;
|
||||
@@ -354,12 +450,12 @@ impl AssetLoader for CSVAnimationLoader {
|
||||
.map_err(CSVAnimationLoaderError::MappingReadBytesError)?;
|
||||
let mapping: CSVAnimationMapping = serde_json::from_slice(&mapping)
|
||||
.map_err(CSVAnimationLoaderError::MappingParseError)?;
|
||||
Some(mapping.0)
|
||||
Some(mapping)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(csv_animation.generate_clip(&clip_info.root_name, mapping.as_deref()))
|
||||
Ok(csv_animation.generate_clip(&clip_info.root_name, mapping.as_ref()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -382,10 +478,10 @@ mod tests {
|
||||
|
||||
use bevy::{
|
||||
animation::{AnimationClip, EntityPath, Keyframes},
|
||||
core::Name,
|
||||
core::Name, math::Vec3,
|
||||
};
|
||||
|
||||
use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError};
|
||||
use crate::{CSVAnimation, CSVAnimationMapping, CSVParseError, Timestamp, TimestampParseError};
|
||||
#[test]
|
||||
fn given_an_empty_timestamp_it_returns_an_empty_timestamp_error() {
|
||||
let timestamp = ""
|
||||
@@ -526,7 +622,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let clip: AnimationClip =
|
||||
animation.generate_clip("my_node", Option::<&[(&str, &str)]>::None);
|
||||
animation.generate_clip("my_node", None);
|
||||
|
||||
assert!(clip.duration() - 1.33333 < 0.1);
|
||||
|
||||
@@ -571,11 +667,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mapping = vec![
|
||||
("shape_2", "shape_2"),
|
||||
("shape_1", "shape_1"),
|
||||
("shape_extra", "shape_extra"),
|
||||
];
|
||||
let mapping = CSVAnimationMapping([("\"shape_2\"", "shape_2"),
|
||||
("\"shape_1\"", "shape_1"),
|
||||
("\"shape_extra\"", "shape_extra")].iter().map(|(key, value)| (serde_json::from_str(key).unwrap(), value.to_string())).collect());
|
||||
|
||||
let clip: AnimationClip = animation.generate_clip("my_node", Some(&mapping));
|
||||
|
||||
@@ -609,4 +703,78 @@ mod tests {
|
||||
assert!((keyframes[3] + 0.4).abs() < 0.0001);
|
||||
assert!((keyframes[5] - 0.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_convert_an_animation_into_an_animation_clip_with_bone_mapping() {
|
||||
let animation = CSVAnimation::parse(
|
||||
"timecode, blendshapecount, shape_1, shape_2, bone_x
|
||||
00:00:00:00.00, 0, 0, 1.0, 0
|
||||
00:00:00:01.00, 0, -0.5, -0.4, 0.5
|
||||
00:00:00:02.00, 0, 0, 0, 0.4
|
||||
00:00:01:00.00, 0, 0, 0, 1
|
||||
00:00:01:01.00, 0, 0, 0, -2",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mapping: CSVAnimationMapping = CSVAnimationMapping([("\"shape_2\"", "shape_2"),
|
||||
("{ \"path\": [\"bone\"], \"axis\": [0, 0, 1] }", "shape_1"),
|
||||
("{ \"path\": [\"bone\"], \"axis\": \"X\" }", "bone_x")].iter().map(|(key, value)| {
|
||||
println!("Attempting to convert {key}");
|
||||
(serde_json::from_str(key).unwrap(), value.to_string())
|
||||
}).collect());
|
||||
|
||||
|
||||
let clip: AnimationClip = animation.generate_clip("my_node", Some(&mapping));
|
||||
|
||||
assert!(clip.duration() - 1.33333 < 0.1);
|
||||
|
||||
let curves = clip
|
||||
.get_curves_by_path(&EntityPath {
|
||||
parts: vec![Name::new("my_node")],
|
||||
})
|
||||
.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(), 5);
|
||||
|
||||
assert!((keyframes[0] - 1.0).abs() < 0.0001);
|
||||
assert!((keyframes[1] + 0.4).abs() < 0.0001);
|
||||
|
||||
let curves = clip
|
||||
.get_curves_by_path(&EntityPath {
|
||||
parts: vec![ Name::new("bone")],
|
||||
})
|
||||
.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::Rotation(keyframes) = &curves.keyframes else {
|
||||
panic!("Keyframes aren't rotations")
|
||||
};
|
||||
|
||||
assert_eq!(keyframes.len(), 5);
|
||||
|
||||
println!("Rotations: {:?}", keyframes.iter().map(|v| v.xyz()).collect::<Vec<_>>());
|
||||
assert!((keyframes[0].xyz() - Vec3::new(0., 0., 0.)).length() < 0.0001);
|
||||
assert!((keyframes[1].xyz() - Vec3::new(0.5, 0.5, -0.5)).length() < 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user