-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
5,652 additions
and
4 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
use bevy::{gltf::GltfNode, prelude::*, utils::HashMap}; | ||
use bevy_vrm::{animations::vrm::VRM_ANIMATION_TARGETS, BoneName}; | ||
|
||
use super::{ | ||
mixamo::{MIXAMO_ANIMATION_TARGETS, MIXAMO_BONE_NAMES}, | ||
AnimationName, | ||
}; | ||
|
||
#[derive(Component, Clone)] | ||
pub struct AvatarAnimationClips(pub HashMap<AnimationName, AvatarAnimation>); | ||
|
||
#[derive(Clone)] | ||
pub struct AvatarAnimation { | ||
pub clip: Handle<AnimationClip>, | ||
pub gltf: Handle<Gltf>, | ||
} | ||
|
||
#[derive(Component, Clone)] | ||
pub struct AvatarAnimationNodes(pub HashMap<AnimationName, AnimationNodeIndex>); | ||
|
||
pub(crate) fn load_animation_nodes( | ||
avatars: Query<(Entity, &AvatarAnimationClips), Without<Handle<AnimationGraph>>>, | ||
mut clips: ResMut<Assets<AnimationClip>>, | ||
mut commands: Commands, | ||
mut gltfs: ResMut<Assets<Gltf>>, | ||
mut graphs: ResMut<Assets<AnimationGraph>>, | ||
nodes: Res<Assets<GltfNode>>, | ||
) { | ||
for (entity, animations) in avatars.iter() { | ||
let mut graph = AnimationGraph::default(); | ||
let mut animation_nodes = HashMap::default(); | ||
|
||
let mut failed = false; | ||
|
||
for (name, animation) in animations.0.iter() { | ||
let clip = match clips.get_mut(&animation.clip) { | ||
Some(c) => c, | ||
None => { | ||
failed = true; | ||
break; | ||
} | ||
}; | ||
|
||
let gltf = match gltfs.get_mut(&animation.gltf) { | ||
Some(c) => c, | ||
None => { | ||
failed = true; | ||
break; | ||
} | ||
}; | ||
|
||
let clip_curves = clip.curves_mut(); | ||
|
||
for (name, target) in MIXAMO_ANIMATION_TARGETS.iter() { | ||
// Head transform is set by user's camera. | ||
if *name == BoneName::Head { | ||
continue; | ||
} | ||
|
||
if let Some(mut curves) = clip_curves.remove(target) { | ||
let mut to_remove = Vec::default(); | ||
|
||
for (i, curve) in curves.iter_mut().enumerate() { | ||
if let Keyframes::Translation(translations) = &mut curve.keyframes { | ||
// TODO: Fix translation animations. | ||
// For some reason, all bones get rotated strangely when a translation | ||
// is applied. | ||
to_remove.push(i); | ||
|
||
for item in translations { | ||
item.x /= 100.0; | ||
item.y /= 100.0; | ||
item.z /= 100.0; | ||
|
||
item.x = -item.x; | ||
} | ||
} | ||
if let Keyframes::Rotation(rotations) = &mut curve.keyframes { | ||
// Find target node. | ||
let mixamo_name = MIXAMO_BONE_NAMES[name]; | ||
let node = match gltf.named_nodes.get(mixamo_name) { | ||
Some(n) => n, | ||
None => { | ||
error!("No animation gltf node for {}", mixamo_name); | ||
continue; | ||
} | ||
}; | ||
|
||
// Get all parents, up to root. | ||
let node = nodes.get(node).unwrap(); | ||
let mut parents = Vec::default(); | ||
create_parent_chain(gltf, &nodes, &mut parents, node); | ||
|
||
// Retarget rotation to be in VRM rig space. | ||
let parent_rot = parents | ||
.iter() | ||
.rev() | ||
.fold(Quat::default(), |rot, n| rot * n.transform.rotation); | ||
|
||
let inverse_rot = (parent_rot * node.transform.rotation).inverse(); | ||
|
||
for item in rotations { | ||
*item = parent_rot * *item * inverse_rot; | ||
|
||
item.y = -item.y; | ||
item.w = -item.w; | ||
} | ||
} | ||
} | ||
|
||
for index in to_remove.into_iter().rev() { | ||
curves.remove(index); | ||
} | ||
|
||
let vrm_target = VRM_ANIMATION_TARGETS[name]; | ||
clip_curves.insert(vrm_target, curves); | ||
} | ||
} | ||
|
||
let node = graph.add_clip(animation.clip.clone(), 1.0, graph.root); | ||
animation_nodes.insert(name.clone(), node); | ||
} | ||
|
||
if failed { | ||
continue; | ||
} | ||
|
||
let graph = graphs.add(graph); | ||
|
||
commands | ||
.entity(entity) | ||
.insert((graph, AvatarAnimationNodes(animation_nodes))); | ||
} | ||
} | ||
|
||
fn create_parent_chain( | ||
gltf: &Gltf, | ||
nodes: &Res<Assets<GltfNode>>, | ||
parents: &mut Vec<GltfNode>, | ||
target: &GltfNode, | ||
) { | ||
for handle in gltf.nodes.iter() { | ||
let node = nodes.get(handle).unwrap(); | ||
if node.children.iter().any(|c| c.name == target.name) { | ||
parents.push(node.clone()); | ||
create_parent_chain(gltf, nodes, parents, node); | ||
break; | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use std::{cell::RefCell, rc::Rc, sync::LazyLock}; | ||
|
||
use bevy::{animation::AnimationTargetId, utils::HashMap}; | ||
use bevy_vrm::{animations::target_chain::TargetChain, BoneName}; | ||
|
||
macro_rules! finger { | ||
($chain:ident, $side:ident, $finger_vrm:ident, $finger_chain:ident) => { | ||
paste::paste! { | ||
{ | ||
let mut chain = $chain.clone(); | ||
|
||
chain.push_bone( | ||
BoneName::[<$side $finger_vrm Proximal>], | ||
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "1"), | ||
); | ||
chain.push_bone( | ||
BoneName::[<$side $finger_vrm Intermediate>], | ||
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "2"), | ||
); | ||
chain.push_bone( | ||
BoneName::[<$side $finger_vrm Distal>], | ||
concat!("mixamorig:", stringify!($side), "Hand", stringify!($finger_chain), "3"), | ||
); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
macro_rules! arm { | ||
($chain:ident, $side:ident) => { | ||
paste::paste! { | ||
{ | ||
let mut chain = $chain.clone(); | ||
|
||
chain.push_bone(BoneName::[<$side Shoulder>], concat!("mixamorig:", stringify!($side), "Shoulder")); | ||
chain.push_bone(BoneName::[<$side UpperArm>], concat!("mixamorig:", stringify!($side), "Arm")); | ||
chain.push_bone(BoneName::[<$side LowerArm>], concat!("mixamorig:", stringify!($side), "ForeArm")); | ||
chain.push_bone(BoneName::[<$side Hand>], concat!("mixamorig:", stringify!($side), "Hand")); | ||
|
||
finger!(chain, $side, Thumb, Thumb); | ||
finger!(chain, $side, Index, Index); | ||
finger!(chain, $side, Middle, Middle); | ||
finger!(chain, $side, Ring, Ring); | ||
finger!(chain, $side, Little, Pinky); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
macro_rules! leg { | ||
($chain:ident, $side:ident) => { | ||
paste::paste! { | ||
{ | ||
let mut chain = $chain.clone(); | ||
|
||
chain.push_bone(BoneName::[<$side UpperLeg>], concat!("mixamorig:", stringify!($side), "UpLeg")); | ||
chain.push_bone(BoneName::[<$side LowerLeg>], concat!("mixamorig:", stringify!($side), "Leg")); | ||
chain.push_bone(BoneName::[<$side Foot>], concat!("mixamorig:", stringify!($side), "Foot")); | ||
chain.push_bone(BoneName::[<$side Toes>], concat!("mixamorig:", stringify!($side), "ToeBase")); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
/// Wrapper around [TargetChain]. | ||
/// Allows us to re-use code for both [MIXAMO_ANIMATION_TARGETS] and [MIXAMO_BONE_NAMES]. | ||
#[derive(Clone)] | ||
struct ChainWrapper<'a> { | ||
chain: TargetChain, | ||
names: Rc<RefCell<HashMap<BoneName, &'a str>>>, | ||
targets: Rc<RefCell<HashMap<BoneName, AnimationTargetId>>>, | ||
} | ||
|
||
impl<'a> ChainWrapper<'a> { | ||
fn new(chain: TargetChain) -> Self { | ||
Self { | ||
chain, | ||
names: Default::default(), | ||
targets: Default::default(), | ||
} | ||
} | ||
|
||
fn push_bone(&mut self, bone: BoneName, name: &'a str) { | ||
self.names.borrow_mut().insert(bone, name); | ||
let target = self.chain.push_target(name.to_string()); | ||
self.targets.borrow_mut().insert(bone, target); | ||
} | ||
|
||
fn into_maps( | ||
self, | ||
) -> ( | ||
HashMap<BoneName, &'a str>, | ||
HashMap<BoneName, AnimationTargetId>, | ||
) { | ||
(self.names.take(), self.targets.take()) | ||
} | ||
} | ||
|
||
fn create_chain() -> ChainWrapper<'static> { | ||
let mut chain = TargetChain::default(); | ||
chain.push_target("Armature".to_string()); | ||
|
||
let mut chain = ChainWrapper::new(chain); | ||
|
||
chain.push_bone(BoneName::Hips, "mixamorig:Hips"); | ||
|
||
leg!(chain, Left); | ||
leg!(chain, Right); | ||
|
||
chain.push_bone(BoneName::Spine, "mixamorig:Spine"); | ||
chain.push_bone(BoneName::Chest, "mixamorig:Spine1"); | ||
chain.push_bone(BoneName::UpperChest, "mixamorig:Spine2"); | ||
|
||
arm!(chain, Left); | ||
arm!(chain, Right); | ||
|
||
chain.push_bone(BoneName::Neck, "mixamorig:Neck"); | ||
chain.push_bone(BoneName::Head, "mixamorig:Head"); | ||
|
||
chain | ||
} | ||
|
||
pub static MIXAMO_ANIMATION_TARGETS: LazyLock<HashMap<BoneName, AnimationTargetId>> = | ||
LazyLock::new(|| { | ||
let chain = create_chain(); | ||
let (_, targets) = chain.into_maps(); | ||
targets | ||
}); | ||
|
||
pub static MIXAMO_BONE_NAMES: LazyLock<HashMap<BoneName, &'static str>> = LazyLock::new(|| { | ||
let chain = create_chain(); | ||
let (names, _) = chain.into_maps(); | ||
names | ||
}); | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
// TODO: This test sometimes fails from a panic while running the main bevy schedule | ||
// use bevy::{gltf::GltfPlugin, prelude::*, render::mesh::skinning::SkinnedMeshInverseBindposes}; | ||
// | ||
// use super::*; | ||
// | ||
// #[test] | ||
// fn test_mixamo_targets() { | ||
// let mut app = App::new(); | ||
// | ||
// app.add_plugins(( | ||
// MinimalPlugins, | ||
// AssetPlugin { | ||
// file_path: "../unavi-app/assets".to_string(), | ||
// ..default() | ||
// }, | ||
// AnimationPlugin, | ||
// GltfPlugin::default(), | ||
// )); | ||
// | ||
// app.init_asset::<Scene>(); | ||
// app.init_asset::<SkinnedMeshInverseBindposes>(); | ||
// | ||
// let asset_server = app.world().get_resource::<AssetServer>().unwrap(); | ||
// let _ = asset_server.load::<AnimationClip>("character-animations.glb#Animation0"); | ||
// | ||
// app.add_systems( | ||
// Update, | ||
// |assets: Res<Assets<AnimationClip>>, | ||
// time: Res<Time>, | ||
// mut exit: EventWriter<AppExit>| { | ||
// if time.elapsed_seconds() > 15.0 { | ||
// panic!("Took too long"); | ||
// } | ||
// | ||
// if let Some((_, clip)) = assets.iter().next() { | ||
// for (name, target) in MIXAMO_ANIMATION_TARGETS.iter() { | ||
// assert!(clip.curves_for_target(*target).is_some(), "name={:?}", name); | ||
// } | ||
// | ||
// exit.send_default(); | ||
// } | ||
// }, | ||
// ); | ||
// | ||
// app.run(); | ||
// } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
use bevy::prelude::*; | ||
|
||
pub(crate) mod load; | ||
mod mixamo; | ||
pub(crate) mod weights; | ||
|
||
pub use load::AvatarAnimationNodes; | ||
use weights::{AnimationWeights, TargetAnimationWeights}; | ||
|
||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] | ||
pub enum AnimationName { | ||
Falling, | ||
#[default] | ||
Idle, | ||
Walk, | ||
WalkLeft, | ||
WalkRight, | ||
Other(String), | ||
} | ||
|
||
pub(crate) fn init_animations( | ||
animation_nodes: Query<&Handle<AnimationGraph>, With<AvatarAnimationNodes>>, | ||
mut animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>, | ||
mut commands: Commands, | ||
) { | ||
for (entity, parent) in animation_players.iter_mut() { | ||
let graph = animation_nodes | ||
.get(parent.get()) | ||
.expect("Failed to initialize animation, animation nodes not found"); | ||
|
||
commands.entity(entity).insert(( | ||
AnimationWeights::default(), | ||
TargetAnimationWeights::default(), | ||
graph.clone(), | ||
)); | ||
} | ||
} |
Oops, something went wrong.